Skip to content

@GeneratedValue 코드보기

GeneratedValue는 jakarta에 정의되어있고, Id에 새로운 값을 자동으로 생성해줄 전략을 지정하기 위한 어노테이션이다.

@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface GeneratedValue {
/**
* (Optional) The primary key generation strategy
* that the persistence provider must use to
* generate the annotated entity primary key.
*/
GenerationType strategy() default AUTO;
/**
* (Optional) The name of the primary key generator
* to use as specified in the {@link SequenceGenerator}
* or {@link TableGenerator} annotation.
* <p> Defaults to the id generator supplied by persistence provider.
*/
String generator() default "";
}

실제로 @GeneratedValue가 등록되는 것은

  • org.hibernate.cfg.AnnotationBinderbindClass라는 public method에서 호출하는
  • processIdPropertiesIfNotAlready라는 private method에서 호출하는
  • processElementAnnotations에서 호출하는 processId에서 호출하는
  • BinderHelper.makeIdGenerator이다.

그 외에도 bindClass에서는 Entity를 등록하고 여러 어노테이션을 적용하기 위한 아주 많은 동작들이 이뤄진다.. 코드량이 워낙 방대해서 전부 보긴 힘들지만 한번 보면 괜찮을 것 같은 부분만 추려보았다.

우리의 목적인 GeneratedValue가 어디서 어떻게 나오는지에 대해 유의하며 코드를 읽어보자. (javadoc 링크)

