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
* expects. The primary key columns come before the version column if the
* 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
*/
@ -61,10 +65,12 @@ public @interface SQLDelete {
ResultCheckStyle check() default ResultCheckStyle.NONE;
/**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable
* secondary tables}, defaults to the primary table.
* The name of the table affected by the delete statement. Required when the
* 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
*/

View File

@ -53,10 +53,9 @@ public @interface SQLDeleteAll {
ResultCheckStyle check() default ResultCheckStyle.NONE;
/**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable
* secondary tables}, defaults to the primary table.
* The name of the table affected. Never required.
*
* @return the name of the table
* @return the name of the secondary table
*
* @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
* {@link Generated @Generated(writable=true)} is specified, again forcing
* 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
*/
@ -77,8 +81,10 @@ public @interface SQLInsert {
ResultCheckStyle check() default ResultCheckStyle.NONE;
/**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable
* secondary tables}, defaults to the primary table.
* The name of the table affected by the insert statement. Required when the
* 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
*

View File

@ -45,6 +45,10 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* loses synchronization with the database after the update is executed unless
* {@link Generated @Generated(event=UPDATE, writable=true)} is specified, again
* 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
*/
@ -80,10 +84,12 @@ public @interface SQLUpdate {
ResultCheckStyle check() default ResultCheckStyle.NONE;
/**
* The name of the table in the case of an entity with {@link jakarta.persistence.SecondaryTable
* secondary tables}, defaults to the primary table.
* The name of the table affected by the update statement. Required when the
* 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
*/

View File

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