본문 바로가기

백엔드 개발/JPA

[하이버네이트 유저 가이드 파헤치기] Generated properties - 2.3.18.

반응형

2.3.18. Generated properties

생성된 속성(Generated properties)은 데이터베이스에 의해 생성된 값을 가지는 속성이다.

일반적으로 하이버네이트 애플리케이션은 데이터베이스가 생성하는 속성들을 포함하는 오브젝트를 갱신해야한다.

그러나 해당 속성을 데이터베이스가 생성하는 속성으로 표시하면 하이버네이트가 대신 오브젝트를 갱신한다.

하이버네이트가 생성된 속성(Generated properties)이 정의된 엔티티를 위해 Insert 문이나 Update 문을 발행할 때,

하이버네이트는 즉시 데이터베이스로부터 생성된 값을 탐색하기 위해 select 문을 발행한다.

 

생성된 속성은 추가적으로 insert와 update를 불가능하도록 설정해야한다.

오직 @Version과 @Basic 타입들이 생성된 속성으로 설정할 수 있다.

 

NEVER (the default)

   주어진 속성값은 데이터베이스내에서 생성되지 않는다.

 

INSERT

   주어진 속성값은 insert 될 때 생성된다. 그러나 이후 엔티티가 업데이트될 때는 속성값이 재생성되지 않는다.

  creationTimestamp와 같은 속성이 이 범주에 속한다.

 

ALWAYS

   insert, update할 때 모두 속성값이 생성된다.

 

생성된 속성으로 설정하기 위해, 하이버네이트는 @Generated 어노테이션을 사용한다.

원문
Generated properties are properties that have their values generated by the database. Typically, Hibernate applications needed to refresh objects that contain any properties for which the database was generating values. Marking properties as generated, however, lets the application delegate this responsibility to Hibernate. When Hibernate issues an SQL INSERT or UPDATE for an entity that has defined generated properties, it immediately issues a select to retrieve the generated values.

Properties marked as generated must additionally be non-insertable and non-updateable. Only @Version and @Basic types can be marked as generated.

  NEVER (the default)
      the given property value is not generated within the database.
  INSERT
      the given property value is generated on insert but is not regenerated on subsequent updates.
      Properties like creationTimestamp fall into this category.
  ALWAYS
      the property value is generated both on insert and update.

To mark a property as generated, use The Hibernate specific @Generated annotation

 

@Generated annotation

@Generated 어노테이션은 엔티티가 영속화되거나 수정된 이후 @Generated이 위치한 속성을 가져오는 용도로 사용된다.

이러한 이유로, @Generated 어노테이션은 GenerationTime  enum 값을 허용한다.

 

아래 엔티티를 고려해보자

원문
The @Generated annotation is used so that Hibernate can fetch the currently annotated property after the entity has been persisted or updated. For this reason, the @Generated annotation accepts a GenerationTime enum value.

Considering the following entity:

 

Example 65. @Generated mapping example

@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String firstName;

	private String lastName;

	private String middleName1;

	private String middleName2;

	private String middleName3;

	private String middleName4;

	private String middleName5;

	@Generated( value = GenerationTime.ALWAYS )
	@Column(columnDefinition =
		"AS CONCAT(" +
		"	COALESCE(firstName, ''), " +
		"	COALESCE(' ' + middleName1, ''), " +
		"	COALESCE(' ' + middleName2, ''), " +
		"	COALESCE(' ' + middleName3, ''), " +
		"	COALESCE(' ' + middleName4, ''), " +
		"	COALESCE(' ' + middleName5, ''), " +
		"	COALESCE(' ' + lastName, '') " +
		")")
	private String fullName;

}

 

 

Person 엔티티가 영속화될 때, 하이버네이트는 데이터베이스가 이름, 중간 이름, 성과 관련된 칼럼들을 연결해서 계산한 fullName 칼럼을 가져올 것이다.

 

(원문: When the Person entity is persisted, Hibernate is going to fetch the calculated fullName column from the database, which concatenates the first, middle, and last name.)

 

Example 66. @Generated persist example