AnnotationBinder.java
public static void bindClass(
XClass clazzToProcess,
Map<XClass, InheritanceState> inheritanceStatePerClass,
MetadataBuildingContext context) throws MappingException {
//@Entity and @MappedSuperclass on the same class leads to a NPE down the road
if ( clazzToProcess.isAnnotationPresent( Entity.class )
&& clazzToProcess.isAnnotationPresent( MappedSuperclass.class ) ) {
throw new AnnotationException( "An entity cannot be annotated with both @Entity and @MappedSuperclass: "
+ clazzToProcess.getName() );
}
// entity의 거의 모든 정보를 담아서 관리하는 객체.
EntityBinder entityBinder = new EntityBinder(
entityAnn,
hibEntityAnn,
clazzToProcess,
persistentClass,
context
);
// superEntity 가져옴
if ( InheritanceType.SINGLE_TABLE.equals( inheritanceState.getType() ) ) {
discriminatorColumn = processSingleTableDiscriminatorProperties(
clazzToProcess,
context,
inheritanceState,
entityBinder
);
} else if ( InheritanceType.JOINED.equals( inheritanceState.getType() ) ) {
discriminatorColumn = processJoinedDiscriminatorProperties(
clazzToProcess,
context,
inheritanceState,
entityBinder
);
} else {
discriminatorColumn = null;
}
// Proxy, BatchSize, Where 어노테이션이 있으면 등록해준다.
entityBinder.setProxy( clazzToProcess.getAnnotation( Proxy.class ) );
entityBinder.setBatchSize( clazzToProcess.getAnnotation( BatchSize.class ) );
entityBinder.setWhere( clazzToProcess.getAnnotation( Where.class ) );
applyCacheSettings( entityBinder, clazzToProcess, context );
// Lazy, DynamicInsert, DynamicUpdate 등의 정보를 등록한다.
entityBinder.bindEntity();
if ( inheritanceState.hasTable() ) {
Check checkAnn = clazzToProcess.getAnnotation( Check.class );
String constraints = checkAnn == null
? null
: checkAnn.constraints();
EntityTableXref denormalizedTableXref = inheritanceState.hasDenormalizedTable()
? context.getMetadataCollector().getEntityTableXref( superEntity.getEntityName() )
: null;
// 스키마, Unique 제약조건 등의 table 정보도 entityBinder에 binding 된다.
entityBinder.bindTable(
schema,
catalog,
table,
uniqueConstraints,
constraints,
denormalizedTableXref
);
}
// todo : sucks that this is separate from RootClass distinction
final boolean isInheritanceRoot = !inheritanceState.hasParents();
final boolean hasSubclasses = inheritanceState.hasSiblings();
if ( InheritanceType.JOINED.equals( inheritanceState.getType() ) ) {
...
}
Set<String> idPropertiesIfIdClass = new HashSet<>();
boolean isIdClass = mapAsIdClass(
inheritanceStatePerClass,
inheritanceState,
persistentClass,
entityBinder,
propertyHolder,
elementsToProcess,
idPropertiesIfIdClass,
context
);
if ( !isIdClass ) {
entityBinder.setWrapIdsInEmbeddedComponents( elementsToProcess.getIdPropertyCount() > 1 );
}
// 여기가 GeneratedValue와 연관있는 부분이다.
processIdPropertiesIfNotAlready(
inheritanceStatePerClass,
context,
persistentClass,
entityBinder,
propertyHolder,
classGenerators,
elementsToProcess,
subclassAndSingleTableStrategy,
idPropertiesIfIdClass
);
...
//add process complementary Table definition (index & all)
entityBinder.processComplementaryTableDefinitions( clazzToProcess.getAnnotation( org.hibernate.annotations.Table.class ) );
entityBinder.processComplementaryTableDefinitions( clazzToProcess.getAnnotation( org.hibernate.annotations.Tables.class ) );
entityBinder.processComplementaryTableDefinitions( tabAnn );
bindCallbacks( clazzToProcess, persistentClass, context ); // entity bind가 끝났음을 알려준다.
}
AnnotationBinder.java
private static void processIdPropertiesIfNotAlready(
Map<XClass, InheritanceState> inheritanceStatePerClass,
MetadataBuildingContext context,
PersistentClass persistentClass,
EntityBinder entityBinder,
PropertyHolder propertyHolder,
HashMap<String, IdentifierGeneratorDefinition> classGenerators,
InheritanceState.ElementsToProcess elementsToProcess,
boolean subclassAndSingleTableStrategy,
Set<String> idPropertiesIfIdClass) {
// IdClass에 지정된 Id List를 저장하고, 하나하나 지워가면서 마지막에 남은게 있으면 AnnotationException을 던진다.
Set<String> missingIdProperties = new HashSet<>( idPropertiesIfIdClass );
for ( PropertyData propertyAnnotatedElement : elementsToProcess.getElements() ) {
String propertyName = propertyAnnotatedElement.getPropertyName();
if ( !idPropertiesIfIdClass.contains( propertyName ) ) {
// id인 필드들에 대해서 process 진행
processElementAnnotations(
propertyHolder,
subclassAndSingleTableStrategy
? Nullability.FORCED_NULL
: Nullability.NO_CONSTRAINT,
propertyAnnotatedElement,
classGenerators,
entityBinder,
false,
false,
false,
context,
inheritanceStatePerClass
);
}
else {
missingIdProperties.remove( propertyName );
}
}
if ( missingIdProperties.size() != 0 ) {
StringBuilder missings = new StringBuilder();
for ( String property : missingIdProperties ) {
missings.append( property ).append( ", " );
}
throw new AnnotationException(
"Unable to find properties ("
+ missings.substring( 0, missings.length() - 2 )
+ ") in entity annotated with @IdClass:" + persistentClass.getEntityName()
);
}
}
AnnotationBinder.java
private static void processElementAnnotations(
PropertyHolder propertyHolder,
Nullability nullability,
PropertyData inferredData,
HashMap<String, IdentifierGeneratorDefinition> classGenerators,
EntityBinder entityBinder,
boolean isIdentifierMapper,
boolean isComponentEmbedded,
boolean inSecondPass,
MetadataBuildingContext context,
Map<XClass, InheritanceState> inheritanceStatePerClass) throws MappingException {
...
if ( isId ) {
//components and regular basic types create SimpleValue objects
final SimpleValue value = ( SimpleValue ) propertyBinder.getValue();
if ( !isOverridden ) {
processId( // Id에 대한 부분을 precess한다.
propertyHolder,
inferredData,
value,
classGenerators,
isIdentifierMapper,
context
);
}
}
...
}
AnnotationBinder.java
private static void processId(
PropertyHolder propertyHolder,
PropertyData inferredData,
SimpleValue idValue,
HashMap<String, IdentifierGeneratorDefinition> classGenerators,
boolean isIdentifierMapper,
MetadataBuildingContext buildingContext) {
...
//manage composite related metadata
//guess if its a component and find id data access (property, field etc)
final boolean isComponent = entityXClass.isAnnotationPresent( Embeddable.class )
|| idXProperty.isAnnotationPresent( EmbeddedId.class );
GeneratedValue generatedValue = idXProperty.getAnnotation( GeneratedValue.class );
String generatorType = generatedValue != null
? generatorType( generatedValue, buildingContext, entityXClass )
: "assigned";
String generatorName = generatedValue != null
? generatedValue.generator()
: BinderHelper.ANNOTATION_STRING_DEFAULT;
if ( isComponent ) {
//a component must not have any generator
generatorType = "assigned";
}
if ( isComponent ) {
//a component must not have any generator
generatorType = "assigned";
}
if ( isGlobalGeneratorNameGlobal( buildingContext ) ) { // global 설정이 있는 경우
buildGenerators( idXProperty, buildingContext );
SecondPass secondPass = new IdGeneratorResolverSecondPass(
idValue,
idXProperty,
generatorType,
generatorName,
buildingContext
);
buildingContext.getMetadataCollector().addSecondPass( secondPass );
}
else { // 일반적인 경우
//clone classGenerator and override with local values
HashMap<String, IdentifierGeneratorDefinition> localGenerators = (HashMap<String, IdentifierGeneratorDefinition>) classGenerators
.clone();
localGenerators.putAll( buildGenerators( idXProperty, buildingContext ) );
BinderHelper.makeIdGenerator( // BinderHelper를 통해 idGenerator를 만들고 등록시킨다.
idValue,
idXProperty,
generatorType,
generatorName,
buildingContext,
localGenerators
);
}
}
BinderHelper.java
public static void makeIdGenerator(
SimpleValue id, // Any value that maps to columns.
XProperty idXProperty, // reflection을 위한 타입
String generatorType,
String generatorName,
MetadataBuildingContext buildingContext,
Map<String, IdentifierGeneratorDefinition> localGenerators) {
log.debugf( "#makeIdGenerator(%s, %s, %s, %s, ...)", id, idXProperty, generatorType, generatorName );
Table table = id.getTable();
table.setIdentifierValue( id );
//generator settings
id.setIdentifierGeneratorStrategy( generatorType );
Properties params = new Properties();
//always settable
params.setProperty(
PersistentIdentifierGenerator.TABLE, table.getName()
);
if ( id.getColumnSpan() == 1 ) {
params.setProperty(
PersistentIdentifierGenerator.PK,
( (org.hibernate.mapping.Column) id.getColumnIterator().next() ).getName()
);
}
// YUCK! but cannot think of a clean way to do this given the string-config based scheme
params.put( PersistentIdentifierGenerator.IDENTIFIER_NORMALIZER, buildingContext.getObjectNameNormalizer() );
params.put( IdentifierGenerator.GENERATOR_NAME, generatorName );
if ( !isEmptyAnnotationValue( generatorName ) ) {
//we have a named generator
IdentifierGeneratorDefinition gen = getIdentifierGenerator( // generator를 찾아옴
generatorName,
idXProperty,
localGenerators,
buildingContext
);
if ( gen == null ) {
throw new AnnotationException( "Unknown named generator (@GeneratedValue#generatorName): " + generatorName );
}
//This is quite vague in the spec but a generator could override the generator choice
String identifierGeneratorStrategy = gen.getStrategy(); // stratge를 가쳐옴
//yuk! this is a hack not to override 'AUTO' even if generator is set
final boolean avoidOverriding =
identifierGeneratorStrategy.equals( "identity" )
|| identifierGeneratorStrategy.equals( "seqhilo" )
|| identifierGeneratorStrategy.equals( MultipleHiLoPerTableGenerator.class.getName() );
if ( generatorType == null || !avoidOverriding ) {
id.setIdentifierGeneratorStrategy( identifierGeneratorStrategy );
}
//checkIfMatchingGenerator(gen, generatorType, generatorName);
for ( Object o : gen.getParameters().entrySet() ) {
Map.Entry elt = (Map.Entry) o;
if ( elt.getKey() == null ) {
continue;
}
params.setProperty( (String) elt.getKey(), (String) elt.getValue() );
}
}
if ( "assigned".equals( generatorType ) ) {
id.setNullValue( "undefined" );
}
id.setIdentifierGeneratorProperties( params );
}

