데이터베이스/Hibernate

[번역] 2-2. Domain 모델 (Enum, UUID, Date, Attribute, Generated Properties)

위들 wedul 2020. 11. 17. 22:52
반응형

2.3.7 Enums 매핑

Hibernate는 기본값 유형으로써 다양한 방법으로 Java Enum의 매핑을 지원한다.

 

@Enumrated

기본적인 JPAdml enums의 매핑 방법은 @Enumrated 또는 @MapKeyEnumrated 애노테이션을 통해 javax.persistence.EnumType에 표시된 두 가지 전략 중 하나에 따라 enum 값이 저장된다는 원칙에 따라 동작한다.

 

 

ORDINAL

- java.lang.Enum#ordinal에 기재된대로 Enum 클래스 내에서 Enum값의 순서에 따라 저장된다

 

STRING

- java.lang.Enum#name 방식에 기재에 따라 Enum값의 이름에 따라서 저장된다.

 

 

아래 예시로 PhoneType이라는 Enum이 있다고 가정해보자.

public enum PhoneType {
    LAND_LINE,
    MOBILE;
}

 

ORDINAL 방식

ORDINAL 방식에서는 INTEGER typedml nullable phone_type 컬럼과 값이 매핑된다.

 

NULL

- null value

 

0

- LAND_LINE과 매핑

 

1

- MOBILE 매핑

 

 

@Enumrated(ORDINAL) 예시

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

	@Id
	private Long id;

	@Column(name = "phone_number")
	private String number;

	@Enumerated(EnumType.ORDINAL)
	@Column(name = "phone_type")
	private PhoneType type;

	//Getters and setters are omitted for brevity

}

entity가 다음과 같을 때 Hibernate는 아래처럼 SQL 구문을 생성한다.

Phone phone = new Phone( );
phone.setId( 1L );
phone.setNumber( "123-456-78990" );
phone.setType( PhoneType.MOBILE );
entityManager.persist( phone );

// 숫자 2를 입력
INSERT INTO Phone (phone_number, phone_type, id)
VALUES ('123-456-78990', 2, 1)

 

 

STRING 전략

STRING 전략에서는 phone_type 컬럼은 nullable VARCHAR 타입 컬럼과 값이 매핑된다.

 

NULL

- null value

 

LAND_TIME

- LAND_TIME enum에 매핑

 

MOBILE

- MOBILE enum에 매핑

 

 

@Enumrated(STRING) 예시

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

	@Id
	private Long id;

	@Column(name = "phone_number")
	private String number;

	@Enumerated(EnumType.STRING)
	@Column(name = "phone_type")
	private PhoneType type;

	//Getters and setters are omitted for brevity

}

@Enumrated(ORDINAL) 예시와 같은 entity가 동일하게 존재한다고 했을 때 @Enumrated(STRING) 인스턴스일 경우에 Hibernate는 아래와 같은 SQL 문장을 생성한다.

INSERT INTO Phone (phone_number, phone_type, id)
VALUES ('123-456-78990', 'MOBILE', 1)

 

 

 

AttributeConverter

'M'과 'F'의 코드로 저장되어 있는 Gender enum을 살펴보자.

public enum Gender {

    MALE( 'M' ),
    FEMALE( 'F' );

    private final char code;

    Gender(char code) {
        this.code = code;
    }

    public static Gender fromCode(char code) {
        if ( code == 'M' || code == 'm' ) {
            return MALE;
        }
        if ( code == 'F' || code == 'f' ) {
            return FEMALE;
        }
        throw new UnsupportedOperationException(
            "The code " + code + " is not supported!"
        );
    }

    public char getCode() {
        return code;
    }
}

JPA 2.1 AttributeConverter를 사용하여 enum 값을 매핑 할 수 있다.

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

	@Id
	private Long id;

	private String name;

	@Convert( converter = GenderConverter.class )
	public Gender gender;

	//Getters and setters are omitted for brevity

}


// 별도 컨버터를 이용하여 DB <-> Enum 사이에 값 매핑
@Converter
public static class GenderConverter
		implements AttributeConverter<Gender, Character> {

	public Character convertToDatabaseColumn( Gender value ) {
		if ( value == null ) {
			return null;
		}

		return value.getCode();
	}

	public Gender convertToEntityAttribute( Character value ) {
		if ( value == null ) {
			return null;
		}

		return Gender.fromCode( value );
	}
}

gender 컬럼은 CHAR 타입에 nullable한 컬럼이고 값 매핑은 아래와 같이 동작한다.

 

NULL

- null value

 

'M'

- MALE enum과 매핑된다.

 

'F'

- FEMALE값과 매핑된다.

 

더 자세한 내용은 AttribueConverter 영역에서 확인해보자.

 

*추가설명*

JPA에서는 @Enumrated 애트리뷰트가 붙어있는 필드에 대해서는 AttributeConverter 사용을 허락하지 않는다.

그래서 만약 AttributeConverter를 사용하고 싶다면 @Enumrated 애트리뷰트를 사용하면 안된다.

 

 

 

 

데이터 필터링을 위해서 쿼리 파라미터로써 AttributeConverter entity 속성을 사용

AttributeConverter를 사용하는 필드를 필터링하는 방식에 대한 예시를 살펴보자.

 

Photo entity를 AttributeConverter와 함께 사용하는 예

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

	@Id
	private Integer id;

	private String name;

	@Convert(converter = CaptionConverter.class)
	private Caption caption;

	//Getters and setters are omitted for brevity
}

