Home JPA 3.1
Post
Cancel

JPA 3.1

JPA 3.1

Spring 6.0, Spring Boot 3.0 버전이 릴리즈된지 한달이 지났습니다. 현재 날짜 (2022-12-26) 기준으로는 Spring의 경우 6.0.3, Spring Boot의 경우 3.0.1 버전까지 올라왔네요.

Spring Boot를 주요 서버 프레임워크로 사용하는 저로써는 Spring Data JPA가 가장 기대됩니다. 그중에서도 최신 JPA 3.1 스펙을 사용해보기를 기대해 보고 있는데요, 아직까지 하이버네이트 메이저 버전에서는 JPA 3.0에 의존하고 있는 것으로 보입니다.

hibernate6.1.6-jpa-dependency
위 이미지는 Hibernate 6.1.6 Final 버전이 의존하고 있는 디펜던시 목록입니다. JPA 3.0에 의존하고 있네요.

hibernate6.2.0-jpa-dependency
위 이미지는 최근(2022-12-23)에 출시된 Hibernate 6.2.0 CR 버전이 의존하고 있는 디펜던시 목록입니다. JPA 3.1에 의존하고 있죠? Spring Data JPA 3.0.0 버전이 Hibernate 6.1.4 Final 버전을 사용하고 있기 때문에 2023년 안에는 JPA 3.1 스펙의 기능들을 Spring Boot에서 기본적으로 사용할 수 있을 것으로 보입니다.

그래서 오늘은 새로운 Spring Data JPA를 기다리며 JPA 3.1의 새로운 기능들, 변경점에 대해 알아보고자 합니다.

JPA 3.0

JPA 3.1 출시 이전에 JPA 3.0이 먼저 출시되었겠죠? 현재 시점의 최신 Hibernate 는 JPA 3.0 스펙을 완전히 지원합니다. Hibernate 6.0.x, 6.1.x 버전이 의존하고 있는 종속성을 확인해보면 jakarta.persistence-api:3.x 를 의존하고 있는것을 확인할 수 있습니다.

JPA가 3.0 버전으로 올라오면서 기존 JPA 2.2과 비교해 새로생긴 기능은 없습니다. 단, 모든 API클래스, 패키지, 점두사, 및 모든 XML 기반 구성 파일의 스키마 네임스페이스는 변경되었습니다. 간단히 말해 javax.persistence 패키지 내에 존재했던 클래스들이 jakarta.persistence 패키지 내부로 들어갔다고 보시면 되는데요, 관련해서는 이 글 을 읽어보시면 도움이 될것 같습니다. 왜 자바EE가 이클립스 재단으로 이관되고 자카르타EE 라는 명칭을 가지게 되었는지 확인하실 수 있습니다.

JPA 3.1

JPA 3.1은 Jakarta 10 의 일부로서 2022년 3월 30일에 릴리즈 되었습니다. 아래부터는 변경점과 새로운 기능들에 대해 알아보겠습니다.

새로 추가된 기능들은 예제를 통해 알아보고자 합니다. 일단 예제 프로젝트부터 구성해 보겠습니다.

첫번째로 build.gradle 파일입니다. hibernate-core 6.2.0 CR1 버전을 사용하였습니다. 파라미터 로깅을 위해 log4j2를 사용하며 db는 h2를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
plugins {
    id 'java'
}

group 'com.keencho'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

test {
    useJUnitPlatform()
}

dependencies {
    implementation 'org.hibernate.orm:hibernate-core:6.2.0.CR1'
    implementation 'com.h2database:h2:2.1.214'
    implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.19.0'

    implementation 'org.projectlombok:lombok:1.18.24'
    annotationProcessor 'org.projectlombok:lombok:1.18.24'

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1'
}