Person person = new Person();
person.setId( 1L );
person.setFirstName( "John" );
person.setMiddleName1( "Flávio" );
person.setMiddleName2( "André" );
person.setMiddleName3( "Frederico" );
person.setMiddleName4( "Rúben" );
person.setMiddleName5( "Artur" );
person.setLastName( "Doe" );

entityManager.persist( person );
entityManager.flush();

assertEquals("John Flávio André Frederico Rúben Artur Doe", person.getFullName());
INSERT INTO Person
(
    firstName,
    lastName,
    middleName1,
    middleName2,
    middleName3,
    middleName4,
    middleName5,
    id
)
values
(?, ?, ?, ?, ?, ?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [John]
-- binding parameter [2] as [VARCHAR] - [Doe]
-- binding parameter [3] as [VARCHAR] - [Flávio]
-- binding parameter [4] as [VARCHAR] - [André]
-- binding parameter [5] as [VARCHAR] - [Frederico]
-- binding parameter [6] as [VARCHAR] - [Rúben]
-- binding parameter [7] as [VARCHAR] - [Artur]
-- binding parameter [8] as [BIGINT]  - [1]

SELECT
    p.fullName as fullName3_0_
FROM
    Person p
WHERE
    p.id=?

-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([fullName3_0_] : [VARCHAR]) - [John Flávio André Frederico Rúben Artur Doe]

 

 

Person 엔티티가 업데이트될 때도 영속화될 때와 똑같이 동작한다. 하이버네이트는 엔티티가 수정된 이후 데이터베이스로부터 계산된 값을 가져올 것이다.

 

(원문: The same goes when the Person entity is updated. Hibernate is going to fetch the calculated fullName column from the database after the entity is modified.)

 

Example 67. @Generated update example

Person person = entityManager.find( Person.class, 1L );
person.setLastName( "Doe Jr" );

entityManager.flush();
assertEquals("John Flávio André Frederico Rúben Artur Doe Jr", person.getFullName());
UPDATE
    Person
SET
    firstName=?,
    lastName=?,
    middleName1=?,
    middleName2=?,
    middleName3=?,
    middleName4=?,
    middleName5=?
WHERE
    id=?

-- binding parameter [1] as [VARCHAR] - [John]
-- binding parameter [2] as [VARCHAR] - [Doe Jr]
-- binding parameter [3] as [VARCHAR] - [Flávio]
-- binding parameter [4] as [VARCHAR] - [André]
-- binding parameter [5] as [VARCHAR] - [Frederico]
-- binding parameter [6] as [VARCHAR] - [Rúben]
-- binding parameter [7] as [VARCHAR] - [Artur]
-- binding parameter [8] as [BIGINT]  - [1]

SELECT
    p.fullName as fullName3_0_
FROM
    Person p
WHERE
    p.id=?

-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([fullName3_0_] : [VARCHAR]) - [John Flávio André Frederico Rúben Artur Doe Jr]

 

필자 해설
예제 65에서 fullName 칼럼도 Basic type으로 실제 DB에 칼럼으로 생성된다.
가상 칼럼이 아니다!
단지, @Generated 어노테이션으로 데이터베이스로 부터 생성되는 속성이라는 것을 하이버네이트에게 알려줬기 때문에 엔티티가 영속화되거나 수정되고 난 이후 별도의 fetch 과정을 거쳐서 fullName의 값을 갱신하는 것이다.

 

@GeneratorType annotation

 

@GeneratorType 어노테이션은 현재 어노테이션이 위치한 속성에 사용자 정의 생성자를 제공하여 값을 설정하기 위해 사용한다.

 

이러한 이유로, @GeneratorType 어노테이션은 GenerationTime enum 값과 사용자 정의 ValueGenerator 클래스 타입을 허용한다.

 

아래의 엔티티를 고려해보자

원문
The @GeneratorType annotation is used so that you can provide a custom generator to set the value of the currently annotated property.

For this reason, the @GeneratorType annotation accepts a GenerationTime enum value and a custom ValueGenerator class type.

Considering the following entity:

 

Example 68. @GeneratorType mapping example

public static class CurrentUser {

	public static final CurrentUser INSTANCE = new CurrentUser();

	private static final ThreadLocal<String> storage = new ThreadLocal<>();

	public void logIn(String user) {
		storage.set( user );
	}

	public void logOut() {
		storage.remove();
	}

	public String get() {
		return storage.get();
	}
}

public static class LoggedUserGenerator implements ValueGenerator<String> {

	@Override
	public String generateValue(
			Session session, Object owner) {
		return CurrentUser.INSTANCE.get();
	}
}

@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String firstName;

	private String lastName;

	@GeneratorType( type = LoggedUserGenerator.class, when = GenerationTime.INSERT)
	private String createdBy;

	@GeneratorType( type = LoggedUserGenerator.class, when = GenerationTime.ALWAYS)
	private String updatedBy;

}

 