그리고 converter가 적용된 Caption 클래스의 구현 내용과 Caption Object에 Attribute 선언된 CaptionConverter의 구현내용은 다음과 같다.

public static class Caption {

	private String text;

	public Caption(String text) {
		this.text = text;
	}

	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Caption caption = (Caption) o;
		return text != null ? text.equals( caption.text ) : caption.text == null;

	}

	@Override
	public int hashCode() {
		return text != null ? text.hashCode() : 0;
	}
}

// caption converter
public static class CaptionConverter
		implements AttributeConverter<Caption, String> {

	@Override
	public String convertToDatabaseColumn(Caption attribute) {
		return attribute.getText();
	}

	@Override
	public Caption convertToEntityAttribute(String dbData) {
		return new Caption( dbData );
	}
}

 

일반적으로 우리는 Caption entity 속성을 언급할 때, String 형태인 현재 경우에서는 dbData caption 표현방식만 사용이 가능하다.

 

ex) DB data 표현 방식을 사용한 Caption 속성 필터링 예제

Photo photo = entityManager.createQuery(
	"select p " +
	"from Photo p " +
	"where upper(caption) = upper(:caption) ", Photo.class )
.setParameter( "caption", "Nicolae Grigorescu" )
.getSingleResult();

String 형태가 아닌 Java Object 형태의 Caption 표현 방식을 사용하기 위해서는 연관된 Hibernate Type을 가져와야한다.

 

ex) Java Object 표현 방식을 사용한 Caption 속성 필터링 (파라미터 사용 where 절)

SessionFactory sessionFactory = entityManager.getEntityManagerFactory()
		.unwrap( SessionFactory.class );

MetamodelImplementor metamodelImplementor = (MetamodelImplementor) sessionFactory.getMetamodel();

Type captionType = metamodelImplementor
		.entityPersister( Photo.class.getName() )
		.getPropertyType( "caption" );

Photo photo = (Photo) entityManager.createQuery(
	"select p " +
	"from Photo p " +
	"where upper(caption) = upper(:caption) ", Photo.class )
.unwrap( Query.class )
.setParameter( "caption", new Caption("Nicolae Grigorescu"), captionType)
.getSingleResult();

연관된 Hibernate Type을 사용하여 Caption Object를 쿼리 파라미터의 값으로 사용할 수 있다.

 

 

 

 

HBM 매핑을 사용하여 AttributeConverter 매핑

HBM(HibernateMapping) 매핑을 사용할 때, Hibernate가 각각의 type을 매핑을 지원하기 때문에 JPA AttributeConverter을 사용할 수 있다.

 

Money Type을 예로 설명해보자.

public class Money {

    private long cents;

    public Money(long cents) {
        this.cents = cents;
    }

    public long getCents() {
        return cents;
    }

    public void setCents(long cents) {
        this.cents = cents;
    }
}

 

Money Type에 대한 정의가 끝났으면 Account entity에 속성을 정의해보자.

public class Money {

    private long cents;

    public Money(long cents) {
        this.cents = cents;
    }

    public long getCents() {
        return cents;
    }

    public void setCents(long cents) {
        this.cents = cents;
    }
}

Hibernate는 Money 타입이 무엇인지 모르기 때문에 JPA AttributeConverter를 사용하여 Money type을 long 타입으로 변경해주는 MoneyConverter를 정의해보자.

 

public class MoneyConverter
        implements AttributeConverter<Money, Long> {

    @Override
    public Long convertToDatabaseColumn(Money attribute) {
        return attribute == null ? null : attribute.getCents();
    }

    @Override
    public Money convertToEntityAttribute(Long dbData) {
        return dbData == null ? null : new Money( dbData );
    }
}

정의한 MoneyConverter를 HBM 매핑에서 사용하기 위해 Configuration xml 파일에 converted::prefix를 달고 정의해준다.

<?xml version="1.0"?>
<!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.userguide.mapping.converter.hbm">
    <class name="Account" table="account" >
        <id name="id"/>

        <property name="owner"/>

        <property name="balance"
            type="converted::org.hibernate.userguide.mapping.converter.hbm.MoneyConverter"/>

    </class>
</hibernate-mapping>

 

 

 

Custom Type

Enum을 매팡하는 마지막 방법으로 Custom Type을 만들어서 'M', ' F' enum value가 저장될 수 있도록 할 수 있다.

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

	@Id
	private Long id;

	private String name;

	@Type( type = "org.hibernate.userguide.mapping.basic.GenderType" )
	public Gender gender;

	//Getters and setters are omitted for brevity

}

public class GenderType extends AbstractSingleColumnStandardBasicType<Gender> {

    public static final GenderType INSTANCE = new GenderType();

    public GenderType() {
        super(
            CharTypeDescriptor.INSTANCE,
            GenderJavaTypeDescriptor.INSTANCE
        );
    }

    public String getName() {
        return "gender";
    }

    @Override
    protected boolean registerUnderJavaType() {
        return true;
    }
}

public class GenderJavaTypeDescriptor extends AbstractTypeDescriptor<Gender> {

    public static final GenderJavaTypeDescriptor INSTANCE =
        new GenderJavaTypeDescriptor();

    protected GenderJavaTypeDescriptor() {
        super( Gender.class );
    }

    public String toString(Gender value) {
        return value == null ? null : value.name();
    }

    public Gender fromString(String string) {
        return string == null ? null : Gender.valueOf( string );
    }

