[번역] 2-1. Domain 모델 (매핑 타입, 네이밍 전략, Basic Type - 1)
데이터베이스/Hibernate

[번역] 2-1. Domain 모델 (매핑 타입, 네이밍 전략, Basic Type - 1)

반응형

2. 도메인 모델

도메인 모델이라는 단어는 데이터 모델링이라는 영역에서 비롯된다. 이것은 작업을 하고 있는 도메인의 모델로 묘사되기도 하고 때때로 이를 persistent classes라고 부른다.

 

궁극적으로 애플리케이션 도메인 모델은 ORM에서 중심이 되는 단어이다. 이것들은 매핑하고 싶은 클래스들로 구성된다. Hibernate는 POJO/JAVA Bean 프로그래밍 모델을 따를 때 가장 잘 동작한다. 그러나 이런 규칙은 어려운 요구사항은 아니다. 게다가 Hibernate는 persistence 객체의 특성에 대해 매우 조금만 담당하고 있다. 사실 Map 인스턴스 트리등을 사용하여 다른 방법으로도 domain 모델 구현이 가능하다.

 


2.1 매핑 타입

Hibernate는 Java와 JDBC 애플리케이션의 데이터 출력을 모두 이해한다. 데이터베이스의 데이터를 읽고 쓸 수 있는 능력은 Hibernate type의 기능이다. 여기서 말하는 type은 org.hibernate.type.Type 인터페이스의 구현된 type을 이야기한다. 이 Hibernate Type은 또한 동일한 지에 대한 체크, 값을 복제하는 방법과 같은 Java type의 다양한 행동에 대한 부분도 기술한다.

 

Hibernate type은 Java Type도 SQL 데이터 타입도 아니다.

 

이는 Java Type과 SQL 데이터 Type 사이에서 marshalling(한 객체의 메모리에서 표현 방식을 저장 또는 전송에 적합한 다른 데이터 형식으로 변환하는 과정)에 대한 이해 정보를 제공한다.

 

앞으로 Hibernate에서 type이라는 단어를 마주치게 된다면 Java type, JDBC type 또는 Hibernate Type 문맥에 따라 3가지 중 하나로 정해질 것이다.

 

type 종류에 대해 이해를 돕기 위해서 간단한 테이블과 매핑에 사용할 도메인 모델을 살펴보자.

// table
create table Contact (
    id integer not null,
    first varchar(255),
    last varchar(255),
    middle varchar(255),
    notes varchar(255),
    starred boolean not null,
    website varchar(255),
    primary key (id)
)

// entity
@Entity(name = "Contact")
public static class Contact {

	@Id
	private Integer id;

	private Name name;

	private String notes;

	private URL website;

	private boolean starred;

	//Getters and setters are omitted for brevity
}

@Embeddable
public class Name {

	private String first;

	private String middle;

	private String last;

	// getters and setters omitted
}

Hibernate 카테고리 타입은 Value 타입과 Entity 타입 두 가지가 존재한다

 

 

2.1.1 Value Types

value type은 자체 수명주기가 정의되어 있지 않은 데이터로 entity의 수명주기에 의존된다.

 

다른 관점에서 본다면 엔티티의 모든 상태는 전적으로 value type으로 구성된다. 이런 상태 필드 또는 JavaBean 속성은 persistent attributes(영속성)이라고 한다.

 

위의 Contact 클래스의 영속성은 value type이다. value type은 더 나아가 3가지의 카테고리로 분류된다.

 

Basic Types

Contact 클래스에서 Name 필드를 제외하고는 모든 type이 basic type이다.

*다음번 논의에서 정식으로 다뤄보자.

 

Embeddable types

Contact 클래스에서 Name 필드가 embeddable Type의 대표적인 예 이다.

*다음번 논의에서 정식으로 다뤄보자.

 

Collection types

위 Contact 클래스에서는 Collection types에 대한 예시가 나와 있지 않지만 value type에 하나이다.

*다음번 논의에서 정식으로 다뤄보자.

 

 

 

2.1.2 Entity Types