Person 엔티티가 영속화될 때, 하이버네이트는 createdBy 칼럼을 현재 로그인한 유저로 채울 것이다.

(원문: When the Person entity is persisted, Hibernate is going to populate the createdBy column with the currently logged user.)

 

Example 69. @GeneratorType persist example

CurrentUser.INSTANCE.logIn( "Alice" );

doInJPA( this::entityManagerFactory, entityManager -> {

	Person person = new Person();
	person.setId( 1L );
	person.setFirstName( "John" );
	person.setLastName( "Doe" );

	entityManager.persist( person );
} );

CurrentUser.INSTANCE.logOut();
INSERT INTO Person
(
    createdBy,
    firstName,
    lastName,
    updatedBy,
    id
)
VALUES
(?, ?, ?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Alice]
-- binding parameter [2] as [VARCHAR] - [John]
-- binding parameter [3] as [VARCHAR] - [Doe]
-- binding parameter [4] as [VARCHAR] - [Alice]
-- binding parameter [5] as [BIGINT]  - [1]

 

 

(원문: The same goes when the Person entity is updated. Hibernate is going to populate the updatedBy column with the currently logged user.)

 

Example 70. @GeneratorType update example

CurrentUser.INSTANCE.logIn( "Bob" );

doInJPA( this::entityManagerFactory, entityManager -> {
	Person person = entityManager.find( Person.class, 1L );
	person.setFirstName( "Mr. John" );
} );

CurrentUser.INSTANCE.logOut();
UPDATE Person
SET
    createdBy = ?,
    firstName = ?,
    lastName = ?,
    updatedBy = ?
WHERE
    id = ?

-- binding parameter [1] as [VARCHAR] - [Alice]
-- binding parameter [2] as [VARCHAR] - [Mr. John]
-- binding parameter [3] as [VARCHAR] - [Doe]
-- binding parameter [4] as [VARCHAR] - [Bob]
-- binding parameter [5] as [BIGINT]  - [1]

 

@CreationTimestamp annotation

@CreationTimestamp 어노테이션은 하이버네이트에게 엔티티가 영속화될 때 현재 JVM의 timestamp 값으로 어노테이션이 위치한 엔티티의 속성값을 설정하라고 지시한다.

 

지원하는 속성의 타입은 아래와 같다.

  • java.util.Date
  • java.util.Calendar
  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp
원문
The @CreationTimestamp annotation instructs Hibernate to set the annotated entity attribute with the current timestamp value of the JVM when the entity is being persisted.

The supported property types are:
- java.util.Date
- java.util.Calendar
- java.sql.Date
- java.sql.Time
- java.sql.Timestamp

 

Example 71. @CreationTimestamp mapping example

@Entity(name = "Event")
public static class Event {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@CreationTimestamp
	private Date timestamp;

	//Constructors, getters, and setters are omitted for brevity
}

Event 엔티티가 영속화될 때, 하이버네이트는 기본 timestamp 칼럼에 현재 JVM의 타임스탬프 값을 채워넣을 것이다.

(원문: When the Event entity is persisted, Hibernate is going to populate the underlying timestamp column with the current JVM timestamp value: )

 

Example 72. @CreationTimestamp persist example

Event dateEvent = new Event( );
entityManager.persist( dateEvent );
INSERT INTO Event ("timestamp", id)
VALUES (?, ?)

-- binding parameter [1] as [TIMESTAMP] - [Tue Nov 15 16:24:20 EET 2016]
-- binding parameter [2] as [BIGINT]    - [1]

 

@UpdateTimestamp annotation

 