    public <X> X unwrap(Gender value, Class<X> type, WrapperOptions options) {
        return CharacterTypeDescriptor.INSTANCE.unwrap(
            value == null ? null : value.getCode(),
            type,
            options
        );
    }

    public <X> Gender wrap(X value, WrapperOptions options) {
        return Gender.fromCode(
            CharacterTypeDescriptor.INSTANCE.wrap( value, options )
        );
    }
}

NULL

- null value

 

'M'

- MALE enum과 매핑된다.

 

'F'

- FEMALE값과 매핑된다.

 

더 자세한 내용은 위에 살펴봤던 CustomType 페이지에서 확인할 수 있다.

 

 


2.3.8 Mapping LOBS 

데이터베이스에 큰 오브젝트인 LOBs를 매핑하는 것은 JDBC 로케이터 타입과 LOB 데이터를 구체화 하는 방식 두 가지 형태로 제공된다.

 

JDBC LOB 로케이터는 LOB 데이터에 효율적인 접근을 위해서 존재한다. 로케이터를 통해 JDBC 드라이버는 필요에 따라 LOB 데이터의 메모리 공간을 확보할 수 있다. LOB 로케이터는 오직 트랜잭션을 획득한 기간동안에만 일시적으로 유효한 값을 사용할 수 있기 때문에 이를 사용하는데는 부자연스러움과 제한이 있을 수 있다.

 

구체화된 LOBs는 java의 String, byte[] 또는 그 이외에 데이터 타입과 같은 자연스러운 프로그래밍 패러다임을 위해 효율성을 절충하는 trade off를 사용하는 개념이다.

 

구체화된 LOBs는 전체적인 LOB 컨텐츠 전체를 메모리안에서 다루고 LOB 로케이터는 필요에 따라서 LOB 데이터 내용의 일부를 메모리 안에서 사용하는 것을 허용한다.

 

JDBC LOB 로케이터 타입은 아래와 같다.

  • java.sql.Blob
  • java.sql.Clob
  • java.sql.NClob

 

구체화된 LOB 매핑의 가장 친숙한 유형은 Java의 String, char[], byte[] 등등과 같은 타입이다. 이런 프로그래밍에 맞게 친숙한 타입을 사용하는 것은 일반적인 성능에 있어서는 trade off 요인이 될 수 있다.

 

 

Mapping CLOB

먼저 매핑에 사용될 CLOB 컬럼이 있다고 가정해보자.

CREATE TABLE Product (
  id INTEGER NOT NULL,
  name VARCHAR(255),
  warranty CLOB,
  PRIMARY KEY (id)
)

 

JPA @LOB 애노테이션을 사용하고 java.sql.Clob type을 사용하여 warranty를 매핑해보자.

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

    @Id
    private Integer id;

    private String name;

    @Lob
    private Clob warranty;

    //Getters and setters are omitted for brevity

}

위 같은 entity가 존재하기 위해서는 ClobProxy를 사용해서 Clob를 만들어야한다.

 

java.sql.Clob entity에 영속성 과정

String warranty = "My product warranty";

final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );

product.setWarranty( ClobProxy.generateProxy( warranty ) );

entityManager.persist( product );

 

그리고 Clob 컨텐츠를 가지고 오고 싶을때는 java.io.Reader를 변환해야한다.

Product product = entityManager.find( Product.class, productId );

try (Reader reader = product.getWarranty().getCharacterStream()) {
    assertEquals( "My product warranty", toString( reader ) );
}

 

사실 java.sql.Clob 형태로 데이터를 받아와도 이를 다루기는 쉽지 않기 때문에 구체화된 CLOB을 사용하여 String 또는 char[] 형태에 데이터에 사용될 수 있도록 넣어보자.

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

	@Id
	private Integer id;

	private String name;

	@Lob
	private String warranty;

	//Getters and setters are omitted for brevity

}

## 추가 설명

JDBC가 LOB 데이터를 다루는 방식은 드라이버마다 다르기 때문에 이에 대한 처리를 Hibernate가 대신 진행해준다.

그러나 몇몇 드라이버(PostgreSql)는 다루기가 까다롭기 때문에 LOB 데이터를 다루기 위해서는 별도의 작업이 더 필요할 수 있다. 이곳에서는 이에 대해서는 더 논하지는 않는다.

 

 

Mapping BLOB

BLOB도 비슷한 패턴으로 데이터를 매핑한다.

 

아래와 같이 테이블이 존재한다고 가정해보자.

CREATE TABLE Product (
    id INTEGER NOT NULL ,
    image blob ,
    name VARCHAR(255) ,
    PRIMARY KEY ( id )
)

 

JDBC(java.sql.Blob)를 사용해서 매핑되는 유형을 살펴보자.

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

    @Id
    private Integer id;

    private String name;

    @Lob
    private Blob image;

    //Getters and setters are omitted for brevity

}

 

CLOB과 마찬가지로 위와 같은 entity가 존재하기 위해서는 BlobProxy Hibernate 유틸리티를 사용해서 Blob를 만들어줘야 한다.

byte[] image = new byte[] {1, 2, 3};

final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );

product.setImage( BlobProxy.generateProxy( image ) );

entityManager.persist( product );

 

Blob 컨텐츠를 가져오기 위해서 java.io.InputStream을 변환해야 한다.

Product product = entityManager.find( Product.class, productId );

try (InputStream inputStream = product.getImage().getBinaryStream()) {
    assertArrayEquals(new byte[] {1, 2, 3}, toBytes( inputStream ) );
}

 