log4j2.xml 파일입니다. 로깅을 위해 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<Configuration monitorInterval="60">
    <Properties>
        <Property name="log-path">PropertiesConfiguration</Property>
    </Properties>
    <Appenders>
        <Console name="Console-Appender" target="SYSTEM_OUT">
            <PatternLayout>
                <pattern>
                    [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n
                </pattern>>
            </PatternLayout>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="org.hibernate.SQL" level="debug" additivity="false">
            <AppenderRef ref="Console-Appender"/>
        </Logger>
        <Logger name="org.hibernate.orm.jdbc.bind" level="trace" additivity="false">
            <AppenderRef ref="Console-Appender"/>
        </Logger>
        <Logger name="org.hibernate.stat" level="trace" additivity="false">
            <AppenderRef ref="Console-Appender"/>
        </Logger>
        <Logger name="org.hibernate.SQL_SLOW" level="trace" additivity="false">
            <AppenderRef ref="Console-Appender"/>
        </Logger>
        <Logger name="org.hibernate.cache" level="trace" additivity="false">
            <AppenderRef ref="Console-Appender"/>
        </Logger>

        <Root level="info">
            <AppenderRef ref="Console-Appender"/>
        </Root>
    </Loggers>
</Configuration>

다음은 /src/main/resources/META-INF/persistence.xml 경로에 존재해야 하는 persistence.xml 파일입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_1.xsd"
             version="3.1">
    <persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
        <description>JPA3.1 Test with hibernate6 persistence-unit</description>
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <class>com.keencho.jpa31.model.Book</class>

        <properties>
            <property name="hibernate.archive.autodetection" value="class, hbm"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
            <property name="hibernate.connection.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1"/>
            <property name="hibernate.connection.username" value="sa"/>
            <property name="hibernate.connection.pool_size" value="5"/>

            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
            <property name="hibernate.max_fetch_depth" value="5"/>

            <property name="hibernate.jdbc.batch_versioned_data" value="true"/>
            <property name="jakarta.persistence.validation.mode" value="NONE"/>
            <property name="hibernate.service.allow_crawling" value="false"/>
            <property name="hibernate.session.events.log" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Book 엔티티 입니다. 이 엔티티로 새로운 기능들을 확인해 볼 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@NoArgsConstructor
@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    private String title;
    private int pages;

    // 판매율 (소수점)
    private BigDecimal sellingRates;
    // 분류기호 (소수점)
    private BigDecimal classificationSymbol;

    // 발매일
    private LocalDateTime releaseDateTime;
}

다음은 테스트를 도와줄 설정 클래스 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class HibernateHelper {

    public static final HibernateHelper instance = new HibernateHelper();

    @Getter
    private final EntityManagerFactory entityManagerFactory;

    @Getter
    private final EntityManager entityManager;

    private HibernateHelper() {
        this.entityManagerFactory = Persistence.createEntityManagerFactory("pu");
        this.entityManager = entityManagerFactory.createEntityManager();
    }

    public static void beginTransaction() {
        instance.getEntityManager().getTransaction().begin();
    }

    public static void commit() {
        instance.getEntityManager().getTransaction().commit();
    }

    public static void close() {
        instance.getEntityManagerFactory().close();
    }

    public static void persist(Object entity) {
        instance.getEntityManager().persist(entity);
    }

    ////////////////////////////////////////////////////////////////////
    /////////////////////////////////// SELECT
    ////////////////////////////////////////////////////////////////////

    @FunctionalInterface
    public interface SelectHelper {
        Selection<?> apply(CriteriaBuilder cb, Root<?> root);
    }

    private static CriteriaBuilder getCriteriaBuilder() {
        // https://hibernate.zulipchat.com/#narrow/stream/132096-hibernate-user/topic/New.20functions.20in.20JPA.203.2E1/near/289429903
        // hibernate 6.1.x final 버전까지 jpa3.0에 의존하고 있기 때문에 HibernateCriteriaBuilder 로 형변환 해야한다.
        // 아직 final은 아니지만 hibernate 6.2.x CR 버전에서는 jpa 3.1에 의존하고 있으며 그냥 이 코드로 사용 가능하다. (형변환이 필요 없다.)
        return instance.getEntityManager().getCriteriaBuilder();
    }

    public static <T> List<T> listAll(Class<T> rootClass) {
        return list(rootClass).stream().map(i -> (T) i.get(0)).collect(Collectors.toList());
    }

    public static List<Tuple> list(Class<?> rootClass, SelectHelper... selectHelper) {
        var cb = getCriteriaBuilder();
        var query = cb.createTupleQuery();
        var root = query.from(rootClass);

        var list = new ArrayList<Selection<?>>();
        for (SelectHelper helper : selectHelper) {
            list.add(helper.apply(cb, root));
        }

        if (!list.isEmpty()) {
            query.multiselect(list);
        }

        return instance.getEntityManager().createQuery(query).getResultList();
    }

    public static List<?> list(String query) {
        return instance.getEntityManager().createQuery(query).getResultList();
    }
}