@UpdateTimestamp 어노테이션은 하이버네이트에게 엔티티가 영속화될 때마다 현재 JVM의 timestamp 값으로 어노테이션이 위치한 엔티티의 속성값을 설정하라고 지시한다.

원문
The @UpdateTimestamp annotation instructs Hibernate to set the annotated entity attribute with the current timestamp value of the JVM when the entity is being persisted.

The supported property types are:
- java.util.Date
- java.util.Calendar
- java.sql.Date
- java.sql.Time
- java.sql.Timestamp

 

Example 73. @UpdateTimestamp mapping example

@Entity(name = "Bid")
public static class Bid {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "updated_on")
	@UpdateTimestamp
	private Date updatedOn;

	@Column(name = "updated_by")
	private String updatedBy;

	private Long cents;

	//Getters and setters are omitted for brevity

}

Bid 엔티티가 영속화될 때, 하이버네이트는 기본 updated_on 칼럼에 현재 JVM 타임스탬프 값을 채워넣을 것이다.

(원문: When the Bid entity is persisted, Hibernate is going to populate the underlying updated_on column with the current JVM timestamp value:)

 

Example 74. @UpdateTimestamp persist example

Bid bid = new Bid();
bid.setUpdatedBy( "John Doe" );
bid.setCents( 150 * 100L );
entityManager.persist( bid );
INSERT INTO Bid (cents, updated_by, updated_on, id)
VALUES (?, ?, ?, ?)

-- binding parameter [1] as [BIGINT]    - [15000]
-- binding parameter [2] as [VARCHAR]   - [John Doe]
-- binding parameter [3] as [TIMESTAMP] - [Tue Apr 18 17:21:46 EEST 2017]
-- binding parameter [4] as [BIGINT]    - [1]

Bid 엔티티가 업데이트될 떄, 하이버네이트는 updated_on 칼럼을 현재 JVM 타임스탬프 값으로 수정할 것이다.

(원문: When updating the Bid entity, Hibernate is going to modify the updated_on column with the current JVM timestamp value:)

 

Example 75. @UpdateTimestamp update example

Bid bid = entityManager.find( Bid.class, 1L );

bid.setUpdatedBy( "John Doe Jr." );
bid.setCents( 160 * 100L );
entityManager.persist( bid );
UPDATE Bid SET
    cents = ?,
    updated_by = ?,
    updated_on = ?
where
    id = ?

-- binding parameter [1] as [BIGINT]    - [16000]
-- binding parameter [2] as [VARCHAR]   - [John Doe Jr.]
-- binding parameter [3] as [TIMESTAMP] - [Tue Apr 18 17:49:24 EEST 2017]
-- binding parameter [4] as [BIGINT]    - [1]

 

 

반응형

 

 

@ValueGenerationType meta-annotation

하이버네이트 4.3 버전에서 도입 된 메타 어노테이션인 @ValueGenerationType는 생성된 속성이나 사용자정의 생성자를 선언하기 위한 새로운 접근법이다.

 

@Generated@ValueGenerationType 메타 어노테이션을 사용하기 위해 개조되었다.

그러나 @ValueGenerationType은 현재 @Generated가 지원하는 것보다 더 많은 기능을 노출하고, 그 기능들을 활용하기위해

간단하게 새로운 생성자 어노테이션를 연결하기만 하면 된다.

 

아래에서 몇가지 예제를 볼 것 인데, @ValueGenerationType 메타 어노테이션은 특정한 생성 전략이 필요한 엔티티 속성을 설정할 때 사용된다.

실질적인 생성 로직은 AnnotationValueGeneration interface를 구현하는 클래스에 꼭 추가되어야만 한다.

 

Hibernate 4.3 introduced the @ValueGenerationType meta-annotation, which is a new approach to declaring generated attributes or customizing generators.

@Generated has been retrofitted to use the @ValueGenerationType meta-annotation. But @ValueGenerationType exposes more features than what @Generated currently supports, and, to leverage some of those features, you’d simply wire up a new generator annotation.

As you’ll see in the following examples, the @ValueGenerationType meta-annotation is used when declaring the custom annotation used to mark the entity properties that need a specific generation strategy.
The actual generation logic must be added to the class that implements the AnnotationValueGeneration interface.

 

Database-generated values