Clob과 마찬가지로 구체화된 Blob을 사용하여 byte array형태로 데이터를 받을 수 있다.

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

    @Id
    private Integer id;

    private String name;

    @Lob
    private byte[] image;

    //Getters and setters are omitted for brevity

}

 

 

 


2.3.9 Nationalized character 데이터 타입 매핑

JDBC4에는 nationalized 캐릭터 데이터 타입에 대해 정확하게 다룰 수 있는 기능이 추가되었다. 이를 위해서 몇 개의 nationalized data type이 추가되었다.

  • NCHAR
  • NVARCHAR
  • LONGNVARCHAR
  • NCLOB

아래 데이터 타입이 있다고 가정해보자.

CREATE TABLE Product (
    id INTEGER NOT NULL ,
    name VARCHAR(255) ,
    warranty NVARCHAR(255) ,
    PRIMARY KEY ( id )
)

 

nationalized 다양한 데이터 타입을 속성을 매핑하기 위해서 Hibernatesms @Nationalized 애노테이션을 정의해줘야한다.

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

    @Id
    private Integer id;

    private String name;

    @Nationalized
    private String warranty;

    //Getters and setters are omitted for brevity

}

 

CLOB 데이터 타입은 NCLOB SQL 데이터 타입으로 Hibernate에서 다룰 수 있다.

CREATE TABLE Product (
    id INTEGER NOT NULL ,
    name VARCHAR(255) ,
    warranty nclob ,
    PRIMARY KEY ( id )
)

 

Hibernate에서 NClob를 java.sql.NClob 형태로 매핑해야 한다.

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

    @Id
    private Integer id;

    private String name;

    @Lob
    @Nationalized
    // Clob also works, because NClob extends Clob.
    // The database type is still NCLOB either way and handled as such.
    private NClob warranty;

    //Getters and setters are omitted for brevity

}

 

위와 같은 형태에 entity가 존재하기 위해서는 NCLobProxy Hibernate 기능을 사용해서 NClob을 만들어줘야 한다.

String warranty = "My product warranty";

final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );

product.setWarranty( NClobProxy.generateProxy( warranty ) );

entityManager.persist( product );

NClob 컨텐츠를 가져오기 위해서는 java.io.Reader 를 변형해야한다.

Product product = entityManager.find( Product.class, productId );

try (Reader reader = product.getWarranty().getCharacterStream()) {
    assertEquals( "My product warranty", toString( reader ) );
}

 

또한 NCLOB을 구체화된 타입으로 String이나 char[]로 매핑할 수 있다.

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

    @Id
    private Integer id;

    private String name;

    @Lob
    @Nationalized
    private String warranty;

		@Lob
    @Nationalized
    private char[] warranty;

    //Getters and setters are omitted for brevity

}

 

만약 애플리케이션과 데이터베이스가 전체적으로 nationalized하다면 기본값으로써 지정하고 싶을 것이다. 이때 hibernate.use_nataionalized_character_data 설정이나 부트스트랩에서MetadataBuilder#enableGlobalNationalizedCharacterDataSupport 설정을 사용함으로써 기본값 지정이 가능하다.

 

 

 


2.3.10 UUID 매핑

Hibernate는 또한 UUID 매핑이 가능하다.

 

기본 UUID 매핑은 효율성을 위해서 binary 형태로 제공된다. 하지만 많은 애플리케이션에서는 읽기 쉽게 char 형태에 타입을 원하고 있기에

MetadataBuilder.applyBasicType( UUIDCharType.INSTANCE, UUID.class.getName() )등의 설정을 이용해서 이를 변경할 수 있는 기능을 제공한다.

 

 

 


2.3.11 UUID binary 매핑  

위에 언급하였듯이 기본 UUID 매핑은 binary 형태이다. UUID를 BINARY 형태로써 저장되어 있는 데이터를 java.util.UUID#getMostSignificantBits and java.util.UUID#getLeastSignificantBits를 사용하여 byte[]에 매핑 할 수 있다.

 

 

 


2.3.12 char 매핑 UUID

UUID를 java.util.UUID#toString and java.util.UUID#fromString을 사용하여 CHAR 또는 VARCHAR 타입을 문자열 타입으로 매핑할 수 있다.

 

 


2.3.13 PostgreSQL 특화된 UUID

PostgreSQL dialects(방언)을 사용할 때 기본으로 사용되는 UUID 매핑이다. JDBC 드라이버는 UUID 데이터 타입을 매핑하기 위해서 OTHER 코드를 선택한다. 드라이버가 OTHER에 다양한 데이터 타입을 매핑하려고 하기 때문에 여러 어려움을 야기할 수 있다.

 

 


2.3.14 식별자로써 UUID

Hibernate는 식별자로써 UUID를 사용하는 것을 허용하고 사용자 대신 자동으로 생성도 지원한다. 자세한 상황은 식별자 생성 영역에서 다시 살펴보자.

 

 


2.3.15 Date/Time 값 매핑

Hibernate는 다양한 Java Date/Time 클래스에 영속적인 도메인 엔티티 속성을 매핑할 수 있게 지원해준다. SQL에서 사용중인 기본적인 DATE/TIME 타입은 3가지 이다.

 

DATE

- 년도, 월, 일을 저장하는 타입으로 java.sql.Date와 동일하다.

 

TIME

- 시, 분, 초의 시간을 지정하며 java.sql.Time 타입과 동일하다.

 

TIMESTAMP

- DATE와 TIME(nanosecond가 포함된)을 저장하며 java.sql.Timestamp 타입과 동일하다.

 

