allow use of SelectGenerator from annotations

- generalize @GenericGenerator to any Generator
- add tests for @GenericGenerator(type=SelectGenerator)
- move some logic for choosing the right InsertGeneratedIdentifierDelegate
  to the generators themselves
This commit is contained in:
Gavin 2022-12-17 17:57:05 +01:00 committed by Gavin King
parent 6536fe0d72
commit e8c2824976
13 changed files with 243 additions and 69 deletions

View File

@ -6,7 +6,7 @@
*/
package org.hibernate.annotations;
import org.hibernate.generator.InMemoryGenerator;
import org.hibernate.generator.Generator;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
@ -19,11 +19,10 @@ import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Defines a named identifier generator, an instance of the interface
* {@link org.hibernate.id.IdentifierGenerator}. This allows the use of
* custom identifier generation strategies beyond those provided by the
* four basic JPA-defined {@linkplain jakarta.persistence.GenerationType
* generation types}.
* Defines a named identifier generator, usually an instance of the interface
* {@link org.hibernate.id.IdentifierGenerator}. This allows the use of custom
* identifier generation strategies beyond those provided by the four basic
* JPA-defined {@linkplain jakarta.persistence.GenerationType generation types}.
* <p>
* A named generator may be associated with an entity class by:
* <ul>
@ -74,17 +73,17 @@ public @interface GenericGenerator {
*/
String name();
/**
* The type of identifier generator, a class implementing {@link InMemoryGenerator}
* The type of identifier generator, a class implementing {@link Generator}
* or, more commonly, {@link org.hibernate.id.IdentifierGenerator}.
*
* @since 6.2
*/
Class<? extends InMemoryGenerator> type() default InMemoryGenerator.class;
Class<? extends Generator> type() default Generator.class;
/**
* The type of identifier generator, the name of either:
* <ul>
* <li>a built-in Hibernate id generator, or
* <li>a custom class implementing {@link InMemoryGenerator}, or, more commonly,
* <li>a custom class implementing {@link Generator}, or, more commonly,
* {@link org.hibernate.id.IdentifierGenerator}.
* </ul>
*

View File

@ -81,6 +81,7 @@ import org.hibernate.cfg.annotations.QueryBinder;
import org.hibernate.dialect.TimeZoneSupport;
import org.hibernate.engine.OptimisticLockStyle;
import org.hibernate.engine.spi.FilterDefinition;
import org.hibernate.generator.Generator;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.GenericsHelper;
import org.hibernate.internal.util.StringHelper;
@ -98,7 +99,6 @@ import org.hibernate.property.access.internal.PropertyAccessStrategyMixedImpl;
import org.hibernate.property.access.spi.PropertyAccessStrategy;
import org.hibernate.resource.beans.spi.ManagedBean;
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.generator.InMemoryGenerator;
import org.hibernate.type.BasicType;
import org.hibernate.type.CustomType;
import org.hibernate.type.descriptor.java.BasicJavaType;
@ -489,7 +489,7 @@ public final class AnnotationBinder {
else if ( generatorAnnotation instanceof GenericGenerator ) {
final GenericGenerator genericGenerator = (GenericGenerator) generatorAnnotation;
definitionBuilder.setName( genericGenerator.name() );
final String strategy = genericGenerator.type().equals(InMemoryGenerator.class)
final String strategy = genericGenerator.type().equals(Generator.class)
? genericGenerator.strategy()
: genericGenerator.type().getName();
definitionBuilder.setStrategy( strategy );

View File

@ -7,7 +7,9 @@
package org.hibernate.generator;
import org.hibernate.dialect.Dialect;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.id.PostInsertIdentityPersister;
import org.hibernate.id.insert.BasicSelectingDelegate;
import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate;
/**
* A value generated by the database might be generated implicitly, by a trigger, or using
@ -69,18 +71,15 @@ public interface InDatabaseGenerator extends Generator {
String[] getReferencedColumnValues(Dialect dialect);
/**
* 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 {@link InsertGeneratedIdentifierDelegate} used to retrieve the generates value if this
* object is an identifier generator.
* <p>
* 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.
*
* @see org.hibernate.id.SelectGenerator
*/
default String getUniqueKeyPropertyName(EntityPersister persister) {
return null;
default InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) {
return new BasicSelectingDelegate( persister, persister.getFactory().getJdbcServices().getDialect() );
}
default boolean generatedByDatabase() {

View File

@ -9,6 +9,9 @@ package org.hibernate.id;
import org.hibernate.dialect.Dialect;
import org.hibernate.id.factory.spi.StandardGenerator;
import org.hibernate.generator.InDatabaseGenerator;
import org.hibernate.id.insert.BasicSelectingDelegate;
import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate;
import org.hibernate.id.insert.InsertReturningDelegate;
/**
* An {@link InDatabaseGenerator} that handles {@code IDENTITY}/"autoincrement" columns
@ -16,11 +19,23 @@ import org.hibernate.generator.InDatabaseGenerator;
* <p>
* Delegates to the {@link org.hibernate.dialect.identity.IdentityColumnSupport} provided
* by the {@linkplain Dialect#getIdentityColumnSupport() dialect}.
* <p>
* The actual work involved in retrieving the primary key value is the job of a
* {@link org.hibernate.id.insert.InsertGeneratedIdentifierDelegate}, either:
* <ul>
* <li>a {@link org.hibernate.id.insert.GetGeneratedKeysDelegate},
* <li>an {@link org.hibernate.id.insert.InsertReturningDelegate}, or a
* <li>a {@link org.hibernate.id.insert.BasicSelectingDelegate}.
* </ul>
*
* @see org.hibernate.dialect.identity.IdentityColumnSupport
* @see org.hibernate.id.insert.InsertGeneratedIdentifierDelegate
*
* @author Christoph Sturm
*/
public class IdentityGenerator
implements PostInsertIdentifierGenerator, BulkInsertionCapableIdentifierGenerator, StandardGenerator {
@Override
public boolean referenceColumnsInSql(Dialect dialect) {
return dialect.getIdentityColumnSupport().hasIdentityInsertKeyword();
@ -30,4 +45,18 @@ public class IdentityGenerator
public String[] getReferencedColumnValues(Dialect dialect) {
return new String[] { dialect.getIdentityColumnSupport().getIdentityInsertString() };
}
@Override
public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) {
Dialect dialect = persister.getFactory().getJdbcServices().getDialect();
if ( persister.getFactory().getSessionFactoryOptions().isGetGeneratedKeysEnabled() ) {
return dialect.getIdentityColumnSupport().buildGetGeneratedKeysDelegate( persister, dialect );
}
else if ( dialect.getIdentityColumnSupport().supportsInsertSelectIdentity() ) {
return new InsertReturningDelegate( persister, dialect );
}
else {
return new BasicSelectingDelegate( persister, dialect );
}
}
}

View File

@ -18,7 +18,7 @@ import java.util.Properties;
import static org.hibernate.generator.EventTypeSets.INSERT_ONLY;
/**
* The counterpart of {@link IdentifierGenerator} for values generated by the database.
* The counterpart to {@link IdentifierGenerator} for values generated by the database.
* This interface is no longer the only way to handle database-generate identifiers.
* Any {@link InDatabaseGenerator} with timing {@link EventTypeSets#INSERT_ONLY} may now
* be used.

View File

@ -11,14 +11,16 @@ 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.InsertGeneratedIdentifierDelegate;
import org.hibernate.id.insert.UniqueKeySelectingDelegate;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
/**
* 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 key of the entity, either:
* column value assigned by the database. The correct row is located using a unique
* key of the entity, either:
* <ul>
* <li>the mapped {@linkplain org.hibernate.annotations.NaturalId} of the entity, or
* <li>a property specified using the parameter named {@code "key"}.
@ -26,11 +28,32 @@ import org.hibernate.type.Type;
* The second approach is provided for backward compatibility with older versions of
* Hibernate.
* <p>
* This generator is intended for use with primary keys assigned by a database trigger
* or something similar, for example:
* <pre>{@code
* @Entity @Table(name="TableWithPKAssignedByTrigger")
* @GenericGenerator(name = "triggered", type = SelectGenerator.class)
* public class TriggeredEntity {
* @Id @GeneratedValue(generator = "triggered")
* private Long id;
*
* @NaturalId
* private String name;
*
* ...
* }
* }</pre>
* For tables with identity/autoincrement columns, use {@link IdentityGenerator}.
* <p>
* The actual work involved in retrieving the primary key value is the job of
* {@link org.hibernate.id.insert.UniqueKeySelectingDelegate}.
* <p>
* Arguably, this class breaks the natural separation of responsibility between the
* {@linkplain InDatabaseGenerator generator} and the coordinating
* code, since it's role is to specify how the generated value is <em>retrieved</em>.
* {@linkplain InDatabaseGenerator generator} and the coordinating code, since its
* role is to specify how the generated value is <em>retrieved</em>.
*
* @see org.hibernate.annotations.NaturalId
* @see org.hibernate.id.insert.UniqueKeySelectingDelegate
*
* @author Gavin King
*/
@ -43,8 +66,12 @@ public class SelectGenerator
uniqueKeyPropertyName = parameters.getProperty( "key" );
}
@Override
public String getUniqueKeyPropertyName(EntityPersister 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.
*/
protected String getUniqueKeyPropertyName(EntityPersister persister) {
if ( uniqueKeyPropertyName != null ) {
return uniqueKeyPropertyName;
}
@ -70,6 +97,12 @@ public class SelectGenerator
return persister.getPropertyNames()[naturalIdPropertyIndices[0]];
}
@Override
public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) {
Dialect dialect = persister.getFactory().getJdbcServices().getDialect();
return new UniqueKeySelectingDelegate( persister, dialect, getUniqueKeyPropertyName( persister ) );
}
@Override
public boolean referenceColumnsInSql(Dialect dialect) {
return false;

View File

@ -13,14 +13,13 @@ import org.hibernate.engine.jdbc.mutation.JdbcValueBindings;
import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.generator.InDatabaseGenerator;
import org.hibernate.jdbc.Expectation;
import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping;
import org.hibernate.sql.model.ast.builder.TableInsertBuilder;
/**
* Each implementation defines a strategy for retrieving a primary key
* {@linkplain InDatabaseGenerator generated by
* {@linkplain org.hibernate.generator.InDatabaseGenerator generated by
* the database} from the database after execution of an {@code insert}
* statement. The generated primary key is usually an {@code IDENTITY}
* column, but in principle it might be something else, for example,
@ -32,9 +31,9 @@ import org.hibernate.sql.model.ast.builder.TableInsertBuilder;
* <li>retrieving the generated identifier value using JDBC.
* </ul>
* The implementation should be written to handle any instance of
* {@link InDatabaseGenerator}.
* {@link org.hibernate.generator.InDatabaseGenerator}.
*
* @see InDatabaseGenerator
* @see org.hibernate.generator.InDatabaseGenerator
*
* @author Steve Ebersole
*/

View File

@ -2743,29 +2743,6 @@ public abstract class AbstractEntityPersister
return includeProperty[getVersionProperty()] || entityMetamodel.isVersionGeneratedByDatabase();
}
public boolean useGetGeneratedKeys() {
return getFactory().getSessionFactoryOptions().isGetGeneratedKeysEnabled();
}
public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate() {
Dialect dialect = getFactory().getJdbcServices().getDialect();
if ( useGetGeneratedKeys() ) {
return dialect.getIdentityColumnSupport().buildGetGeneratedKeysDelegate(this, dialect );
}
else if ( dialect.getIdentityColumnSupport().supportsInsertSelectIdentity() ) {
return new InsertReturningDelegate(this, dialect );
}
else {
return new BasicSelectingDelegate(this, dialect );
}
}
@Override
public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegateForProperty(String uniqueKeyPropertyName) {
Dialect dialect = getFactory().getJdbcServices().getDialect();
return new UniqueKeySelectingDelegate( this, dialect, uniqueKeyPropertyName );
}
@Override
public String getIdentitySelectString() {
//TODO: cache this in an instvar
@ -3082,10 +3059,7 @@ public abstract class AbstractEntityPersister
private void doLateInit() {
if ( isIdentifierAssignedByInsert() ) {
final InDatabaseGenerator generator = (InDatabaseGenerator) getGenerator();
final String uniqueKeyPropertyName = generator.getUniqueKeyPropertyName(this);
identityDelegate = uniqueKeyPropertyName == null
? getGeneratedIdentifierDelegate()
: getGeneratedIdentifierDelegateForProperty( uniqueKeyPropertyName );
identityDelegate = generator.getGeneratedIdentifierDelegate( this );
}
tableMappings = buildTableMappings();

View File

@ -1015,14 +1015,6 @@ public interface EntityPersister extends EntityMappingType, RootTableGroupProduc
boolean canUseReferenceCacheEntries();
default InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate() {
return null;
}
default InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegateForProperty(String uniqueKeyPropertyName) {
return null;
}
/**
* The property name of the "special" identifier property in HQL
*

View File

@ -25,6 +25,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.generator.EventType;
import org.hibernate.id.BulkInsertionCapableIdentifierGenerator;
import org.hibernate.id.OptimizableGenerator;
import org.hibernate.id.PostInsertIdentityPersister;
import org.hibernate.id.enhanced.Optimizer;
import org.hibernate.id.insert.Binder;
import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate;
@ -559,10 +560,8 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio
if ( generator.generatedByDatabase() ) {
final InDatabaseGenerator databaseGenerator = (InDatabaseGenerator) generator;
final String uniqueKeyPropertyName = databaseGenerator.getUniqueKeyPropertyName( entityPersister );
final InsertGeneratedIdentifierDelegate identifierDelegate = uniqueKeyPropertyName == null
? entityPersister.getGeneratedIdentifierDelegate()
: entityPersister.getGeneratedIdentifierDelegateForProperty( uniqueKeyPropertyName );
final InsertGeneratedIdentifierDelegate identifierDelegate =
databaseGenerator.getGeneratedIdentifierDelegate( (PostInsertIdentityPersister) entityPersister );
final String finalSql = identifierDelegate.prepareIdentifierGeneratingInsert( jdbcInsert.getSqlString() );
final BasicEntityIdentifierMapping identifierMapping = (BasicEntityIdentifierMapping) entityDescriptor.getIdentifierMapping();
final ValueBinder jdbcValueBinder = identifierMapping.getJdbcMapping().getJdbcValueBinder();

View File

@ -0,0 +1,31 @@
<?xml version="1.0"?>
<!--
~ 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
-->
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="org.hibernate.orm.test.generatedkeys.select" default-access="field">
<database-object>
<create>
<![CDATA[CREATE OR REPLACE TRIGGER t_i_my_entity
BEFORE INSERT ON my_entity
FOR EACH ROW
BEGIN
select nvl( max(id), 0 ) + 1
into :new.id
from my_entity;
END;]]>
</create>
<drop>
<![CDATA[DROP TRIGGER t_i_my_entity]]>
</drop>
<dialect-scope name="org.hibernate.dialect.OracleDialect"/>
</database-object>
</hibernate-mapping>

View File

@ -0,0 +1,48 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.generatedkeys.selectannotated;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.NaturalId;
import org.hibernate.id.SelectGenerator;
/**
* @author <a href="mailto:steve@hibernate.org">Steve Ebersole </a>
*/
@Entity @Table(name="my_entity")
@GenericGenerator(name = "triggered", type = SelectGenerator.class)
public class MyEntity {
@Id @GeneratedValue(generator = "triggered")
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;
}
}

View File

@ -0,0 +1,71 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.generatedkeys.selectannotated;
import org.hibernate.dialect.OracleDialect;
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(value = OracleDialect.class)
public class SelectGeneratorTest {
@Test
public void testJDBC3GetGeneratedKeysSupportOnOracle(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
MyEntity e = new MyEntity( "entity-1" );
session.persist( e );
// this insert should happen immediately!
assertEquals( Long.valueOf( 1L ), e.getId(), "id not generated through forced insertion" );
session.remove( e );
}
);
}
@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" );
}
}