고유 식별자의 특성상 Entity는 다른 객체와 독립적으로 존재하지만 value는 그렇지 않다. Entity는 특별한 indetifier를 사용하여 데이터베이스 테이블의 행들과 연결하는 도메인 모델 클래스이다. 고유 식별자에 대한 요구 사항으로 인해 엔티티는 독립적으로 존재하며 자체 수명 주기를 정의한다. Contact 클래스는 바로 Entity의 한 예이다.

 

Entity mapping에 대한 자세한 예는 하단에서 다뤄보자.

 


 

2.2 이름 전략

관계형 데이터베이스에 대한 객체 모델의 매핑은 데이터베이스의 이름과 일치하는 것을 객체 모델에 이름을 매핑하는 것이다. Hibernate에서는 아래 보는 것과 같이 2가지의 과정을 따른다.

 

 

1. 도메인 모델 매핑으로부터 적절한 논리적 이름을 정하는게 첫번째 순서이다. 논리적 이름은 @Column 또는 @Table등을 사용해서 사용자로 부터 이름을 부여 받거나 ImplicitNamingStrategy 전략을 통해서 Hibernate를 통해 암묵적으로 부여 될 수 있다. 

@Column
private String wedulName;

 

2. 두 번째는 PhysicalNamingStrategy 전략을 통해서 논리적 이름을 물리적 이름으로 정의하는 것이다. (wedulName → wedul_name 을 찾게 해주는 전략)

 

 

역사적으로 Hibernate의 이름 전략은 org.hibernate.cfg.NamingStrategy로 되어있었다. 하지만 NamingStrategy 전략은 종종 API에 정보가 부족하거나 API가 적절하게 네이밍 규칙을 부여할 만큼 유연하지 못하여서 두 개의 전략으로 쪼개지게 되었다.

 

핵심적으로 이런 이름 전략은 개발자가 도메인 모델을 매핑하기 위해서 반복적으로 해야하는 업무를 최소하는데 목적이 있다.

 

 

2.2.1 ImplicitNamingStrategy

 

Hibernate는 entity나 attribute 이름을 테이블 또는 컬럼 이름과 매핑 시키기 위해서 logical name일 사용하는데 이때 사용하는 전략이 ImplicitNamingStrategy이다.

 

Entity가 명쾌하게 데이터베이스 테이블 이름을 매핑 하지 못했을 때, 암묵적으로 테이블 이름을 지정해줘야 한다. 또한 데이터베이스 컬럼 이름이 정확하게 매핑 되지 않았을 때도 컬럼 이름을 암묵적으로 지정해줘야한다.

 

이럴 때 사용되는 Naming Strategy이 ImplicitNamingStrategy이다.

 

다음은 논리적 이름을 명쾌하게 결정하지 못했을 때 org.hibernate.boot.model.naming.ImplicitNamingStrategy 전략 규칙을 사용하는 대표적인 예이다.

Hibernate는 여러 ImplicitNamingStrategy를 구현을 정의하고 있고 application에서 자유롭게 커스텀 구현이 가능하다.

 

ImplicitNamingStrategy를 사용하는데 여러 방법이 존재한다.

첫 번째로 애플리케이션에서 hibernate.implicit_naming_strategy 설정값을 변경하여 사용하고자 하는 방식을 선택할 수 있다.

 

 

- 이미 정의 되어 있는 구현체

1. default, jpa

ImplicitNamingStrategyJpaCompliantImpl 사용 (for JPA 2.0)

Java class, property와 동일한 logical name 사용

@Entity
@Table(name = "Customers")
public class Customer {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String firstName;
 
    private String lastName;
 
    @Column(name = "email")
    private String emailAddress;
    
    // getters and setters
    
}

// 사용되는 이름
Customer -> Customers
firstName -> firstName
lastName -> lastName
emailAddress -> email

 

2. legacy-hbm

ImplicitNamingStrategyLegacyHbmImpl 사용

 

3. legacy-jpa

ImplicitNamingStrategyLegacyJpaImpl (for JPA 1.0)

 

4. component-path