java.sql 패키지에 의존성을 피하기 위해서 java.util 또는 java.time Date/Time 클래스를 대신 사용한다. java.sql 클래스는 SQL의 DATE/TIME 데이터 타입을 직접적으로 연관될 수 있지만, java.util은 @Temporal 애노테이션을 사용해서 지정해줘야 한다.

 

이런 방식으로 java.util.Date 또는 java.util.Calendar를 SQL DATE, TIME, TIMESTAMP 유형에 매핑할 수 있다.

 

 

DATE 타입을 java.util.Date에 매핑하는 예제

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

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@Temporal(TemporalType.DATE)
	private Date timestamp;

	//Getters and setters are omitted for brevity

}

 

java.util.Date 엔티티 영속성 매핑과 DATE 타입 INSERT 쿼리

DateEvent dateEvent = new DateEvent( new Date() );
entityManager.persist( dateEvent );

// insert query
INSERT INTO DateEvent ( timestamp, id )
VALUES ( '2015-12-29', 1 )

년도, 월, 일로 구성된 값이 데이터베이스에 저장된다.

 

만약 여기서 TemporalType의 TIME 속성을 지정할 경우 시간값을 저장한다.

@Column(name = "`timestamp`")
@Temporal(TemporalType.TIME)
private Date timestamp;


// insert query
INSERT INTO DateEvent ( timestamp, id )
VALUES ( '16:51:58', 1 )

 

 

만약 TIMESTAMP 속성을 지정할 경우에는 DATE + TIME(nanosecond가 추가된) 데이터가 저장된다.

@Column(name = "`timestamp`")
@Temporal(TemporalType.TIMESTAMP)
private Date timestamp;

// insert query
INSERT INTO DateEvent ( timestamp, id )
VALUES ( '2015-12-29 16:54:04.544', 1 )

 

이처럼 java.util.Date, java.util.Calendar는 DATE, TIME, TIMESTAMP의 jdbc 데이터 타입 중 어떤 타입을 사용할 지를 알기 위해서 @Temporal 어노테이션이 필요하다. 만약 java.util.Date를 시간으로 지정할 경우 java.util.Calendar는 기본 Timezone을 사용한다.

 

 

 

 

Java 8 Date/Time 값 매핑

java 8은 java.time 패키지에 기본으로 제공되는 instance date, interval, 로컬과 특정 존이 설정되어 있는 immutable한 Date, Time 인스턴스를 사용할 수 있는 새로운 API를 제공한다.

 

Java8에서 제공하는 SQL Date/Time 기본 타입은 아래와 같다.

 

DATE

- java.time.LocalDate

 

TIME

- java.time.LocalTime, java.time.OffsetTime

 

TIMESTAMP

- java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime and java.time.ZonedDateTime

 

Java 8 Date/Time 과 SQL type사이의 매핑은 지정이 되어있기 때문에 java.util 처럼 @Temporal 어노테이션이 불필요하다. 만약 java.time에 어노테이션을 지정해줄 경우에는 exception을 던져버린다.

org.hibernate.AnnotationException: @Temporal should only be set on a java.util.Date or java.util.Calendar property

 

 

 

특별한 타임존 사용

기본적으로 Hibernate는 java.sql.Timestamp 또는 java.sqlTime 속성을 저장할 경우에 PreparedStatement.setTimestamp(int parameterIndex, java.sql.Timestamp) or PreparedStatement.setTime(int parameterIndex, java.sql.Time x)를 사용하여 저장한다.

 

만약 timezone이 설정되어 있지 않은 경우에는 JDBC 드라이버는 JVM에 명시되어 있는 기본 타임존을 사용하는데 이는 애플리케이션이 글로벌한 환경에서 사용된다면 좋지 않은 선택이다. 이러한 이유로 일반적으로 데이터베이스에서 데이터를 가져오고 저장할 때 UTC같은 참조 timezone을 사용한다.

 

 

참조 타임존을 사용하기 위한 JVM 설정방법

1. 명시해서 설정

java -Duser.timezone=UTC ...

 

2. 프로그램 내에서 지정

TimeZone.setDefault( TimeZone.getTimeZone( "UTC" ) );

하지만 이러한 방법은 이 기사에 나와 있는 것처럼 실용적이지 않기 때문에 Hibernate는 hibernate.jdbc.time_zone 설정을 통해서 설정을 할 수 있다.

 

SessionFactory를 통한 설정

settings.put(
    AvailableSettings.JDBC_TIME_ZONE,
    TimeZone.getTimeZone( "UTC" )
);

 

Session을 통해 설정

Session session = sessionFactory()
    .withOptions()
    .jdbcTimeZone( TimeZone.getTimeZone( "UTC" ) )
    .openSession();

 

이렇게 값을 설정을 하게 되면 Hibernate가 java.util.Calendar는 hibernate.jdbc.time_zone 속성을 통해서 제공된 참조 timezone을 사용하여 PreparedStatement.setTimestamp(int parameterIndex, java.sql.Timestamp, Calendar cal) or PreparedStatement.setTime(int parameterIndex, java.sql.Time x, Calendar cal)를 호출하여 데이터를 저장한다.

 

 

 


2.3.16 JPA 2.1 AttributeConverters

Hibernate는 오랜기간동안 Custom 타입을 제공하였고 JPA 2.1에서는 AttributeConverter도 지원을 한다. AttributeConverter를 사용하면 애플리케이션 개발자는 JDBC 타입을 엔티티 기본 타입에 매핑할 수 있다.

 