아래 예제에서, 우리가 timestamp 칼럼을 ANSI SQL 표준 함수인 current_timestamp를 호출해서 생성하고 싶다고 가정한다.

(트리거나 DEFAULT values 보다는 current_timestamp를 호출해서 칼럼 생성시 값을 설정하고 싶다..)

원문
For example, let’s say we want the timestamps to be generated by calls to the standard ANSI SQL function 
current_timestamp
 (rather than triggers or DEFAULT values):

 

Example 76. A ValueGenerationType mapping for database generation

@Entity(name = "Event")
public static class Event {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@FunctionCreationTimestamp
	private Date timestamp;

	//Constructors, getters, and setters are omitted for brevity
}

@ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface FunctionCreationTimestamp {}

public static class FunctionCreationValueGeneration
		implements AnnotationValueGeneration<FunctionCreationTimestamp> {

	@Override
	public void initialize(FunctionCreationTimestamp annotation, Class<?> propertyType) {
	}

	/**
	 * Generate value on INSERT
	 * @return when to generate the value
	 */
	public GenerationTiming getGenerationTiming() {
		return GenerationTiming.INSERT;
	}

	/**
	 * Returns null because the value is generated by the database.
	 * @return null
	 */
	public ValueGenerator<?> getValueGenerator() {
		return null;
	}

	/**
	 * Returns true because the value is generated by the database.
	 * @return true
	 */
	public boolean referenceColumnInSql() {
		return true;
	}

	/**
	 * Returns the database-generated value
	 * @return database-generated value
	 */
	public String getDatabaseGeneratedReferencedColumnValue() {
		return "current_timestamp";
	}
}

 

Event 엔티티가 영속화될 때, 하이버네이트는 아래의 SQL 문법을 생성한다.

(원문: When persisting an Event entity, Hibernate generates the following SQL statement: )

INSERT INTO Event ("timestamp", id)
VALUES (current_timestamp, 1)

위 SQL 문법을 보면, ANSI SQL 표준 함수인 current_timestamp는 칼럼 timestamp의 값을 할당하는데 사용된다.

(원문: As you can see, the current_timestamp value was used for assigning the timestamp column value.)

 

In-memory-generated values

만약  칼럼 timestamp의 값을 메모리내에서(데이터베이스가 아니라 애플리케이션 런타임에서) 생성해야한다면, 아래와 같은 매핑이 대신 사용된다.

(원문: If the timestamp value needs to be generated in-memory, the following mapping must be used instead:)

 

Example 77. A ValueGenerationType mapping for in-memory value generation

@Entity(name = "Event")
public static class Event {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@FunctionCreationTimestamp
	private Date timestamp;

	//Constructors, getters, and setters are omitted for brevity
}

@ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface FunctionCreationTimestamp {}

public static class FunctionCreationValueGeneration
		implements AnnotationValueGeneration<FunctionCreationTimestamp> {

	@Override
	public void initialize(FunctionCreationTimestamp annotation, Class<?> propertyType) {
	}

	/**
	 * Generate value on INSERT
	 * @return when to generate the value
	 */
	public GenerationTiming getGenerationTiming() {
		return GenerationTiming.INSERT;
	}

	/**
	 * Returns the in-memory generated value
	 * @return {@code true}
	 */
	public ValueGenerator<?> getValueGenerator() {
		return (session, owner) -> new Date( );
	}

	/**
	 * Returns false because the value is generated by the database.
	 * @return false
	 */
	public boolean referenceColumnInSql() {
		return false;
	}

	/**
	 * Returns null because the value is generated in-memory.
	 * @return null
	 */
	public String getDatabaseGeneratedReferencedColumnValue() {
		return null;
	}
}

 

Event 엔티티가 영속화될 때, 하이버네이트는 아래의 SQL 문법을 생성한다.

 

(원문 : When persisting an Event entity, Hibernate generates the following SQL statement:)

 

INSERT INTO Event ("timestamp", id)
VALUES ('Tue Mar 01 10:58:18 EET 2016', 1)

 

위 SQL 문법을 보면, 생성한 Date 객체값이 timestamp 칼럼의 값으로 할당되는데 사용된다.

(원문: As you can see, the new Date() object value was used for assigning the timestamp column value.)

반응형