HHH-17848 tolerate primary table name in @SqlXxxx annotations

just cleaning up a TODO I left behind a while ago
This commit is contained in:
Gavin King 2024-03-14 10:53:57 +01:00
parent 6c4aa400d4
commit 557a4f16da
6 changed files with 147 additions and 33 deletions

View File

@ -26,6 +26,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@code ?} parameters that Hibernate expects, in the exact order Hibernate * {@code ?} parameters that Hibernate expects, in the exact order Hibernate
* expects. The primary key columns come before the version column if the * expects. The primary key columns come before the version column if the
* entity is versioned. * entity is versioned.
* <p>
* If an entity has {@linkplain jakarta.persistence.SecondaryTable secondary
* tables}, it may have a {@code @SQLDelete} annotation for each secondary table.
* The {@link #table} member must specify the name of the secondary table.
* *
* @author Laszlo Benke * @author Laszlo Benke
*/ */
@ -61,10 +65,12 @@ public @interface SQLDelete {
ResultCheckStyle check() default ResultCheckStyle.NONE; ResultCheckStyle check() default ResultCheckStyle.NONE;
/** /**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable * The name of the table affected by the delete statement. Required when the
* secondary tables}, defaults to the primary table. * statement affects a {@linkplain jakarta.persistence.SecondaryTable secondary
* table} of an entity. Not required for collections nor when the insert statement
* affects the primary table of an entity.
* *
* @return the name of the table * @return the name of the secondary table
* *
* @since 6.2 * @since 6.2
*/ */

View File

@ -53,10 +53,9 @@ public @interface SQLDeleteAll {
ResultCheckStyle check() default ResultCheckStyle.NONE; ResultCheckStyle check() default ResultCheckStyle.NONE;
/** /**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable * The name of the table affected. Never required.
* secondary tables}, defaults to the primary table.
* *
* @return the name of the table * @return the name of the secondary table
* *
* @since 6.2 * @since 6.2
*/ */

View File

@ -42,6 +42,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* loses synchronization with the database after the insert is executed unless * loses synchronization with the database after the insert is executed unless
* {@link Generated @Generated(writable=true)} is specified, again forcing * {@link Generated @Generated(writable=true)} is specified, again forcing
* Hibernate to reread the state of the entity after each insert. * Hibernate to reread the state of the entity after each insert.
* <p>
* If an entity has {@linkplain jakarta.persistence.SecondaryTable secondary
* tables}, it may have a {@code @SQLInsert} annotation for each secondary table.
* The {@link #table} member must specify the name of the secondary table.
* *
* @author Laszlo Benke * @author Laszlo Benke
*/ */
@ -77,8 +81,10 @@ public @interface SQLInsert {
ResultCheckStyle check() default ResultCheckStyle.NONE; ResultCheckStyle check() default ResultCheckStyle.NONE;
/** /**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable * The name of the table affected by the insert statement. Required when the
* secondary tables}, defaults to the primary table. * statement affects a {@linkplain jakarta.persistence.SecondaryTable secondary
* table} of an entity. Not required for collections nor when the insert statement
* affects the primary table of an entity.
* *
* @return the name of the secondary table * @return the name of the secondary table
* *

View File

@ -45,6 +45,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* loses synchronization with the database after the update is executed unless * loses synchronization with the database after the update is executed unless
* {@link Generated @Generated(event=UPDATE, writable=true)} is specified, again * {@link Generated @Generated(event=UPDATE, writable=true)} is specified, again
* forcing Hibernate to reread the state of the entity after each update. * forcing Hibernate to reread the state of the entity after each update.
* <p>
* If an entity has {@linkplain jakarta.persistence.SecondaryTable secondary
* tables}, it may have a {@code @SQLUpdate} annotation for each secondary table.
* The {@link #table} member must specify the name of the secondary table.
* *
* @author Laszlo Benke * @author Laszlo Benke
*/ */
@ -80,10 +84,12 @@ public @interface SQLUpdate {
ResultCheckStyle check() default ResultCheckStyle.NONE; ResultCheckStyle check() default ResultCheckStyle.NONE;
/** /**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable * The name of the table affected by the update statement. Required when the
* secondary tables}, defaults to the primary table. * statement affects a {@linkplain jakarta.persistence.SecondaryTable secondary
* table} of an entity. Not required for collections nor when the insert statement
* affects the primary table of an entity.
* *
* @return the name of the table * @return the name of the secondary table
* *
* @since 6.2 * @since 6.2
*/ */

View File

@ -230,8 +230,11 @@ public class EntityBinder {
final EntityBinder entityBinder = new EntityBinder( clazzToProcess, persistentClass, context ); final EntityBinder entityBinder = new EntityBinder( clazzToProcess, persistentClass, context );
entityBinder.bindEntity(); entityBinder.bindEntity();
entityBinder.handleClassTable( inheritanceState, superEntity ); entityBinder.bindSubselect(); // has to happen before table binding
entityBinder.handleSecondaryTables(); entityBinder.bindTables( inheritanceState, superEntity );
entityBinder.bindCustomSql(); // has to happen after table binding
entityBinder.bindSynchronize();
entityBinder.bindFilters();
entityBinder.handleCheckConstraints(); entityBinder.handleCheckConstraints();
final PropertyHolder holder = buildPropertyHolder( final PropertyHolder holder = buildPropertyHolder(
clazzToProcess, clazzToProcess,
@ -246,7 +249,7 @@ public class EntityBinder {
final InFlightMetadataCollector collector = context.getMetadataCollector(); final InFlightMetadataCollector collector = context.getMetadataCollector();
if ( persistentClass instanceof RootClass ) { if ( persistentClass instanceof RootClass ) {
collector.addSecondPass( new CreateKeySecondPass( (RootClass) persistentClass ) ); collector.addSecondPass( new CreateKeySecondPass( (RootClass) persistentClass ) );
bindSoftDelete( clazzToProcess, (RootClass) persistentClass, inheritanceState, context ); bindSoftDelete( clazzToProcess, (RootClass) persistentClass, context );
} }
if ( persistentClass instanceof Subclass) { if ( persistentClass instanceof Subclass) {
assert superEntity != null; assert superEntity != null;
@ -262,6 +265,11 @@ public class EntityBinder {
entityBinder.callTypeBinders( persistentClass ); entityBinder.callTypeBinders( persistentClass );
} }
private void bindTables(InheritanceState inheritanceState, PersistentClass superEntity) {
handleClassTable( inheritanceState, superEntity );
handleSecondaryTables();
}
private static void checkOverrides(XClass clazzToProcess, PersistentClass superEntity) { private static void checkOverrides(XClass clazzToProcess, PersistentClass superEntity) {
if ( superEntity != null ) { if ( superEntity != null ) {
//TODO: correctly handle compound paths (embeddables) //TODO: correctly handle compound paths (embeddables)
@ -312,28 +320,24 @@ public class EntityBinder {
private static void bindSoftDelete( private static void bindSoftDelete(
XClass xClass, XClass xClass,
RootClass rootClass, RootClass rootClass,
InheritanceState inheritanceState,
MetadataBuildingContext context) { MetadataBuildingContext context) {
// todo (soft-delete) : do we assume all package-level registrations are already available? // todo (soft-delete) : do we assume all package-level registrations are already available?
// or should this be a "second pass"? // or should this be a "second pass"?
final SoftDelete softDelete = extractSoftDelete( xClass, rootClass, inheritanceState, context ); final SoftDelete softDelete = extractSoftDelete( xClass, rootClass, context );
if ( softDelete == null ) { if ( softDelete != null ) {
return; SoftDeleteHelper.bindSoftDeleteIndicator(
softDelete,
rootClass,
rootClass.getRootTable(),
context
);
} }
SoftDeleteHelper.bindSoftDeleteIndicator(
softDelete,
rootClass,
rootClass.getRootTable(),
context
);
} }
private static SoftDelete extractSoftDelete( private static SoftDelete extractSoftDelete(
XClass xClass, XClass xClass,
RootClass rootClass, RootClass rootClass,
InheritanceState inheritanceState,
MetadataBuildingContext context) { MetadataBuildingContext context) {
final SoftDelete fromClass = xClass.getAnnotation( SoftDelete.class ); final SoftDelete fromClass = xClass.getAnnotation( SoftDelete.class );
if ( fromClass != null ) { if ( fromClass != null ) {
@ -1332,9 +1336,7 @@ public class EntityBinder {
ensureNoMutabilityPlan(); ensureNoMutabilityPlan();
bindCustomPersister(); bindCustomPersister();
bindCustomSql(); bindCustomLoader();
bindSynchronize();
bindFilters();
registerImportName(); registerImportName();
@ -1381,9 +1383,12 @@ public class EntityBinder {
} }
private void bindCustomSql() { private void bindCustomSql() {
//TODO: tolerate non-empty table() member here if it explicitly names the main table final String primaryTableName = persistentClass.getTable().getName();
final SQLInsert sqlInsert = findMatchingSqlAnnotation( "", SQLInsert.class, SQLInserts.class ); SQLInsert sqlInsert = findMatchingSqlAnnotation( primaryTableName, SQLInsert.class, SQLInserts.class );
if ( sqlInsert == null ) {
sqlInsert = findMatchingSqlAnnotation( "", SQLInsert.class, SQLInserts.class );
}
if ( sqlInsert != null ) { if ( sqlInsert != null ) {
persistentClass.setCustomSQLInsert( persistentClass.setCustomSQLInsert(
sqlInsert.sql().trim(), sqlInsert.sql().trim(),
@ -1395,7 +1400,10 @@ public class EntityBinder {
} }
} }
final SQLUpdate sqlUpdate = findMatchingSqlAnnotation( "", SQLUpdate.class, SQLUpdates.class ); SQLUpdate sqlUpdate = findMatchingSqlAnnotation( primaryTableName, SQLUpdate.class, SQLUpdates.class );
if ( sqlUpdate == null ) {
sqlUpdate = findMatchingSqlAnnotation( "", SQLUpdate.class, SQLUpdates.class );
}
if ( sqlUpdate != null ) { if ( sqlUpdate != null ) {
persistentClass.setCustomSQLUpdate( persistentClass.setCustomSQLUpdate(
sqlUpdate.sql().trim(), sqlUpdate.sql().trim(),
@ -1407,7 +1415,10 @@ public class EntityBinder {
} }
} }
final SQLDelete sqlDelete = findMatchingSqlAnnotation( "", SQLDelete.class, SQLDeletes.class ); SQLDelete sqlDelete = findMatchingSqlAnnotation( primaryTableName, SQLDelete.class, SQLDeletes.class );
if ( sqlDelete == null ) {
sqlDelete = findMatchingSqlAnnotation( "", SQLDelete.class, SQLDeletes.class );
}
if ( sqlDelete != null ) { if ( sqlDelete != null ) {
persistentClass.setCustomSQLDelete( persistentClass.setCustomSQLDelete(
sqlDelete.sql().trim(), sqlDelete.sql().trim(),
@ -1438,12 +1449,16 @@ public class EntityBinder {
persistentClass.setLoaderName( loaderName ); persistentClass.setLoaderName( loaderName );
QueryBinder.bindQuery( loaderName, hqlSelect, context ); QueryBinder.bindQuery( loaderName, hqlSelect, context );
} }
}
private void bindCustomLoader() {
final Loader loader = annotatedClass.getAnnotation( Loader.class ); final Loader loader = annotatedClass.getAnnotation( Loader.class );
if ( loader != null ) { if ( loader != null ) {
persistentClass.setLoaderName( loader.namedQuery() ); persistentClass.setLoaderName( loader.namedQuery() );
} }
}
private void bindSubselect() {
final Subselect subselect = annotatedClass.getAnnotation( Subselect.class ); final Subselect subselect = annotatedClass.getAnnotation( Subselect.class );
if ( subselect != null ) { if ( subselect != null ) {
this.subselect = subselect.value(); this.subselect = subselect.value();

View File

@ -0,0 +1,82 @@
package org.hibernate.orm.test.customsql;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.SecondaryTable;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLInsert;
import org.hibernate.annotations.SQLUpdate;
import org.hibernate.jdbc.Expectation;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SessionFactory
@DomainModel(annotatedClasses = CustomSqlPrimaryTableExplicitTest.Custom.class)
public class CustomSqlPrimaryTableExplicitTest {
@Test
public void testCustomSql(SessionFactoryScope scope) {
Custom c = new Custom();
c.name = "name";
c.text = "text";
scope.inTransaction(s->{
s.persist(c);
s.flush();
s.clear();
Custom cc = s.find(Custom.class, c.id);
assertEquals(cc.text, "TEXT");
assertEquals(cc.name, "NAME");
cc.name = "eman";
cc.text = "more text";
s.flush();
s.clear();
cc = s.find(Custom.class, c.id);
assertEquals(cc.text, "MORE TEXT");
assertEquals(cc.name, "EMAN");
s.remove(cc);
s.flush();
s.clear();
cc = s.find(Custom.class, c.id);
assertEquals(cc.text, "DELETED");
assertEquals(cc.name, "DELETED");
});
}
@Entity
@Table(name = "CustomPrimary")
@SecondaryTable(name = "CustomSecondary")
@SQLInsert(table = "CustomPrimary",
sql="insert into CustomPrimary (name, revision, id) values (upper(?),?,?)",
verify = Expectation.RowCount.class)
@SQLInsert(table = "CustomSecondary",
sql="insert into CustomSecondary (text, id) values (upper(?),?)",
verify = Expectation.None.class)
@SQLUpdate(table = "CustomPrimary",
sql="update CustomPrimary set name = upper(?), revision = ? where id = ? and revision = ?",
verify = Expectation.RowCount.class)
@SQLUpdate(table = "CustomSecondary",
sql="update CustomSecondary set text = upper(?) where id = ?",
verify = Expectation.None.class)
@SQLDelete(table = "CustomPrimary",
sql="update CustomPrimary set name = 'DELETED' where id = ? and revision = ?",
verify = Expectation.RowCount.class)
@SQLDelete(table = "CustomSecondary",
sql="update CustomSecondary set text = 'DELETED' where id = ?",
verify = Expectation.None.class)
static class Custom {
@Id @GeneratedValue
Long id;
@Version @Column(name = "revision")
int version;
String name;
@Column(table = "CustomSecondary")
String text;
}
}