ImplicitNamingStrategyComponentPathImpl (composite path를 제외하고ImplicitNamingStrategyJpaCompliantImpl와 비슷)

 

 

두 번째 방법으로 애플리케이션과 통합은 org.hibernate.boot.MetadataBuilder # applyImplicitNamingStrategy를 활용하여 사용할 ImplicitNamingStrategy를 지정할 수 있다.

 

 

2.2.2 PhysicalNamingStrategy

많은 곳에서 데이터베이스 개체 (데이터베이스, 열, 외래키..) 이름 지정에 대한 규칙을 정의한다. PhysicalNamingStrategy의 아이디어는 별도의 하드 코딩 없이도 매핑할 수 있도록 명명 규칙을 구현하는데 도움을 준다.

 

ImplicitNamingStrategy의 목적은 accountNumber 속성 이름이 별도 지정하지 않은 경우 accountNumber로 매핑되는 반면에 PhysicalNamingStrategy는 acct_num 처럼 별도 물리적 컬럼 이름으로 대신하게 할 수 있다.

 

 acct_num 같은 규칙은 ImplicitNamingStrategy에서 다룰수 있지만 PhysicalNamingStrategy를 사용하는 건 관심의 분리이다. 
 
 PhysicalNamingStrategy는 특별하게 column name을 명시하였거나 암묵적으로 결정 되었는지에 관련 없이 적용이 된다. 
 ImplicitNamingStrategy는 명시적인 이름이 제공되지 않은 경우에만 적용되기 때문에 이는 필요나 의도가 있을 때 적용된다. 

 

가장 간단한 구현은 논리적이름을 물리적 이름처럼 사용하는 것이다. 하지만 애플리케이션은 PhysicalNamingStrategy 구현을 커스텀하게 정의할 수 있다.

 

hibernate는 위 두 가지 ImplicitNamingStrategy과 PhysicalNamingStrategy 두 가지의 다른 전략을 제공한다. 
그러나 Spring Boot는 SpringPhysicalNamingStrategy라는 PhysicalNamingStrategy를 구성한다. 여기서 모든 점과 camel case는 밑줄(underscore)로 대체되고 모든 테이블 이름은 소문자로 구성된다.

만약 naming strategy를 커스텀 하고 싶다면 application.properties를 통해서 변경 할 수 있다.
spring.jpa.hibernate.naming.physical-strategy=com.devglan.config.CustomPhysicalNamingStrategy);

 


 

 

2.3 기본 타입들 (Basic Types)

기본 value 타입들은 대게 하나의 데이터베이스 컬럼과 하나의 Java 유형에 매핑된다. Hibernate는 JDBC 명세서에서 권장하는 매핑에 대한 기본 타입들을 여러 제공한다.

 

내부적으로 Hibernate는 org.hibernate.type.Type 의 명세를 찾아야 할 때 basic type의 registry를 사용한다.

 

2.3.1 Hibernate에서 제공하는 Basic type들

더 많은 Type은 링크 참조 (https://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#basic-provided)

 

 

Java8 기본 타입들

 

Hibernate Spatial BasicTypes

 

hibernate의 스페셜 타입들을 사용하기 위해서는 hibernate-spatial 의존성을 classpath에 추가해야하고 
org.hibernate.spatial.SpatialDialect 구현체를 사용해야한다. 자세한 spatial type에 대한 내용은 이곳(https://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#spatial)을 참고 하기 바란다.

이 매핑들은 Hibernate 내부에 org.hibernate.type.BasicTypeRegistry라는 서비스에 의해 관리되고 있으며 이름을 키로 하고 있는 org.hibernate.type.BasicType 인스턴스 맵을 가지고 있는데 이는 위 테이블에서 "BasicTypeRegistry key(s)" 부분의 용도로써 사용된다.

 

 

2.3.2 @Basic 어노테이션

이런 basic type들은 javax.persistence.Basic 어노테이션으로 표시하지만, 기본적으로 제공되기에 생략되어 사용된다. 아래는 해당 @Basic 어노테이션의 사용 예이다

 

ex 1.) 직접 명시한 @Basic 어노테이션

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

	@Id
	@Basic
	private Integer id;

	@Basic
	private String sku;

	@Basic
	private String name;

	@Basic
	private String description;
}

 