아래 예에서 java.time.Period를 VARCHAR 타입과 매핑을 해볼 예정이다.

@Converter
public class PeriodStringConverter
        implements AttributeConverter<Period, String> {

    @Override
    public String convertToDatabaseColumn(Period attribute) {
        return attribute.toString();
    }

    @Override
    public Period convertToEntityAttribute(String dbData) {
        return Period.parse( dbData );
    }
}

 

그럼 이렇게 만든 custom 컨버터를  Entity 속성 위에 @Convert 애노테이션을 붙여서 사용해보자 .

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

    @Id
    @GeneratedValue
    private Long id;

    @Convert(converter = PeriodStringConverter.class)
    @Column(columnDefinition = "")
    private Period span;

    //Getters and setters are omitted for brevity

}

 

엔티티가 영속성 상태일 때 Hibernate는 AttributeConverter로직을 기반으로 type convert 작업을 진행한다.

INSERT INTO Event ( span, id )
VALUES ( 'P1Y2M3D', 1 )

 

 

AttributeConverter Java 및 JDBC types

데이터베이스 측면에서 Java Type에 대해 알지 못할 경우에는 Hibernate는 java.io.Serializable 타입을 반환한다.

또한 Java 타입이 Hibernate에게 알려져 있지 않은 타입일 경우에는 아래의 에러메시지가 표시된다.

HHH000481: Encountered Java type for which we could not locate a JavaTypeDescriptor and which does not appear to implement equals and/or hashCode. This can lead to significant performance problems when performing equality/dirty checking involving this Java type. Consider registering a custom JavaTypeDescriptor or at least implementing equals/hashCode.

Java 타입이 Hibernate에 알려져 있는지에 대한 여부는 JavaTypeDescriptorRegistry에 항목이 들어 있는지에 따라 결정된다. 기본적으로 Hibernate는 많은 JDK 타입을 JavaTypeDescriptorRegistry에 load 작업을 진행하지만 애플리케이션은 추가적으로 JavaTypeDescriptorRegistry에 새로운 JavaTypeDescriptor 엔트리를 추가할 수 있다.

 

이런 방식을 사용하여 Hibernate는 JDBC 레벨에서 특정 Java Object 타입을 다룰 수 있는 방법에 대해 알 수 있다.

 

 

 

JPA 2.1 AttributeConverter Mutability Plan

기본적인 JPA AttributeConverter는 만약 기본 자바 타입이 불변일 경우 불변의 속성을 가지고 연관된 속성 유형이 변경이 가능할 경우 동일하게 변경이 가능한 속성을 가진다.

 

그러므로 AttributeConverter의 변경 가능성은 연관된 엔티티의 속성 타입의 JavaTypeDescriptor#getMutabilityPlan에 의해 결정된다.

 

1. 불변타입

만약 엔티티 속성이 String, primitive 래퍼 타입(Integer, Long) 또는 다른 불변 객체일 경우 엔티티 속성값은 새로운 값으로 재 할당해서 사용해야만 한다.

 

아래 예시에서 Period year, Month, Day등의 속성이 immutable 하기 때문에 새로운 Period를 생성해서 매핑해주는 방식으로 사용된다.

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

    @Id
    @GeneratedValue
    private Long id;

    @Convert(converter = PeriodStringConverter.class)
    @Column(columnDefinition = "")
    private Period span;

    //Getters and setters are omitted for brevity

}

// new Object mapping
Event event = entityManager.createQuery( "from Event", Event.class ).getSingleResult();
event.setSpan(Period
    .ofYears( 3 )
    .plusMonths( 2 )
    .plusDays( 1 )
);

 

 

2. 변경 가능한 타입

그럼 반대로 setter를 통해서 값 변경이 가능한 Money 타입을 살펴보자. Money에서 제공하는 setter를 이용해서 cent값을 바로 수정할 수 있다.

public static class Money {

	private long cents;

	//Getters and setters are omitted for brevity
}

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

	@Id
	private Long id;

	private String owner;

	@Convert(converter = MoneyConverter.class)
	private Money balance;

	//Getters and setters are omitted for brevity
}

public static class MoneyConverter
		implements AttributeConverter<Money, Long> {

	@Override
	public Long convertToDatabaseColumn(Money attribute) {
		return attribute == null ? null : attribute.getCents();
	}

	@Override
	public Money convertToEntityAttribute(Long dbData) {
		return dbData == null ? null : new Money( dbData );
	}
}


// mutable 속성을 이용하여 값 변경
Account account = entityManager.find( Account.class, 1L );
account.getBalance().setCents( 150 * 100L );
entityManager.persist( account );

비록 변경 가능한 속성을 사용한 AttributeConverter이 dirty check, 2차 캐시 등을 정상적으로 동작할 수 있도록 할 수 있지만 사실 이러한 유형들은 불변으로 처리하는 것이 더 효율적이다.

 

이러한 이유로 변경 가능한 타입을 사용할 수 있어도 불변하도록 객체를 사용하는 이유이다.

 

 


2.3.17 쿼터로 씌어진 SQL 식별자

테이블 또는 컬럼 이름을 로 감싸서 Hibernate가 생성하는 SQL에서 사용되게 할 수 있다. 전형적으로 Hibernate는 예약어로 구성된 키워드를 사용하기 위해서를 사용하지만 JPA는 ""를 대신 사용한다.

 

