diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java b/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java index 652741208d..65c4a161bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java @@ -6,14 +6,15 @@ */ package org.hibernate.annotations; -import java.lang.annotation.ElementType; import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.hibernate.generator.EventType; import org.hibernate.generator.internal.GeneratedGeneration; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.generator.EventType.UPDATE; @@ -56,8 +57,9 @@ import static org.hibernate.generator.EventType.UPDATE; * @see GeneratedColumn */ @ValueGenerationType( generatedBy = GeneratedGeneration.class ) -@Target({ElementType.FIELD, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) +@IdGeneratorType( GeneratedGeneration.class ) +@Target( {FIELD, METHOD} ) +@Retention( RUNTIME ) public @interface Generated { /** * Specifies the events that cause the value to be generated by the diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/GeneratedColumn.java b/hibernate-core/src/main/java/org/hibernate/annotations/GeneratedColumn.java index 97c96fde33..1240f05bb9 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/GeneratedColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/GeneratedColumn.java @@ -27,9 +27,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; * * @see DialectOverride.GeneratedColumn */ +@ValueGenerationType( generatedBy = GeneratedAlwaysGeneration.class ) @Target( {FIELD, METHOD} ) @Retention( RUNTIME ) -@ValueGenerationType(generatedBy = GeneratedAlwaysGeneration.class) public @interface GeneratedColumn { /** * The expression to include in the generated DDL. diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 49f33501fd..8bc0549104 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -3448,7 +3448,7 @@ public abstract class Dialect implements ConversionContext { return false; } - public boolean supportedInsertReturningGeneratedKeys() { + public boolean supportsInsertReturningGeneratedKeys() { return false; } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index bf210199a7..d810e9f1c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -324,7 +324,7 @@ public class OracleDialect extends Dialect { } @Override - public boolean supportedInsertReturningGeneratedKeys() { + public boolean supportsInsertReturningGeneratedKeys() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/InDatabaseGenerator.java b/hibernate-core/src/main/java/org/hibernate/generator/InDatabaseGenerator.java index fecdbc6106..f070c0035d 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/InDatabaseGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/InDatabaseGenerator.java @@ -6,10 +6,16 @@ */ package org.hibernate.generator; +import org.hibernate.Incubating; import org.hibernate.dialect.Dialect; import org.hibernate.id.PostInsertIdentityPersister; -import org.hibernate.id.insert.BasicSelectingDelegate; +import org.hibernate.id.insert.GetGeneratedKeysDelegate; import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; +import org.hibernate.id.insert.InsertReturningDelegate; +import org.hibernate.id.insert.UniqueKeySelectingDelegate; +import org.hibernate.persister.entity.EntityPersister; + +import static org.hibernate.generator.internal.NaturalIdHelper.getNaturalIdPropertyName; /** * A value generated by the database might be generated implicitly, by a trigger, or using @@ -71,15 +77,60 @@ public interface InDatabaseGenerator extends Generator { String[] getReferencedColumnValues(Dialect dialect); /** - * The {@link InsertGeneratedIdentifierDelegate} used to retrieve the generates value if this + * The {@link InsertGeneratedIdentifierDelegate} used to retrieve the generated value if this * object is an identifier generator. *

* This is ignored by {@link org.hibernate.metamodel.mapping.internal.GeneratedValuesProcessor}, - * which handles multiple generators at once. This method arguably breaks the separation of - * concerns between the generator and the coordinating code. + * which handles multiple generators at once. So if this object is not an identifier generator, + * this method is never called. + *

+ * Note that this method arguably breaks the separation of concerns between the generator and + * coordinating code, by specifying how the generated value should be retrieved. + *

+ * The problem solved here is: we want to obtain an insert-generated primary key. But, sadly, + * without already knowing the primary key, there's no completely-generic way to locate the + * just-inserted row to obtain it. + *

+ * We need one of the following things: + *

+ * Alternatively, if the generated id is an identity/"autoincrement" column, we can take + * advantage of special platform-specific functionality to retrieve it. Taking advantage + * of the specialness of identity columns is the job of one particular implementation: + * {@link org.hibernate.id.IdentityGenerator}. And the need for customized behavior for + * identity columns is the reason why this layer-breaking method exists. */ + @Incubating default InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) { - return new BasicSelectingDelegate( persister, persister.getFactory().getJdbcServices().getDialect() ); + Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); + if ( dialect.supportsInsertReturningGeneratedKeys() ) { + return new GetGeneratedKeysDelegate( persister, dialect, false ); + } + else if ( dialect.supportsInsertReturning() ) { + return new InsertReturningDelegate( persister, dialect ); + } + else { + // let's just hope the entity has a @NaturalId! + return new UniqueKeySelectingDelegate( persister, dialect, getUniqueKeyPropertyName( persister ) ); + } + } + + /** + * The name of a property of the entity which may be used to locate the just-{@code insert}ed + * row containing the generated value. Of course, the columns mapped by this property should + * form a unique key of the entity. + *

+ * The default implementation uses the {@link org.hibernate.annotations.NaturalId @NaturalId} + * property, if there is one. + */ + @Incubating + default String getUniqueKeyPropertyName(EntityPersister persister) { + return getNaturalIdPropertyName( persister ); } default boolean generatedByDatabase() { diff --git a/hibernate-core/src/main/java/org/hibernate/generator/internal/NaturalIdHelper.java b/hibernate-core/src/main/java/org/hibernate/generator/internal/NaturalIdHelper.java new file mode 100644 index 0000000000..2564b4ee57 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/generator/internal/NaturalIdHelper.java @@ -0,0 +1,35 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.generator.internal; + +import org.hibernate.id.IdentifierGenerationException; +import org.hibernate.persister.entity.EntityPersister; + +public class NaturalIdHelper { + public static String getNaturalIdPropertyName(EntityPersister persister) { + int[] naturalIdPropertyIndices = persister.getNaturalIdentifierProperties(); + if ( naturalIdPropertyIndices == null ) { + throw new IdentifierGenerationException( + "no natural-id property defined; " + + "need to specify [key] in generator parameters" + ); + } + if ( naturalIdPropertyIndices.length > 1 ) { + throw new IdentifierGenerationException( + "generator does not currently support composite natural-id properties;" + + " need to specify [key] in generator parameters" + ); + } + if ( persister.getEntityMetamodel().isNaturalIdentifierInsertGenerated() ) { + throw new IdentifierGenerationException( + "natural-id also defined as insert-generated; " + + "need to specify [key] in generator parameters" + ); + } + return persister.getPropertyNames()[naturalIdPropertyIndices[0]]; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/id/SelectGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/SelectGenerator.java index 8f59f81fa9..6f8b1f1fbb 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/SelectGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/SelectGenerator.java @@ -11,14 +11,12 @@ import java.util.Properties; import org.hibernate.dialect.Dialect; import org.hibernate.generator.InDatabaseGenerator; import org.hibernate.id.factory.spi.StandardGenerator; -import org.hibernate.id.insert.GetGeneratedKeysDelegate; -import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; -import org.hibernate.id.insert.InsertReturningDelegate; -import org.hibernate.id.insert.UniqueKeySelectingDelegate; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.Type; +import static org.hibernate.generator.internal.NaturalIdHelper.getNaturalIdPropertyName; + /** * A generator that {@code select}s the just-{@code insert}ed row to determine the * column value assigned by the database. The correct row is located using a unique @@ -45,6 +43,21 @@ import org.hibernate.type.Type; * ... * } * } + * However, after a very long working life, this generator is now handing over its + * work to {@link org.hibernate.generator.internal.GeneratedGeneration}, and the + * above code may be written as: + *

{@code
+ * @Entity @Table(name="TableWithPKAssignedByTrigger")
+ * public class TriggeredEntity {
+ *     @Id @Generated(event = INSERT)
+ *     private Long id;
+ *
+ *     @NaturalId
+ *     private String name;
+ *
+ *     ...
+ * }
+ * }
* For tables with identity/autoincrement columns, use {@link IdentityGenerator}. *

* The actual work involved in retrieving the primary key value is the job of @@ -68,49 +81,11 @@ public class SelectGenerator uniqueKeyPropertyName = parameters.getProperty( "key" ); } - /** - * The name of a property of the entity which may be used to locate the just-{@code insert}ed - * row containing the generated value. Of course, the columns mapped by this property should - * form a unique key of the entity. - */ - protected String getUniqueKeyPropertyName(EntityPersister persister) { - if ( uniqueKeyPropertyName != null ) { - return uniqueKeyPropertyName; - } - int[] naturalIdPropertyIndices = persister.getNaturalIdentifierProperties(); - if ( naturalIdPropertyIndices == null ) { - throw new IdentifierGenerationException( - "no natural-id property defined; need to specify [key] in " + - "generator parameters" - ); - } - if ( naturalIdPropertyIndices.length > 1 ) { - throw new IdentifierGenerationException( - "select generator does not currently support composite " + - "natural-id properties; need to specify [key] in generator parameters" - ); - } - if ( persister.getEntityMetamodel().isNaturalIdentifierInsertGenerated() ) { - throw new IdentifierGenerationException( - "natural-id also defined as insert-generated; need to specify [key] " + - "in generator parameters" - ); - } - return persister.getPropertyNames()[naturalIdPropertyIndices[0]]; - } - @Override - public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) { - Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); - if ( dialect.supportedInsertReturningGeneratedKeys() ) { - return new GetGeneratedKeysDelegate( persister, dialect, false ); - } - else if ( dialect.supportsInsertReturning() ) { - return new InsertReturningDelegate( persister, dialect ); - } - else { - return new UniqueKeySelectingDelegate( persister, dialect, getUniqueKeyPropertyName( persister ) ); - } + public String getUniqueKeyPropertyName(EntityPersister persister) { + return uniqueKeyPropertyName != null + ? uniqueKeyPropertyName + : getNaturalIdPropertyName( persister ); } @Override @@ -120,6 +95,6 @@ public class SelectGenerator @Override public String[] getReferencedColumnValues(Dialect dialect) { - return new String[0]; + return null; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/GeneratedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/GeneratedTest.java new file mode 100644 index 0000000000..d70cbb26d3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/GeneratedTest.java @@ -0,0 +1,85 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.generatedkeys.generated; + +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.tool.hbm2ddl.SchemaExport; +import org.hibernate.tool.schema.TargetType; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.EnumSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * @author Steve Ebersole + * @author Marco Belladelli + */ +@DomainModel( + annotatedClasses = MyEntity.class, + xmlMappings = "org/hibernate/orm/test/generatedkeys/selectannotated/MyEntity.hbm.xml" +) +@SessionFactory +@RequiresDialect(OracleDialect.class) +@RequiresDialect(PostgreSQLDialect.class) +@RequiresDialect(MySQLDialect.class) +@RequiresDialect(H2Dialect.class) +@RequiresDialect(DB2Dialect.class) +@RequiresDialect(SQLServerDialect.class) +public class GeneratedTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + MyEntity e1 = new MyEntity( "entity-1" ); + session.persist( e1 ); + // this insert should happen immediately! + assertEquals( Long.valueOf( 1L ), e1.getId(), "id not generated through forced insertion" ); + + MyEntity e2 = new MyEntity( "entity-2" ); + session.persist( e2 ); + assertEquals( Long.valueOf( 2L ), e2.getId(), "id not generated through forced insertion" ); + + session.remove( e1 ); + session.remove( e2 ); + } + ); + } + + @Test + @JiraKey("HHH-15900") + public void testGeneratedKeyNotIdentityColumn(SessionFactoryScope scope) throws IOException { + File output = File.createTempFile( "schema_export", ".sql" ); + output.deleteOnExit(); + + final SchemaExport schemaExport = new SchemaExport(); + schemaExport.setOutputFile( output.getAbsolutePath() ); + schemaExport.execute( + EnumSet.of( TargetType.SCRIPT ), + SchemaExport.Action.CREATE, + scope.getMetadataImplementor() + ); + + String fileContent = new String( Files.readAllBytes( output.toPath() ) ); + assertFalse( fileContent.toLowerCase().contains( "identity" ), "Column was generated as identity" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/H2Trigger.java b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/H2Trigger.java new file mode 100644 index 0000000000..261f2e95e4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/H2Trigger.java @@ -0,0 +1,16 @@ +package org.hibernate.orm.test.generatedkeys.generated; + +import org.h2.tools.TriggerAdapter; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class H2Trigger extends TriggerAdapter { + @Override + public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) throws SQLException { + ResultSet resultSet = conn.createStatement().executeQuery("select coalesce(max(id), 0) from my_entity"); + resultSet.next(); + newRow.updateInt( "id", resultSet.getInt(1) + 1 ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/MyEntity.hbm.xml b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/MyEntity.hbm.xml new file mode 100644 index 0000000000..6495ccc22f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/MyEntity.hbm.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/MyEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/MyEntity.java new file mode 100644 index 0000000000..acff2ada48 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/generated/MyEntity.java @@ -0,0 +1,49 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.generatedkeys.generated; + + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.NaturalId; + +import static org.hibernate.generator.EventType.INSERT; + +/** + * @author Steve Ebersole + */ +@Entity @Table(name="my_entity") +public class MyEntity { + @Id @Generated(event = INSERT) + @ColumnDefault("-666") //workaround for h2 'before insert' triggers being crap + private Long id; + + @NaturalId + private String name; + + public MyEntity() { + } + + public MyEntity(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/select/SelectGeneratorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/select/SelectGeneratorTest.java index 9c1f9ae857..f0d6b0de3c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/select/SelectGeneratorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/select/SelectGeneratorTest.java @@ -45,7 +45,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; public class SelectGeneratorTest { @Test - public void testJDBC3GetGeneratedKeysSupportOnOracle(SessionFactoryScope scope) { + public void test(SessionFactoryScope scope) { scope.inTransaction( session -> { MyEntity e = new MyEntity( "entity-1" ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/selectannotated/SelectGeneratorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/selectannotated/SelectGeneratorTest.java index f7a0a6e792..2a094b979c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/selectannotated/SelectGeneratorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/generatedkeys/selectannotated/SelectGeneratorTest.java @@ -47,7 +47,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; public class SelectGeneratorTest { @Test - public void testJDBC3GetGeneratedKeysSupportOnOracle(SessionFactoryScope scope) { + public void test(SessionFactoryScope scope) { scope.inTransaction( session -> { MyEntity e1 = new MyEntity( "entity-1" );