ex 2.) 생략 가능한 @Basic 어노테이션

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

	@Id
	private Integer id;

	private String sku;

	private String name;

	private String description;
}

 

JPA 명세에서 Java type에서 Basic 어노테이션을 사용할 수 있는 타입을 제한하고 있다. 

- Java primitive types (boolean, int, etc)
- wrappers for the primitive types (java.lang.Boolean, java.lang.Integer, etc)
- java.lang.String
- java.math.BigInteger
- java.math.BigDecimal
- java.util.Date
- java.util.Calendar
- java.sql.Date
- java.sql.Time
- java.sql.Timestamp
- byte[] or Byte[]
- char[] or Character[]
- enums
- Serializable을 구현한 타입 (JPA는 Serializable 타입을 구현한 경우 그들의 상태(정보)를 databse에 serialize한다.)

만약 공급자 이식성에 대해 우려가 있는 경우 기본 basic type들만 사용해야한다. JPA 2.1에서는 javax.persistence.AttributeConverter를 통해 이런 우려를 조금 덜어주고 있다. 자세한 내용은 [이 곳](https://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#basic-jpa-convert)을 참고 하기를 바란다.

 

@Basic 어노테이션은 2가지의 속성을 가지고 있다.

1. optional (boolean type - default true)

null값을 허용하는지에 대한 명세로 JPA는 필요 여부에 대한 힌트로써 사용한다. primitive 타입이 아니라면 이는 NULLABLE 하다는 뜻으로써 사용된다.

 

2. fetch (Fetch type - default EAGER)

이 속성은 eagerly 또는 lazily하게 데이터를 가져올 것인지에 대한 명세이다. JPA에서 EAGER는 entity가 fetch될 때 함께 fetch 되어야 한다고 provider 측에 요구하는 것을 말하고 반면에 LAZY는 해당 속성에 접근했을 때 fetch 하겠다는 별도의 명세이다. hibernate는 bytecode enhancement를 사용하지 않으면 basic types의 설정은 무시된다. 부가적인 fetch와 bytecode enhancement에 대한 설정은 이곳을 살펴보면 된다.

 

 

2.3.3 @Column 애노테이션

JPA는 위에 본것 처럼 Naming Rule을 사용하여 컬럼과 테이블의 이름을 결정하는 규칙을 정의한다. basic type 속성에서 column name과 속성 네임은 동일하게 사용되지만 name을 직접 명시해주어 사용이 가능하다.

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

	@Id
	private Integer id;

	private String sku;

	private String name;

	@Column( name = "NOTES" )
	private String description;
}

위의 설정에서 사용된 name으로 description 속성을 NOTES에 매핑을 하게 된다. @Column 어노테이션을 통해 다른 매핑 정보들도 사용할 수 있는데 이는 javadocs에서 자세히 살펴보길 바란다.

 

 

2.3.4 BasicTypeRegistry

전에 위에서 말했듯이 Hibernate의 type은 자바 타입도 SQL 타입도 아니지만 그 두개의 타입 사이에서 서로를 이해하고 사용할 수 있도록 해주는 역할을 한다. 하지만 이전 예로 봤던 것 처럼 basic type이 어떻게 org.hibernate.type.StringType이 java.lang.String 이 속성과 매핑되고 org.hibernate.type.IntegerType 이 속성이 어떻게 java.lang.Integer 타입으로 매핑되는 것일까 궁금하다.

 

이 질문에 답은 Hibernate 내부에 이름을 키로 하고 있는 org.hibernate.type.BasicType의 맵을 보유 하고 있는 org.hibernate.type.BasicTypeRegistry 서비스로 알 수 있다.

 

뒤에 공부하겠지만 Explicit BasicTypes 세션에서 특정 속성 사용을 위한 BasicType을 hibernate에 알려줄 수 있다. 하지만 먼저 어떻게 동작하고 조정할 수 있는지 알아보자.