예약어가 사용이 되면 Hibernate는 SQL Dialect(방언)에 맞는 데이터를 사용한다. 일반적으로 SQL Server는 괄호를 사용하고 Mysql은 ``(백틱)을 사용하기 때문에 Hibernate가 자동으로 이를 변형해서 사용한다.

 

기존에는 name, number와 같은 예약어를 사용하기 위해서 Hibernate는 레거시 quoting(백틱)을 사용했다.

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

	@Id
	private Long id;

	@Column(name = "`name`")
	private String name;

	@Column(name = "`number`")
	private String number;

	//Getters and setters are omitted for brevity

}

 

하지만 JPA에서는 "을 사용해서 공용으로 처리할 수 있다.

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

	@Id
	private Long id;

	@Column(name = "\"name\"")
	private String name;

	@Column(name = "\"number\"")
	private String number;

	//Getters and setters are omitted for brevity

}

 

이를 사용하면 Hibernate는 자동으로 다음과 같은 쿼리를 만들어서 사용한다.

Product product = new Product();
product.setId( 1L );
product.setName( "Mobile phone" );
product.setNumber( "123-456-7890" );
entityManager.persist( product );

INSERT INTO Product ("name", "number", id)
VALUES ('Mobile phone', '123-456-7890', 1)

 

 

Global quote 설정

모든 엔티티에 해당 설정이 필요하게 된다면 얼마나 귀찮을까? 이를 해결하기 위해서 이런 부분에 대해 global로 설정할 수 있는 부분이 존재한다.

 

hibernate에서 또 spring jpa 에서의 설정은 다음과 같다.

// hibernate 설정
<property
    name="hibernate.globally_quoted_identifiers"
    value="true"
/>

// spring jpa를 사용할 경우
spring.jpa.properties.hibernate.globally_quoted_identifiers=true

이렇게 설정하게 되면 모든 쿼리에서 컬럼과 테이블 이름에 모두 quote가 설정될 것이다. 좀 더 quoting에 대한 설정을 알아보고 싶다면 이곳을 참조하면 된다. https://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations-mapping

 

Hibernate ORM 5.3.20.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

 

 

 


2.3.18 생성되는 값

데이터베이스로 부터 생성되는 값을 의미한다. 전형적으로 Hibernate 애플리케이션은 생성되는 값을 위해서 데이터베이스로 부터 그 값을 받아와야 하는 번거로움이 있었다. 그러나 생성되는 값을 사용하면 애플리케이션의 이런 책임을 Hibernate에게 전가할 수 있게된다.

 

Hibernate는 생성되는 값으로 설정한 엔티티에 SQL INSERT 또는 Update를 수행할 경우 생성된 값을 검색하기 위해 select를 수행한다.

 

생성되는 값은 추가적으로 삽입 또는 업데이트 작업을 할 수 없어야 한다. @Version @Basic 타입만 생성되는 값으로써 사용될 수 있다.

 

NEVER (default)

- 주어진 속성 값은 자동으로 생성되지 않는다.

 

INSERT

- 주어진 속성값은 삽입 시 생성되지만 업데이트에서는 다시 생성되지 않는다.

 

ALWAYS

- 주어진 속성은 insert와 update시 모두 생성된다.

 

이 생성되는 값을 사용하기 위해서는 Hibernate는 @Generated 애노테이션 사용하면 된다.

 

 

@Generated 애노테이션

@Generated 애노테이션은 Hibernate가 엔티티가 영속성 상태가 되거나 업데이트 되었을 때 최신 값을 가져오기 위해서 사용된다. 이러한 이유로 @Generated 애노테이션은 GenerationTime Enum 값을 허용한다. (이 값은 자동으로 값이 생성되는 시점을 고를 수 있다. ALWAYS, INSERT, NEVER 위의 설명 참조)

 

아래 예를 통해 @Generated를 살펴보자.

Person 엔티티가 영속성 상태가 되었을 때 Hibernate는 database로 부터 계산되어 저장된 fullName 값을 가져온다.

@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;

}

// persist 과정
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]

// fullName 값 패치
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]

데이터베이스 full_name 필드에 데이터가 저장되고 반환되는 entity 정보에도 해당 full_name 데이터가 입력되어 있다. (설정에 따라 generated된 컬럼에 저장이 될 수도 있고 조회시마다 나올 수도 있다.)

 

GenerationTime.ALWAYS를 사용했었기 때문에 추가, 업데이트 모든 시점에서 사용된다.

 

ex) 업데이트 시점에 사용되는 persist, 쿼리 내용

// persist 과정
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]

 

@GeneratorType 애노테이션

@GeneratorType 애노테이션은 현재 애노테이션이 지정된 값을 설정하는 커스텀 generator로써 사용될 때 쓰여진다.

이런 이유로 @GeneratorType 애노테이션은 GenerationTime enum값과 커스텀 ValueGenerator 클래스 타입을 지원한다.

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;

}

위의 예를 통해 살펴보면 LoggedUserGenerator를 사용해서 지정된 GenerationTime 시간에 맞게 수행된다.

 

// persist
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 query
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]

 

 

@CreationTimestamp 애노테이션

@CreationTimestamp 애노테이션은 엔티티가 persist되었을 때 해당 애노테이션을 붙이고 있는 속성을 JVM의 최신 timestamp 값을 부여하도록 하는 기능을 한다.

 

해당 애노테이션을 지원하는 type은 다음과 같다.

  • java.util.Date
  • java.util.Calendar
  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp
@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 엔티티가 persist되면 Hibernate는 JVM 최근 timestamp값을 timestamp 컬럼에 업데이트 한다.