위에서 SimpleValue id에 set했던 부분은 field로 저장된다. id가 값 세팅받는 부분들을 보면, 중요한 정보를 많이 담고있다는 것을 알 수 있다.

  • Table table = id.getTable(); table.setIdentifierValue( id );
  • id.setIdentifierGeneratorStrategy( generatorType );
  • id.setIdentifierGeneratorStrategy( identifierGeneratorStrategy );
  • id.setIdentifierGeneratorProperties( params );
SimpleValue.java
public class SimpleValue implements KeyValue {
private static final CoreMessageLogger log = CoreLogging.messageLogger( SimpleValue.class );
public static final String DEFAULT_ID_GEN_STRATEGY = "assigned";
private final MetadataImplementor metadata;
private final List<Selectable> columns = new ArrayList<>();
private final List<Boolean> insertability = new ArrayList<>();
private final List<Boolean> updatability = new ArrayList<>();
private String typeName;
private Properties typeParameters;
private boolean isVersion;
private boolean isNationalized;
private boolean isLob;
private Properties identifierGeneratorProperties;
private String identifierGeneratorStrategy = DEFAULT_ID_GEN_STRATEGY;
private String nullValue;
private Table table;
private String foreignKeyName;
private String foreignKeyDefinition;
private boolean alternateUniqueKey;
private boolean cascadeDeleteEnabled;
private ConverterDescriptor attributeConverterDescriptor;
private Type type;
...
}