BasicTypeRegistry의 대한 모든 내용과 type에 대한 모든 다른 방법들에 대해서는 이 문서에서는 주제가 벗어난다. 자세한 부분은 문서를 확인하길 바란다.

 

예로써 위의 Product의 sku라는 String 속성을 살펴보자. 해당 속성에는 어떠한 매핑 타입이 없기 때문에 Hibernate는 java.lang.String 타입에 등록된 매핑을 찾기 위해서 BasicTypeRegistry을 살펴본다. 2.3.1 챕터에 'BasicTypeRegistry key(s)' 컬럼으로 돌아가보자.

 

BasicTypeRegistry 내의 기준선으로써 hibernate는 java 유형에 대한 권장되는 JDBC 타입에 대한 매핑을 사용한다. JDBC는 StringType 처리하는 매핑인 VARCHAR 타입을 추천한다. (위의 2.3.1 맵 참조) 이 부분이 String 타입을 위한 BasicTypeRegistry 이내에 기준선 매핑이다.

 

어플리케이션은 MetadataBuilder#applyBasicType 또는 MetadataBuilder#applyTypes 메소드를 사용하여 BasicType 새로 등록함으로써 확정하거나 기존 BasicType을 override 하여 사용할 수 있다. 이 부분에 대한 자세한 내용은 이 곳을 참고하자.

 

 

2.3.5 정확한 BasicTypes

때때로 특별하게 특수한 속성을 다루고 싶을 때가 있다. 경우에 따라 Hibernate는 원하지 않는 BasicType을 사용하거나 어떤 이유로 BasicTypeRegistry를 사용하고 싶지 않을 수 있다.

 

이런 경우 Hibernate에게 org.hibernate.annotations.Type 애노테이션을 사용하여 사용하고자 하는 BasicType을 지정해줄 수 있다.

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

	@Id
	private Integer id;

	private String sku;

	@org.hibernate.annotations.Type( type = "nstring" )
	private String name;

	@org.hibernate.annotations.Type( type = "materialized_nclob" )
	private String description;
}

위의 예에서는 String을 nationalized data로써 저장한다. 이건 단지 설명을 위한 예시로 만약 nationalized data로써 더 좋은 방법으로 매핑하고 싶다면 해당 부분을 참고하자.

 

추가적으로 description 속성은 LOB로 사용된다. 이 또한 설명을 위한 예시로 LOB 매핑에 대한 자세한 설명은 이곳을 참고하자.

 

org.hibernate.annotations.Type#type 속성은 아래 예시중 하나의 이름을 지정할 수 있다. (위 type에 들어갈 수 있는 이름)

  • org.hibernate.type.Type 구현체의 Full qualified name
  • BasicTypeRegistry에 등록된 모든 키
  • 알려진 type 정의의 이름

 

2.3.6 Custom Basic Type

Hibernate는 상대적으로 개발자들이 그들의 basic type 매핑 유형을 쉽게 만들 수 있도록 제공해준다. 예를 들어 java.util.BigInteger를 VARCHAR 타입에 매핑 시키거나 완전하게 쌔로운 타입을 지원할 수 있게 해준다. 

 

아래는 custom type을 개발하기 위해서 사용 되는 두 가지 접근 방식이다.

  • BasicType을 구현하고 등록한다.
  • 별도 등록이 필요하지 않는 UserType 구현

위의 수단에 대한 수단으로써 java.util.BitSet을 VARCHAR로 사용할 수 있또록 지원해주는 use case에 대해 고려해보자.

 

 

1. BasicType 구현

첫 번째 방법으로 BasicType 인터페이스를 직접 접근하는 것이다.

BasicType 인터페이스는 많은 메소드를 가지고 있기 때문에 더 편리하게 AbstractStandardBasicType, AbstractSingleColumnStandardBasicType를 사용할 수도 있다.
(단, value가 하나의 데이터베이스 컬럼의 저장된 값이어야 한다.)

첫 번째로 AbstractSingleColumnStandardBasicType를 extend 해보자.