Hibernate 6.1.x 이하 버전을 사용하신다면 중간의 getCriteriaBuilder() 메소드는 HibernateCriteriaBuilder를 리턴해야 합니다. Hibernate 6.1.x 이하 버전은 JPA 3.0을 사용하기 때문인 것으로 보이며 이 글을 그대로 따라하셨거나 Hibernate 6.2.x 이상 버전을 사용한다면 이는 JPA 3.1을 사용하기 때문에 예제 그대로 CriteriaBuilder를 리턴하게 두시면 됩니다. 자세한 내용은 이 스레드를 확인해 보세요.

마지막으로 테스트 클래스 입니다. 테스트 전에 트랜잭션을 시작하고 2개의 엔티티를 영속화 합니다. 아래에서 작성할 모든 예제 테스트들은 이 클래스 내부에 위치하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class JPA31Test {

    @BeforeAll
    public static void init() {
        // disable hibernate info log
        Logger logger = Logger.getLogger("org.hibernate");
        logger.setLevel(Level.OFF);

        HibernateHelper.beginTransaction();

        var book1 = new Book();
        book1.setTitle("Effective Java");
        book1.setPages(342);
        book1.setSellingRates(new BigDecimal("48.54"));
        book1.setClassificationSymbol(new BigDecimal("170.537"));
        book1.setReleaseDateTime(LocalDateTime.of(2020, 3, 13, 17, 20, 30));

        var book2 = new Book();
        book2.setTitle("Modern Java in Action");
        book2.setPages(573);
        book2.setSellingRates(new BigDecimal("51.46"));
        book2.setClassificationSymbol(new BigDecimal("2096.331"));
        book2.setReleaseDateTime(LocalDateTime.of(2022, 8, 13, 10, 43, 0));

        HibernateHelper.persist(book1);
        HibernateHelper.persist(book2);

        HibernateHelper.commit();
    }

    @AfterAll
    public static void close() {
        HibernateHelper.close();
    }
}

1. EntityManagerFactory, EntityManager 인터페이스가 java.lang.AutoCloseable 인터페이스를 상속합니다. (변경)

jpa3.0-emf
jpa3.1-emf

순서대로 JPA 3.0의 EntityManagerFactory, JPA 3.1의 EntityManagerFactory 인터페이스 입니다. 이제 SessionFactory와 관련된 자원들을 자동으로 종료하게 됩니다.

jpa3.1-sf

2. ClassTransformer.transform 메소드가 Persistence API 스펙의 예외를 던집니다. (변경)

jpa3.1-classtransformer

기존 transform 메소드는 IllegalClassFormatException를 던졌었는데 이제는 TransformerException를 던지게 됩니다.

3. java.util.UUID와 GenerationType.UUID 지원 (추가)

@GeneratedValue 어노테이션의 생성전략을 AUTO 로 저장하거나 새롭게 추가된 UUID로 지정할 수 있습니다. 만약 베이직 타입을 UUID로 맞추고 생성 전략을 AUTO로 하였다면 자동으로 UUID전략을 따르게 됩니다.

generation-type

다음은 예제입니다. 영속화된 엔티티의 id가 uuid형태가 맞는지 정규 표현식으로 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static final String UUID_PATTERN = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}";

@Test
@DisplayName("GenerationType.UUID test")
void testUUID() {
    var resultList = HibernateHelper.listAll(Book.class);

    resultList.forEach(book -> {
        var id = book.getId().toString();

        System.out.println("uuid: " + id);
        Assertions.assertTrue(Pattern.matches(UUID_PATTERN, id));
    });
}
1
2
uuid: 9a612d02-1905-497a-9d9b-719918eec58f
uuid: 39824f2f-ac89-4357-9a3d-8b67d24a6fe1

4. Numeric 함수 추가 (추가)