@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
}

 

 

@UpdateTimestamp 애노테이션

UpdateTimestamp 애노테이션을 붙이면 엔티티가 persist상태가 되었을 때 JVM의 현재 timestamp의 값을 Hibernate가 업데이트 해준다.

 

지원하는 타입은 다음과 같다.

  • java.util.Date
  • java.util.Calendar
  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp
@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의 현재 timestamp 값을 Hibernate가 업데이트 한다.

 

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 애노테이션

Hibernate 4.3에서는 Generated 속성, 커스터마이징 generator에 대한 새로운 접근 방식인 @ValueGenerationType 메타 애노테이션을 발표했다.

 

@Generated에는 @ValueGenerationType 메타 애노테이션을 사용하도록 만들어져 있다. 하지만 @ValueGenerationType는 @Generated가 지원하는 부분보다 더 많은 기능을 제공하고 있으며 @ValueGenerationType를 사용하면 새로 생성하는 generator를 단순하게 연결만 하면 되기 때문에 더 편한다.

 

아래 나올 예제를 통해서 볼 수 있듯이 @ValueGenerationType 메타 애노테이션은 generation 생성 전략을 사용하고 싶은 엔티티 속성 위에 커스텀 애노테이션을 추가하여 사용할 수 있다. 여기서 사용 되는 generation logic은 AnnotationValueGeneration 인터페이스를 구현해서 만들 수 있다.

 

데이터베이스에서 생성되는 값들

예를 들어서 SQL function으로 생성되는 CURRENT_TIMESTAMP 값을 호출하여 타임스탬프 값을 사용하는 방법에 대해 살펴보자.

@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 엔티티가 persiste 되었을 때 Hibernate에서는 아래 SQL을 실행한다.

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

이런 방식으로 데이터베이스에서 생성되는 컬럼 값을 엔티티가 persist되는 시점에 삽입할 수 있다.

 

 

인메모리에서 발생되는 값들

만약 timestamp값을 인메모리 내에서 생성되는 값을 사용해야 하는 경우 아래 매핑 방식을 사용해야 한다.

@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 엔티티가 persiste 되었을 때 Hibernate에서는 아래 SQL을 실행한다.

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

query에서 확인 할 수 있듯이 timestamp값에 new Date()값이 할당되어 데이터가 들어간다. (@ValueGenerationType을 이용해서 데이터베이스에 들어갈 값을 지정할 수 있다.)

 

 

 


2.3.19 컬럼 transformers: 읽기 쓰기 표현

Hibernate는 @Basic 으로 매핑되어 있는 컬럼의 값들의 읽기 쓰기 작업 시 사용될 SQL을 커스터마이징 하는 것을 지원한다. 예를들어 데이터베이스에서 해당 컬럼의 암호화 로직을 사용하고 있다면 데이터를 읽고 쓸 경우 암호 알고리즘을 컬럼 별로 적용할 수 있다.

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

	@Id
	private Long id;

	@NaturalId
	private String username;

	@Column(name = "pswd")
	@ColumnTransformer(
		read = "decrypt( 'AES', '00', pswd  )",
		write = "encrypt('AES', '00', ?)"
	)
	private String password;

	private int accessLevel;

	@ManyToOne(fetch = FetchType.LAZY)
	private Department department;

	@ManyToMany(mappedBy = "employees")
	private List<Project> projects = new ArrayList<>();

	//Getters and setters omitted for brevity
}

만약 속성이 하나 이상의 컬럼에서 사용될 경우에는 forColumn 속성을 이용해서 동작을 진행할 타겟을 설정해야 한다.

 

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

	@Id
	private Long id;

	@Type(type = "org.hibernate.userguide.mapping.basic.MonetaryAmountUserType")
	@Columns(columns = {
		@Column(name = "money"),
		@Column(name = "currency")
	})
	@ColumnTransformer(
		forColumn = "money",
		read = "money / 100",
		write = "? * 100"
	)
	private MonetaryAmount wallet;

	//Getters and setters omitted for brevity

}

Hibernate는 쿼리에서 해당 속성이 언급되었을 때 자동적으로 명시한 custom 표현으로 변경하여 쿼리를 수정한다. 이 기능은 @Formula와 두 가지의 차이점을 제외하고는 유사한 방식으로 동작한다.

 

  • 해당 속성은 자동 스키마 생성의 한 부분으로써 하나 또는 그 이상의 컬럼에서 지원된다.
  • 해당 속성은 읽고 쓰기가 가능하고 읽기전용으로는 불가능하다.

쓰기 표현은 값 적용을 위해서 ? 표현이 꼭 필요하다.

doInJPA( this::entityManagerFactory, entityManager -> {
	Savings savings = new Savings( );
	savings.setId( 1L );
	savings.setWallet( new MonetaryAmount( BigDecimal.TEN, Currency.getInstance( Locale.US ) ) );
	entityManager.persist( savings );
} );

doInJPA( this::entityManagerFactory, entityManager -> {
	Savings savings = entityManager.find( Savings.class, 1L );
	assertEquals( 10, savings.getWallet().getAmount().intValue());
} );
INSERT INTO Savings (money, currency, id)
VALUES (10 * 100, 'USD', 1)

SELECT
    s.id as id1_0_0_,
    s.money / 100 as money2_0_0_,
    s.currency as currency3_0_0_
FROM
    Savings s
WHERE
    s.id = 1

 

 

출처 :docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#basic-enums

728x90
반응형