public class BitSetType
        extends AbstractSingleColumnStandardBasicType<BitSet>
        implements DiscriminatorType<BitSet> {

    public static final BitSetType INSTANCE = new BitSetType();

    public BitSetType() {
        super( VarcharTypeDescriptor.INSTANCE, BitSetTypeDescriptor.INSTANCE );
    }

    @Override
    public BitSet stringToObject(String xml) throws Exception {
        return fromString( xml );
    }

    @Override
    public String objectToSQLString(BitSet value, Dialect dialect) throws Exception {
        return toString( value );
    }

    @Override
    public String getName() {
        return "bitset";
    }

}

AbstractSingleColumnStandardBasicType을 구현하기 위해서는 sqlTypeDescriptor와 javaTypeDescriptor가 필요하다.

 

sqlTypeDescriptor은 Database type이 varchar를 대상으로 할 것이라서 VarcharTypeDescriptor을 사용하였고 java쪽 매핑을 위해서는 BitSetTypeDescriptor을 아래처럼 구현해 줘야 한다.

public class BitSetTypeDescriptor extends AbstractTypeDescriptor<BitSet> {

    private static final String DELIMITER = ",";

    public static final BitSetTypeDescriptor INSTANCE = new BitSetTypeDescriptor();

    public BitSetTypeDescriptor() {
        super( BitSet.class );
    }

    @Override
    public String toString(BitSet value) {
        StringBuilder builder = new StringBuilder();
        for ( long token : value.toLongArray() ) {
            if ( builder.length() > 0 ) {
                builder.append( DELIMITER );
            }
            builder.append( Long.toString( token, 2 ) );
        }
        return builder.toString();
    }

    @Override
    public BitSet fromString(String string) {
        if ( string == null || string.isEmpty() ) {
            return null;
        }
        String[] tokens = string.split( DELIMITER );
        long[] values = new long[tokens.length];

        for ( int i = 0; i < tokens.length; i++ ) {
            values[i] = Long.valueOf( tokens[i], 2 );
        }
        return BitSet.valueOf( values );
    }

    @SuppressWarnings({"unchecked"})
    public <X> X unwrap(BitSet value, Class<X> type, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        if ( BitSet.class.isAssignableFrom( type ) ) {
            return (X) value;
        }
        if ( String.class.isAssignableFrom( type ) ) {
            return (X) toString( value);
        }
        throw unknownUnwrap( type );
    }

    public <X> BitSet wrap(X value, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        if ( String.class.isInstance( value ) ) {
            return fromString( (String) value );
        }
        if ( BitSet.class.isInstance( value ) ) {
            return (BitSet) value;
        }
        throw unknownWrap( value.getClass() );
    }
}

구현 메소드 unwrap은 BitSet 타입을 PreparedStatement 바인딩 파라미터로 사용될 때 사용되는 메서드 이고 wrap은 JDBC 컬럼 값을 실제 BitSet Object 타입으로 매핑할 때 사용한다.

 

이렇게 만든 새로운 BasicType은 부트 스트랩시 등록하는 동작이 진행 되어야 한다.

// 방법 1. 
configuration.registerTypeContributor( (typeContributions, serviceRegistry) -> {
	typeContributions.contributeType( BitSetType.INSTANCE );
} );

// 방법 2. (MetadataBuilder)
ServiceRegistry standardRegistry =
    new StandardServiceRegistryBuilder().build();

MetadataSources sources = new MetadataSources( standardRegistry );

MetadataBuilder metadataBuilder = sources.getMetadataBuilder();

metadataBuilder.applyBasicType( BitSetType.INSTANCE );

등록한 Type은 entity 매핑 시 다음처럼 사용할 수 있다.

// 방법 1. @Type 어노테이션 사용
@Entity(name = "Product")
public static class Product {

	@Id
	private Integer id;

	@Type( type = "bitset" )
	private BitSet bitSet;

	public Integer getId() {
		return id;
	}

	//Getters and setters are omitted for brevity
}

// 방법 2. @TypeDef 어노테이션 사용
@Entity(name = "Product")
@TypeDef(
	name = "bitset",
	defaultForType = BitSet.class,
	typeClass = BitSetType.class
)
public static class Product {

	@Id
	private Integer id;