CEILING, EXP, FLOOR, LN, POWER, ROUND, SIGN 함수가 JPQL에 추가되었으며 ceiling(), exp(), floor(), ln(), power(), round(), sign() 함수가 Criteria API에 추가되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Test
@DisplayName("new numeric functions test")
void testNewNumericFunctions() {
    var listAll = HibernateHelper.listAll(Book.class);

    var resultList = HibernateHelper.list(
            Book.class,
            // 수 올림
            (cb, root) -> cb.ceiling(root.get("sellingRates")),
            // 수 내림
            (cb, root) -> cb.floor(root.get("sellingRates")),
            // n번째 자리 반올림
            (cb, root) -> cb.round(root.get("sellingRates"), 1),
            // x를 인수로 하는 e^x 값을 반환
            (cb, root) -> cb.exp(root.get("sellingRates")),
            // 자연 로그를 반환
            (cb, root) -> cb.ln(root.get("sellingRates")),
            // n만큼 거듭제곱하여 반환
            (cb, root) -> cb.power(root.get("sellingRates"), 2),
            // 인수의 부호를 반환 (-1, 0, 1)
            (cb, root) -> cb.sign(root.get("sellingRates"))
    );

    System.out.println("\n\n");

    for (var i = 0; i < listAll.size(); i ++) {
        System.out.println("==========================");
        System.out.printf("원래 값: %s%n", listAll.get(i).getSellingRates().toString());
        System.out.printf("ceiling 함수: %s%n", resultList.get(i).get(0));
        System.out.printf("floor 함수: %s%n", resultList.get(i).get(1));
        System.out.printf("round 함수: %s%n", resultList.get(i).get(2));
        System.out.printf("exp 함수: %s%n", resultList.get(i).get(3));
        System.out.printf("ln 함수: %s%n", resultList.get(i).get(4));
        System.out.printf("power 함수: %s%n", resultList.get(i).get(5));
        System.out.printf("sign 함수: %s%n", resultList.get(i).get(6));
    }

    System.out.println("\n\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
==========================
원래 값: 48.54
ceiling 함수: 49
floor 함수: 48
round 함수: 48.50
exp 함수: 1.2040766975298458E21
ln 함수: 3.8823882002984553
power 함수: 2356.1315999999997
sign 함수: 1
==========================
원래 값: 51.46
ceiling 함수: 52
floor 함수: 51
round 함수: 51.50
exp 함수: 2.232513217248359E22
ln 함수: 3.940804806853598
power 함수: 2648.1316
sign 함수: 1

5. Date Time 관련 함수 추가 (추가)

LOCAL DATE, LOCAL DATETIME, LOCAL TIME 함수가 JPQL에 추가되었으며 localDate(), localDateTime(), localTime()함수가 Criteria API에 추가되었습니다.

이 함수들은 날짜를 계산하는 함수가 아닌 현재 시간을 반환하는 함수들 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
@DisplayName("new DateTime functions test")
void testNewDateTimeFunctions() {
    var now = LocalDateTime.now();
    var firstResult = HibernateHelper.list(
            Book.class,
            // 현재 시간
            (cb, root) -> cb.localTime(),
            // 현재 날짜
            (cb, root) -> cb.localDate(),
            // 현재 날짜 + 현재 시간
            (cb, root) -> cb.localDateTime()
    ).get(0);

    System.out.println("\n\n");

    System.out.println("현재시간: " + now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    System.out.println("시간: " + firstResult.get(0));
    System.out.println("날짜: " + firstResult.get(1));
    System.out.println("날짜 + 시간: " + firstResult.get(2));

    System.out.println("\n\n");
}
1
2
3
4
현재시간: 2022-12-26 16:23:16
시간: 16:23:16
날짜: 2022-12-26
날짜 + 시간: 2022-12-26T16:23:16.323509

6. EXTRACT 함수 추가 (추가)

EXTRACT 함수가 JPQL에 추가되었습니다. 안타깝게도 Criteria API에는 이에 대응하는 함수가 아직 없습니다. PR에 게빈 킹이 요청한 흔적이 있긴 합니다만 JPA 3.1에는 추가되지 않은듯 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Test
@DisplayName("new extract functions test")
void textExtractFunctions() {
    // https://github.com/jakartaee/persistence/pull/356
    // criteria api에는 extract 함수가 존재하지 않는다 흑흑

    var query = """
            SELECT 
                b.releaseDateTime as releaseDateTime,
                EXTRACT(YEAR from b.releaseDateTime) as year,
                EXTRACT(MONTH from b.releaseDateTime) as month,
                EXTRACT(DAY from b.releaseDateTime) as day,
                EXTRACT(HOUR from b.releaseDateTime) as hour,
                EXTRACT(MINUTE from b.releaseDateTime) as minute,
                EXTRACT(SECOND from b.releaseDateTime) as second
            FROM Book b
            """;

    System.out.println("\n\n");

    var list = HibernateHelper.list(query);

    for (var i = 0; i < list.size(); i ++) {
        var result = (Object[]) list.get(i);
        System.out.println("==========================");
        System.out.println("오리지널 값 (발매일): " + result[0]);
        System.out.println("년도: " + result[1]);
        System.out.println("달: " + result[2]);
        System.out.println("날짜: " + result[3]);
        System.out.println("시: " + result[4]);
        System.out.println("분: " + result[5]);
        System.out.println("초: " + result[6]);
    }

    System.out.println("\n\n");

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
==========================
오리지널 값 (발매일): 2020-03-13T17:20:30
년도: 2020
달: 3
날짜: 13
시: 17
분: 20
초: 30.0
==========================
오리지널 값 (발매일): 2022-08-13T10:43
년도: 2022
달: 8
날짜: 13
시: 10
분: 43
초: 0.0

7. Criteria CASE 표현식에서 Expressions를 조건식으로 사용할 수 있도록 지원 (추가)

hibernate-6.1.6-simplecase
hibernate-6.2.0-simplecase

SimpleCase<C, R> when(Expression<? extends C> condition, R result) 메소드와 SimpleCase<C, R> when(Expression<? extends C> condition, Expression<? extends R> result) 메소드가 추가되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
@DisplayName("new criteria simple case expressions")
void testCriteriaCaseExpressions() {

    var list = HibernateHelper.list(
            Book.class,
            (cb, root) -> root.get("title"),
            (cb, root) -> cb.selectCase(root.get("title"))
                    .when(cb.literal("Effective Java"), cb.literal(true))
                    .otherwise(false)
    );

    System.out.println("\n\n");

    list.forEach(item -> System.out.println("title: " + item.get(0) + " / title is 'Effective Java'?: " + item.get(1)));

    System.out.println("\n\n");
}

참고로 현재 영속화된 book 엔티티는 2개이며 각각 Effective Java, Modern Java in Action 이라는 제목을 가지고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
select
    b1_0.title,
    case b1_0.title 
        when 'Effective Java' then true 
        else cast(? as boolean) 
    end 
from
    Book b1_0
[TRACE] 2022-12-27 08:17:10.015 [main] bind - binding parameter [1] as [BOOLEAN] - [false]

title: Effective Java / title is 'Effective Java'?: true
title: Modern Java in Action / title is 'Effective Java'?: false

위 테스트를 돌려보면 결과는 위와같이 나오는데요, sql문을 보시면 상수표현식(literal)으로 작성한 첫번째 when 구문은 파라미터가 나중에 바인딩 되는것이 아니고 바로 바인딩 되게 됩니다. 이렇게 되면 내부적으로 Prepared Statements로써 동작하지는 않겠습니다만 sql구문을 Prepared Statements로 풀어낼수 없는 상황은 분명히 존재하므로 필요한 기능이라고 생각됩니다.

8. 기타 (수정)

기타 수정된 사항들도 있습니다. 이들은 어떤 기능을 추가 / 수정한 것이 아니라 어떤 주제를 명확히, 따지자면 문서 수정작업 정도로 보시면 될것 같습니다. 변경점 원문 그대로와 Github PR 링크를 걸어드리니 한번 읽어보시기 바랍니다.

  1. Adds missing definition of single_valued_embeddable_object_field in Jakarta Persistence QL BNF
  2. Clarifies mixing types of query input parameters
  3. Clarifies definition of the Basic type
  4. Clarifies the order of parameters in the LOCATE function
  5. Clarifies SqlResultSetMapping with multiple EntityResults and conflicting aliases

마치며

JPA 3.1에 대해 간략히 알아보았습니다. JPA 3.1에 의존하는 Hibernate Final 버전과 이 Hibernate Final 버전에 의존하는 Spring Data JPA가 얼른 나오면 좋겠습니다. Spring 팀이 어떻게 이를 응용해 새로운 기능들을 만들어낼지 궁금하네요.

This post is licensed under CC BY 4.0 by the author.