이렇게 생성된 SimpleValue는 SessionFactoryImpl의 생성자에서 가져오고, IdentifierGeneratoridentifierGeneratorProperties에 SimpleValue param들이 넣어져서 identifierGenerators로 등록된다. 이렇게 생성된 SessionFactory는 애플리케이션 전체에서 Session을 만들때 사용되므로, 전체에 적용된다 생각할 수 있다.

SessionFactoryImpl.java
public SessionFactoryImpl(
final MetadataImplementor metadata,
SessionFactoryOptions options,
QueryPlanCache.QueryPlanCreator queryPlanCacheFunction) {
LOG.debug( "Building session factory" );
this.sessionFactoryOptions = options;
this.settings = new Settings( options, metadata );
...
try {
for ( Integrator integrator : serviceRegistry.getService( IntegratorService.class ).getIntegrators() ) {
integrator.integrate( metadata, this, this.serviceRegistry );
integratorObserver.integrators.add( integrator );
}
//Generators:
this.identifierGenerators = new HashMap<>();
metadata.getEntityBindings().stream().filter( model -> !model.isInherited() ).forEach( model -> {
// 이 createIdentifierGenerator라는 함수에서 SimpleValue에 있던 param들을 IdentifierGenerator의 identifierGeneratorProperties로 등록시킨다.
IdentifierGenerator generator = model.getIdentifier().createIdentifierGenerator(
metadata.getIdentifierGeneratorFactory(),
jdbcServices.getJdbcEnvironment().getDialect(),
(RootClass) model
);
generator.initialize( sqlStringGenerationContext );
identifierGenerators.put( model.getEntityName(), generator );
} );
metadata.validate();
}
...
}

이렇게 저장된 identifierGenerators는 엔티티에서 사용될 기본 정보인 EntityMetamodelIdentifierProperty(ID에 대한 설정 정보 클래스) 형태로 담긴다.

원래 String으로 저장되어있던 설정 정보가 createIdentifierGenerator에서 클래스 참조로 바뀐다. IdentifierGenerator를 상속받아서 구현한 경우, 저 안에서 스캔되어서 들어간다고 생각하면 된다.

`IdentifierGenerator` 코드 보기
/**
* The general contract between a class that generates unique
* identifiers and the <tt>Session</tt>. It is not intended that
* this interface ever be exposed to the application. It <b>is</b>
* intended that users implement this interface to provide
* custom identifier generation strategies.<br>
* <br>
* Implementors should provide a public default constructor.<br>
* <br>
* Implementations that accept configuration parameters should
* also implement <tt>Configurable</tt>.
* <br>
* Implementors <em>must</em> be thread-safe
*
* @author Gavin King
*
* @see PersistentIdentifierGenerator
*/
public interface IdentifierGenerator extends Configurable, ExportableProducer {
String ENTITY_NAME = "entity_name";
String JPA_ENTITY_NAME = "jpa_entity_name";
String GENERATOR_NAME = "GENERATOR_NAME";
@Override
default void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
}
@Override
default void registerExportables(Database database) {
}
default void initialize(SqlStringGenerationContext context) {
}
Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException;
default boolean supportsJdbcBatchInserts() {
return true;
}
}

그 entity metadata는 EntityPersister라는 곳에 담겨서 StatelessSession에서 insert가 호출될때, 같이 generate된다.

public abstract class AbstractEntityPersister
implements OuterJoinLoadable, Queryable, ClassMetadata, UniqueKeyLoadable,
SQLLoadable, LazyPropertyInitializer, PostInsertIdentityPersister, Lockable {
...
public IdentifierGenerator getIdentifierGenerator() throws HibernateException {
return entityMetamodel.getIdentifierProperty().getIdentifierGenerator();
}
...
}
StatelessSessionImpl.java
@Override
public Serializable insert(String entityName, Object entity) {
checkOpen();
EntityPersister persister = getEntityPersister( entityName, entity );
Serializable id = persister.getIdentifierGenerator().generate( this, entity ); // 호출되는 부분
Object[] state = persister.getPropertyValues( entity );
if ( persister.isVersioned() ) {
boolean substitute = Versioning.seedVersion(
state,
persister.getVersionProperty(),
persister.getVersionType(),
this
);
if ( substitute ) {
persister.setPropertyValues( entity, state );
}
}
if ( id == IdentifierGeneratorHelper.POST_INSERT_INDICATOR ) {
id = persister.insert( state, entity, this );
}
else {
persister.insert( id, state, entity, this );
}
persister.setIdentifier( entity, id, this );
return id;
}