	private BitSet bitSet;

	//Getters and setters are omitted for brevity
}

정상 등록되어 매핑이 잘 되는지 테스트 하기 위해서 아래 방식으로 테스트 가능하다.

 

BitSet bitSet = BitSet.valueOf( new long[] {1, 2, 3} );

doInHibernate( this::sessionFactory, session -> {
	Product product = new Product( );
	product.setId( 1 );
	product.setBitSet( bitSet );
	session.persist( product );
} );

doInHibernate( this::sessionFactory, session -> {
	Product product = session.get( Product.class, 1 );
	assertEquals(bitSet, product.getBitSet());
} );

위 단위 테스트를 실행 했을 때, Hibernatesms 아래 SQL 문장을 생성 시킨다.

DEBUG SQL:92 -
    insert
    into
        Product
        (bitSet, id)
    values
        (?, ?)

TRACE BasicBinder:65 - binding parameter [1] as [VARCHAR] - [{0, 65, 128, 129}]
TRACE BasicBinder:65 - binding parameter [2] as [INTEGER] - [1]

DEBUG SQL:92 -
    select
        bitsettype0_.id as id1_0_0_,
        bitsettype0_.bitSet as bitSet2_0_0_
    from
        Product bitsettype0_
    where
        bitsettype0_.id=?

TRACE BasicBinder:65 - binding parameter [1] as [INTEGER] - [1]
TRACE BasicExtractor:61 - extracted value ([bitSet2_0_0_] : [VARCHAR]) - [{0, 65, 128, 129}]

지금까지 본 것과 같이 BitSetType은 Java-SQL 및 Sql-to-Java 유형 변환을 처리할 수 있다.

 

 

 

2. UserType 구현

두 번째 방식은 UserType 인터페이스를 구현하는 것이다.

public class BitSetUserType implements UserType {

	public static final BitSetUserType INSTANCE = new BitSetUserType();

    private static final Logger log = Logger.getLogger( BitSetUserType.class );

    @Override
    public int[] sqlTypes() {
        return new int[] {StringType.INSTANCE.sqlType()};
    }

    @Override
    public Class returnedClass() {
        return BitSet.class;
    }

    @Override
    public boolean equals(Object x, Object y)
			throws HibernateException {
        return Objects.equals( x, y );
    }

    @Override
    public int hashCode(Object x)
			throws HibernateException {
        return Objects.hashCode( x );
    }

    @Override
    public Object nullSafeGet(
            ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
            throws HibernateException, SQLException {
        String columnName = names[0];
        String columnValue = (String) rs.getObject( columnName );
        log.debugv("Result set column {0} value is {1}", columnName, columnValue);
        return columnValue == null ? null :
				BitSetTypeDescriptor.INSTANCE.fromString( columnValue );
    }

    @Override
    public void nullSafeSet(
            PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
            throws HibernateException, SQLException {
        if ( value == null ) {
            log.debugv("Binding null to parameter {0} ",index);
            st.setNull( index, Types.VARCHAR );
        }
        else {
            String stringValue = BitSetTypeDescriptor.INSTANCE.toString( (BitSet) value );
            log.debugv("Binding {0} to parameter {1} ", stringValue, index);
            st.setString( index, stringValue );
        }
    }

    @Override
    public Object deepCopy(Object value)
			throws HibernateException {
        return value == null ? null :
            BitSet.valueOf( BitSet.class.cast( value ).toLongArray() );
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Serializable disassemble(Object value)
			throws HibernateException {
        return (BitSet) deepCopy( value );
    }

    @Override
    public Object assemble(Serializable cached, Object owner)
			throws HibernateException {
        return deepCopy( cached );
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
			throws HibernateException {
        return deepCopy( original );
    }
}

구현된 UserType은 위의 BasicType과 동일한 방식으로 등록하여 사용하고 테스트할 수 있다.

 

만약 별도의 등록 과정을 거치고 싶지 않을 경우 full classified 이름을 기재하여 사용할 수 있다.
@Type( type = "org.hibernate.userguide.mapping.basic.BitSetUserType" )

 

 

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

반응형