JPA 까먹지 말자! (2)

오늘은 저번시간에 이어서 JPA 까먹지 말자! (2) 를 시작해보자. JPA라 했지만 구현체는 hibernate 기준으로 설명하니 다른 구현체들은 조금씩 다를 수도 있으니 참고하면 되겠다. 또한 종종 hibernate 이야기도 있을 수도 있다.

@GeneratedValue strategy

JPA에서는 @GeneratedValue 어노테이션의 strategy 속성으로 기본키 전략을 설정할 수 있다. 물론 직접 기본키를 생성해주는 것도 좋지만 그보다는 자동생성 전략도 나쁘지 않게 생각된다. 필요하다면 비지니스 키를 따로 만들어서 직접 생성해주는 방법도 생각해 볼 수 있다.

JPA에서는 기본키의 전략이 3가지가 있다. TABLE, SEQUENCE, IDENTITY 전략이다. AUTO 전략도 있지만 AUTO는 해당 벤더에 따라 위의 세가지중 하나를 선택하게 된다. 3가지의 전략은 따로 설명하지 않겠다.

근데 이때 알아야 할 것은 기본 키 전략이 IDENTITY 일경우에는 쓰기 지연이 동작하지 않는다. 예를들어 다음과 같다.

public class Account {

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
//..
}

@Transactional
public void saveAccount() {
 Account account = new Account();
 account.setName("wonwoo");
 entityManager.persist(account);
}

위와 같이 작성할 경우에는 entityManager.persist(account) 이 메서드를 호출 할때 바로 insert 쿼리라 작성되어 DB에 날라간다. 하지만 TABLE전략이나 SEQUENCE 전략은 Transaction이 끝나고 flush 혹은 commit이 호출 될 때 insert 쿼리가 만들어져서 날라 가게 된다. 그 이유는 아마 데이터베이스에서 ID를 가져와야 하므로 insert 쿼리라 먼저 만들어져서 DB에 날라가는 것 같다.

만약 쓰기 지연 효과를 얻고 싶다면 TABLE 또는 SEQUENCE 전략으로 설계를 해야 한다.

@Basic LAZY

JPA 에서는 @Basic 어노테이션이라는 것이 있다. 하지만 우리는 거의 사용하지 않는다. 왜냐하면 사용할 일이 없기 떄문이다.. 기본 타입을 말하는 건데. 굳이 쓰지 않아도 암시적으로 JPA가 이거슨 기본타입이라고 설정해 주기 때문이다.

public class Account {

 @Id
 @GeneratedValue
 @Basic
 private Long id;

 @Basic
 private String name;

}

public class Account {

 @Id
 @GeneratedValue
 private Long id;

 private String name;
}

위의 코드는 암묵적으로 동일하다. 그런데 우리는 기본타입을 Lazy 로딩 할 수 있다. @Basic 어노테이션에는 fetch 속성이 존재한다. @OneToMany@OneToOne 기타 매핑하는 어노테이션에서 사용하는 fetch와 동일하다.

@Basic(fetch = FetchType.LAZY)
@Lob
private String content;

위와 같이 fetch 속성에 FetchType을 LAZY 설정해 주면 된다. 그럼 실제 content가 사용 될때 쿼리가 작성되어 날라간다.

Account account = entityManager.find(Account.class, 1L);
System.out.println(account.getContent()); //LAZY

실제 쿼리를 보면 아래와 같다.

select
 account0_.id as id1_0_0_,
 account0_.name as name3_0_0_
from
 account account0_
where
 account0_.id=?

select
 account_.content as content2_0_
from
 account account_
where
 account_.id=?

위 처럼 content를 사용할 때 한번 더 쿼리를 날린다. 근데 사용할 일이 있을까 싶다. content의 크기가 어마어마하게 크면 모를까..
근데 바로는 되지 않고 설정을 조금 해야한다. 여러방법이 있는 것 같은데 그 중에서 가장 쉬운 방법은 메이븐을 사용한다면 아래와 같이 plugin을 작성해야 한다.

<plugin>
 <groupid>org.hibernate.orm.tooling</groupid>
 <artifactid>hibernate-enhance-maven-plugin</artifactid>
 <version>${hibernate.version}</version>
 <executions>
  <execution>
   <configuration>
    <failonerror>true</failonerror>
    <enablelazyinitialization>true</enablelazyinitialization>
   </configuration>
   <goals>
    <goal>enhance</goal>
   </goals>
  </execution>
 </executions>
</plugin>

근데 설정을 좀 잘해야 될거 같다. 다른 프록시들과 조금 꼬이는 듯하다. 만약 사용할 경우에는 다른 프록시들이 잘되는지 테스트를 많이 해봐야 될 것 같다.

Criteria

요즘의 개발자들은 거의 사용하지 않겠지만 (필자 역시도 요즘개발자라) 예전 개발자분들은 익숙한 클래스일 듯 싶다. 요즘 Criteria 보다는 훨씬 쉬운 Querydsl 이라는게 있어 대부분이 Querydsl을 사용하지 Criteria를 사용하지는 않는 것 같다. Criteria와 Querydsl은 동일하게 JPQL을 편하게 작성하도록 만든 빌더 클래스들이다. Criteria는 JPA표준이고 Querydsl은 비표준이지만 훨씬 간단하고 알아보기 좋고 쉽다. 하지만 이런게 있다고만 알아두면 좋을 것 같다.

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<account> criteria = builder.createQuery(Account.class);
Root<account> root = criteria.from(Account.class);
criteria.select(root);
criteria.where(builder.equal(root.get("id"), 1L));
Account account = entityManager.createQuery(criteria).getSingleResult();

위와 같이 조금 어렵다. 뭔가 많은 작업을 해야 된다. 그리고 조금 알아보기도 힘든 것같다. 익숙하지 않아서 그런가? 그래도 사용하고 싶다면 사용해도 된다. JPA 표준이니까. 이것의 장점은 타입세이프해서 좋은 것 같다. 물론 root.get("id") 이 부분도 타입세이프하게 만들 수 있다.

참고로 spring data jpa 에서도 Criteria를 사용한다. 당연히 그래야 했을 거고 그럴꺼라고 생각했다.

Proxy

JPA에서는 바로 쿼리를 날려서 가져오는 find 메서드와 나중에, 해당 객체를 사용할 떄 가져오는(Lazy) getReference 메서드가 존재한다.

Account account = entityManager.find(Account.class, 1L);
Account account1 = entityManager.getReference(Account.class, 2L);

위와 같이 딱 두줄만 코드를 작성했을 때 쿼리는 몇번 날아갈까?
정답은 한번이다. getReference 메서드를 사용할 때는 실제 쿼리를 날리지 않고 프록시 객체만 전달 된다. 왜 그럼 굳이 proxy 객체만 전달하는 getReference 를 만들었을까?
예를들어 연관관계를 맺을 경우 account의 엔티티가 필요한 상황이라고 가정해보자. 물론 account의 엔티티들을 사용한다면 프록시 객체가 필요 없겠지만 단지 연관관계를 맺을 목적이라면 굳이 데이터베이스에 쿼리를 날릴 필요가 없다. 그럴 경우 이 프록시 객체를 사용하면 된다.

Account account = entityManager.getReference(Account.class, 2L);
Address address = new Address();
address.setAddress("seoul");
address.setAccount(account);
entityManager.persist(address);

위와 같이 작성했을 경우 insert 쿼리 한번만 데이터베이스에 날라가게 된다. 좀 더 성능적으로 최적화를 할 수 있다. 이런 경우에라면 정말 좋은 기능이지 않나 싶다.

참고로 다른 JPA의 구현체들은 어떤 프록시를 사용하는지 모르겠지만 hibernate 경우에는 javassist를 사용했다. 하지만 이 글을 쓰는 기준으로 최신버전인 5.3.7 버전은 bytebuddy로 proxy를 변경하였다. 아마도 5.3 이후부터 bytebuddy로 변경한 듯 싶다. 요즘에 code generation 으로 bytebuddy 를 많이 이용하는 것 같다.

또한 Spring data jpa 프로젝트에서도 해당 메서드를 사용가능하다. 현재 버전은 findById 이전버전은 findOne 메서드로 데이터베이스에서 바로 조회했다면 getOne 메서드로 해당 프록시 엔티티를 가져올 수 있다.

FlushMode

hibernate 경우에는 FlushMode 6가지 정도 지원하는데 JPA 스펙에는 2가지가 있다. 하이버네이트 API를 사용한다면 6개 모두 사용할 수 있겠지만 JPA API만 사용한다면 2가지 타입만이 존재한다. COMMITAUTO 가 JPA에서 지원해주는 FlushMode 타입이다.
일반적으로 JPA는 트랜잭션이 commit 되기 직전에 flush도 자동으로 된다. 또 한가지 자동으로 flush 가 될 때가 있는데 그때는 jpql이나 쿼리를 날릴 때 자동으로 flush가 호출 된다.

jpql이나 쿼리를 작성해서 날릴 때 flush 되는 설정이 FlushMode.AUTO 설정이다. 이것은 JPA의 기본값이다. 굳이 아래처럼 명시해주지 않아도 된다.

Account account = new Account();
account.setName("wonwoo");
entityManager.setFlushMode(FlushModeType.AUTO);
entityManager.persist(account);
entityManager.createQuery("select a from Account a", Account.class).getResultList();

위와 같이 코드를 작성할 경우 insert 쿼리는 jpql 쿼리를 날리 전에 먼저 데이터베이스에 날라간다.

insert
 into
account
 (content, name, id)
values
 (?, ?, ?)

select
 account0_.id as id1_0_,
 account0_.content as content2_0_,
 account0_.name as name3_0_
from
 account account0_

만약 AUTO가 아닌 COMMIT으로 했을 경우에는 insert 는 commit 되고 날라가고 그전에 jpql이나 쿼리가 먼저 날라간다. 그래서 원하는 데이터가 나오지 않을 수도 있다.

entityManager.setFlushMode(FlushModeType.COMMIT);

아래는 FlushMode를 COMMIT으로 했을 떄 쿼리이다. select 쿼리가 먼저 데이터베이스에 날라갔다.

select
 account0_.id as id1_0_,
 account0_.content as content2_0_,
 account0_.name as name3_0_
from
 account account0_

insert
 into
account
 (content, name, id)
values
 (?, ?, ?)

쿼리를 날릴때 마다 매번 flush를 하지 않아 성능은 더 좋겠지만 원하는 데이터가 나오지 않을 수도 있으니 상황에 맞게 고민을 해서 사용하면 되겠다.

@NamedQuery

필자는 잘은 사용하지 않지만 사용하면 나쁘지 않은 기능이다. 정적쿼리를 사용할 때 매우 유용한 어노테이션이다. 매번 동일한 쿼리를 작성하지 않고 해당 Name만 지정해줘서 쿼리를 날리면 된다.

@Entity
@NamedQuery(name = "findByname", query = "select a from Account a where name = :name")
public class Account {
  //...
}

위와 같이 @NamedQuery 어노테이션을 이용해서 정적 쿼리를 만들 수 있다. 아주 심플하다. 근데 많으면 보기 불편할거 같다.

List<account> accounts = entityManager.createNamedQuery("findByname", Account.class)
  .setParameter("name", "wonwoo")
  .getResultList();

매번 같은 쿼리를 작성하지 않고 해당 name만 작성해서 쿼리를 날릴 수 있으니 재사용성도 있다. 만약 여러개를 작성하고 싶다면 @NamedQueries 어노테이션을 이용하면 된다.

@Entity
@NamedQueries({
  @NamedQuery(name = "findByname", query = "select a from Account a where name = :name"),
  @NamedQuery(name = "findByEmail", query = "select a from Account a where email = :email")
})
public class Account {
  //...
}

이것은 JPA2.1 기준이며 JPA 2.2부터는 좀 더 간편하게 @NamedQuery 어노테이션만 이용해도 된다.

@Entity
@NamedQuery(name = "findByname", query = "select a from Account a where name = :name")
@NamedQuery(name = "findByEmail", query = "select a from Account a where email = :email")
public class Account {
}

@NamedQueries 어노테이션을 사용하는 것보다 @NamedQuery를 사용하는 것이 더 보기에 좋아 보인다. 하지만 JPA 2.2 부터 지원하니 그 전 사용자라면 @NamedQueries를 사용해야 한다.

오늘은 이렇게 JPA에 관련해서 두번째 시간을 가져봤다. 물론 필자도 JPA를 잘 사용하지 못한다. (나도 잘사용하고 싶다고오오..)

계속계속 사용해야 하는데 말이다.

Jpa 까먹지 말자.

오늘은 필자가 자주 까먹거나 기억을 하지 못하는 부분들을 좀 정리좀 해보려고 한다. JPA는 자주 기억이 안난다. 이상하게 사용하고 있을 때는 기억이 나지만 또 사용하지 않으면 기억이 안난다. 뭐 원래 그런건가? 아무튼 오늘 한번 자주 기억이 나지 않는 부분을 정리해보자.

LAZY, EAGER

Jpa 에서는 LAZY 로딩과 EAGER 로딩이 존재한다. LAZY 로딩일 경우에는 쿼리를 날리지 않고 해당 객체를 사용하는 시점에 쿼리가 나간다. EAGER 로딩일 경우에는 처음 부터 쿼리를 모두 날린다.

public class Account {
  @OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
  private List orders;
}

Account의 모든 리스트를 가져올 경우를 생각해보자.

List<account> accounts = accountRepository.findAll()

이 경우에는 n + 1 의 쿼리가 나간다. 하지만 accountRepository.findAll()을 호출 했을 때가 아니라 account.orders 를 호출 했을 때 각 쿼리가 작성된다.

그렇다면 EAGER로 했을 때는 어떨까? 마찬가지다. 마찬가지긴 하지만 Lazy와 달리 accountRepository.findAll() 메서드를 호출할 때 모든 쿼리가 날라간다. (n + 1)

하지만 이때는 좀 다르다. 모든 Account를 가져오는게 아니라 특정한 아이디로 Account를 하나를 가져와보자.

Account account = accountRepository.findById(1L).get();

만약 위와 같이 단일 Account를 가져온다면 Lazy 경우에는 동일하게 account.orders를 호출 할때 쿼리가 한번 더 나간다. 하지만 Eager일 경우에는 쿼리가 두번 날라가지 않고 한번만 날라간다. 그 이유는 JPA가 최적화를 해서 조인을 한다.

from
account account0_
left outer join
orders orders1_
on account0_.id=orders1_.account_id

여기서는 OneToMany만 했지만 ManyToOne 도 동일하다.
참고로 xxxToMany의 기본전략은 Lazy이고 xxxToOne의 기본전략은 Eager 로딩 전략이다.

cascade

casCade 속성은 많지만 필자가 자주 사용할 만한 몇가지만 설명하고 나머지는 생략하겠다. 또한 굳이 설명하지 않아도 대충 어떤 의미 인지 알 수 있을 듯하다.

casCade는 영속성 전이를 의미한다. 쉽게 설명하면 부모객체와 함께 하겠다는 의미이다.

Account account = new Account();
Order order = new Order();
order.setName("mac");
order.setAccount(account);
account.setOrders(Arrays.asList(order));
account.setName("wonwoo");
entityManager.persist(account);

예로 위와 같이 엔티티를 저장할 경우에 부모 엔티티만 저장했지만 자식 Order 엔티티까지 저장된다.

@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private List<order> orders;

위와 같이 cascade 를 CascadeType.PERSIST 로 설정하면 된다.

remove도 동일하다. 해당 엔티티가 삭제될 때 자식의 엔티티도 함께 삭제 하는 것이다.

@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
private List<order> orders;</order>

entityManager.remove(account);

그러면 먼저 자식의 엔티티부터 삭제하고 다음의 부모의 엔티티인 account를 삭제한다.
이외에도 MERGE, REFRESH, DETACH가 있다. 모두 해당 메서드를 호출할 때 영속성 전이가 된다.

entityManager.merge(account);
entityManager.refresh(account);
entityManager.detach(account);

만약 어떤 것이든 모두 하고 싶다면 ALL로 설정하면 된다.

OneToOne

기본적으로 OneToOne은 Lazy 로딩이 먹지 않는다. JPA OneToOne?
일반적으로 부모의 키가 자식의 테이블에 외래키로 되길 마련이다. 만약 그런다면 문제가 된다. 예를들어 우리는 Account를 가져오는 시점에 orders의 정보를 알고 있어야 한다. 그래야 jpa에서는 null을 넣을지 아니면 프록시 객체를 넣을지 결정해야 되기 때문이다. 이미 프록시 객체를 넣는다면 그건 이미 null이 아니다. 그래서 Lazy 로딩이 먹히지 않는다.
그렇다면 만약 자식의 키를 부모가 들고 있으면 어떨까? 그럼 가능하지 않을까? 왜냐하면 부모의 엔티티를 가져올 때 해당 자식의 id가 있으면 그건 null이 아니라는 뜻이다. 그럼 그때는 프록시 객체를 넣어주면 되고 id가 null일 경우에는 그냥 null을 넣어 주면 되니까 말이다. 맞다. 그렇게 하면 OneToOne 이라도 Lazy 로딩을 할 수 있다.

참고로 필자가 이것 저것 테스트를 해봤는데 다음과 같다.
– optional 여부와는 상관없다.
– 양방향과 단방향의 여부도 상관없다. 물론 단방향으로 했을 때는 부모 테이블에 지식 주키가 저장된다.
– 부모 테이블에 자식 주키가 저장된다면 그건 Lazy 로딩이 된다.

고아 객체

Account account = entityManager.find(Account.class, 2L);
account.getOrders().remove(0);

CascadeType.ALL 이고 orphanRemoval = true 일 경우에만 컬렉션 고아 객체가 삭제가 된다. orphanRemoval = true 만 있을 경우에는 삭제가 안되는데?
CascadeType PERSIST REMOVE 는 바로 전이가 되지 않고 플러시를 호출 할때 전이가 된다.

CascadeType.REMOVEorphanRemoval = true 동일하게 부모 엔티티를 삭제하면 자식 엔티티 까지 삭제 된다.

persist, merge

persist() 와 merge()는 모두 엔티티를 저장할 수 있다. 하지만 조금 다르다. persist 경우에는 key의 값이 존재 하면 안된다. 하지만 merge 경우에는 key의 값이 존재 해도 상관없다. 만약 db에 있다면 업데이트를 진행하고 없으면 저장을 한다.

또한 영속성을 관리하는 부분이 조금 다르다.

Account account = new Account();
account.setName("wonwoo");
entityManager.persist(account);
account.setName("wonwoo1");

위 경우에는 저장 후에 업데이트를 하지만 merge를 사용할 경우에는 업데이트를 하지 않는다.

Account account = new Account();
account.setName("wonwoo");
Account merge = entityManager.merge(account);
merge.setName("wonwoo1");

이 처럼 merge 에서 나온 엔티티로 상태를 변경해야 한다. 그래야 업데이트를 한다. account 필드는 영속성 컨텍스트에서 관리하지 않고 merge 에서 리턴한 엔티티를 영속성 컨텍스에서 관리하고 있다.

isolation

JPA 이야기는 아니지만 그래도 자주 기억이 나지 않기에..

READ_UNCOMMITTED

READ_UNCOMMITTED 커밋되지 않는 읽기 가능. 예를들어 A라는 트랜잭션이 데이터를 쓰고 (where id = 1) 커밋 하지 않았지만 B라는 트랜잭션이 (select where id = 1) 을 하면 데이터를 가지고 온다.

@Transactional
public void insertAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
  try {
   TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {
  }
}

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUnCommit() {
  accountRepository.findAll().forEach(System.out::println); //위의 foobar를 커밋도 하지 않았지만 select 된다.
}

READ_COMMITTED

READ_COMMITTED 커밋된 읽기 가능. 예를들어 A라는 트랜잭션이 데이터를 읽는 도중 B라는 트랜잭션이 데이터를 쓰고 커밋을 한 후 다시 A트랜잭션이 해당 데이터를 읽으면 B에서 넣은 데이터가 읽어진다. A라는 트랜잭션은 일관성 떨어진다.

@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommit() {
  accountRepository.findAll().forEach(System.out::println);

  try {
    TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {
  }
  accountRepository.findAll().forEach(System.out::println); // 아래의 foobar 가 나온다.
}

@Transactional
public void insertCommitAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
}

REPEATABLE_READ

READ_COMMITTED 반대로 데이터가 일관성이 있다. A라는 트랜잭션은 일관성있게 동일한 데이터를 읽고 온다.

@Transactional
public void insertRepeatableAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void readRepeatableCommit() {
  accountRepository.findAll().forEach(System.out::println);

  try {
    TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {

 }
  accountRepository.findAll().forEach(System.out::println);  //foobar 가 나오지 않는다.
}

SERIALIZABLE

A라는 트랜잭션이 데이터를 쓰고 있을때 B라는 때 트랜잭션은 select 를 할 lock 이 걸린다. 가장 비용도 높고 성능은 떨어지지만 가장 신뢰도가 높다.

@Transactional
public void insertSerializableAccount() {
  Account account = new Account();
  account.setName("foobar");
  accountRepository.save(account);
  try {
   TimeUnit.SECONDS.sleep(5);
  } catch (InterruptedException e) {

  }
}

@Transactional(isolation = Isolation.SERIALIZABLE)
public void readSerializableCommit() {
  System.out.println("lock");
  //lock
  accountRepository.findAll().forEach(System.out::println);

}

참고로 DB 밴더들마다 기능과 기본값이 조금씩 다를 수 있다.

오늘은 이렇게 필자가 자주 기억이 안나는 것을 정리해봤다. 헷갈린다. 자주자주 해야 되는데 사용하다 말다 그러다 보니까 맨날 잊어버린다.

필자가 테스트 해보면서 쓴 내용이니 정확하게 맞지 않을 수 도 있다.

Spring boot 2.0 과 Jpa2.2 그리고 Hibernate 5.2

오늘은 Spring boot2 기반으로 Jpa2.2 대해서 알아볼 예정이다. 실제 Spring boot 2.0은 기본적으로 하이버네이트 5.2를 지원한다.
그렇다고 해서 Spring boot 2.0 에 대해서 알아볼건 아니고 Spring boot 가 여러가지로 편해서 Spring boot 기반으로 작성할 예정이다. 만약 단독으로 Hibernate 를 사용한다면 여러 설정을 해야하므로 설정 관련은 따로 보는 것으 좋을 듯하다. 주로 JPA 2.2에 대해서 알아보도록 할 것이다. 시작해보자!

기본적으로 Spring boot 2.0Hibernate 5.2.16을 지원하고 있으며 (현재 이글을 쓰고 있을 때 최신버전은 Spring boot 2.0.1) Hibernate 5.2은 JPA2.1을 디펜더시 받고 있으며 2.1을 지원하고 있다. 그렇지만 Hibernate 5.2는 JPA2.2 도 일부 지원하고 있다. 그럼 지원하는 몇가지를 알아보도록 하자.

일단 spring boot 기준으로 설명하니 디펜더시도 Spring boot 기준일 것이다.

Maven Install

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.1-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>javax.persistence-api</artifactId>
    <version>2.2</version>
</dependency>

Spring data jpa 에서 hibernate-jpa-2.1-api 를 제외하고 그대신 javax.persistence-api 2.2 를 작성하면 된다. 그럼 일단 준비는 완료다.

JSR-310

JPA2.2 에서는 JSR-310 스펙인 Date and Time API를 지원한다.

@Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Date> {

    @Override
    public Date convertToDatabaseColumn(LocalDateTime date) {
        //blabla
    }

    @Override
    public LocalDateTime convertToEntityAttribute(Date date) {
        //blabla
    }
}

예전에는 LocalDateTime, LocalDate, LocalTime 등 JSR-310을 사용하려면 Converter를 만들어서 위와 같이 변환을 했어야 했다. 하지만 JPA2.2 부터는 그럴 필요 없다. 기본적으로 LocalDate, LocalTime, LocalDateTime, OffsetTime, OffsetDateTime 등을 지원하고 있으니 참고하면 되겠다.

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private String email;

    private LocalDateTime date;

   //etc
}

그냥 위와 같이 작성해도 정상적으로 동작한다.

@Repeatable

여러 어노테이션에서 @Repeatable 어노테이션이 추가가 되었다.

  • AssociationOverride
  • AttributeOverride
  • NamedQuery
  • NamedStoredProcedureQuery
  • PersistenceUnit
  • PrimaryKeyJoinColumn
  • SequenceGenerator
  • SecondaryTable
  • SqlResultSetMapping
  • TableGenerator
  • JoinColumn
  • MapKeyJoinColumn
  • NamedEntityGraph
  • NamedNativeQuery
  • PersistenceContext

필자가 찾아본 것으로는 대략 위와 같다. 얼추 찾아본거라 더 있을 수 있거나 잘못 작성한 내용도 있을 수 있으니 다시 확인하길 바란다.
기존에는 위의 어노테이션을 여러개 사용했을 경우 아래와 같이 사용했어야 했다.

@Entity
public class Person {

    //...

    @Embedded
    private Phone phone;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "number1", column = @Column(name = "telNumber1")),
            @AttributeOverride(name = "number2", column = @Column(name = "telNumber2")),
            @AttributeOverride(name = "number3", column = @Column(name = "telNumber3"))
    })
    private Phone telNumber;
    //...
}

하지만 이제는 그럴 필요 없이 @AttributeOverride 어노테이션만 여러개 작성하면 된다. 좀 더 깔끔해졌다. 물론 위의 코드가 더 낫다고 생각할 수 도 있겠지만 필자는 아래가 더 깔끔한 것 같다.

@Entity
public class Person {

    //...

    @Embedded
    private Phone phone;

    @Embedded
    @AttributeOverride(name = "number1", column = @Column(name = "telNumber1"))
    @AttributeOverride(name = "number2", column = @Column(name = "telNumber2"))
    @AttributeOverride(name = "number3", column = @Column(name = "telNumber3"))
    private Phone telNumber;

    //...
}

@Repeatable어노테이션을 이해하려면 좀 더 내용을 찾아보기를 권장한다. 그렇게 어려운 내용은 아니니 말이다.

resultStream

EntityManager로 통해서 값을 가져올 경우에 Stream 으로 변환해서 가져올 수 있다. 실제 이 메서드는 TypedQuery 인터페이스의 default 메서드로 List 를 Stream 으로 변환하는 단순한 메서드 이다.

default Stream<X> getResultStream() {
    return getResultList().stream();
}

딱히 어려운 부분은 없는 것 같다.

@Test
void entityManagerStream(@Autowired EntityManager entityManager) {
    entityManager.persist(new Person("wonwoo", "wonwoo@test.com", LocalDateTime.now(),
            new Phone("000", "111", "2222"), new Phone("333", "444", "5555")));
    List<PersonDto> persons = entityManager.createQuery("select p from Person p", Person.class)
            .getResultStream()
            .map(person -> new PersonDto(person.getName(), person.getEmail())).collect(Collectors.toList());
    assertThat(persons).hasSize(1);
    PersonDto personDto = persons.iterator().next();
    assertThat(personDto.getName()).isEqualTo("wonwoo");
    assertThat(personDto.getEmail()).isEqualTo("wonwoo@test.com");
}

위와 같이 map, filter등 변환할 것이나 stream 메서드를 사용하고 싶다면 getResultList() 메서드 말고 getResultStream() 메서드를 사용하면 된다.

CDI Injection

글쎄다. 일단 CDI Injection 스펙은 JPA2.2 에 존재하지만 아직 Hibernate에서는 지원하지 않는 것으로 보인다. 아니면 필자가 잘몰라서..

@Converter(autoApply = true)
public class TypeConverter implements AttributeConverter<Type, String> {

    @Inject
    private PersonService personService;

    @Override
    public String convertToDatabaseColumn(Type attribute) {
        return attribute.getType();
    }

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

위와 같이 DI를 받을 수 있다는 내용 같다. 하지만 필자는 동작하지 않는다.ㅠㅠ 만약 되는 것이라면 피드백을..

참고

하다가 몇가지 발견한게 있는데 뭐 중요한 내용은 아니지만 그래도 혹시나 누군가 이런 상황이 온다면 잘 해결하길 바란다. (무책임) 필자는 그냥 테스트한번 해볼려다 당한거니..
Hibernate 5.2의 SessionFactoryEntityManagerFactory를 상속 받고 있다.

public interface SessionFactory extends EntityManagerFactory, HibernateEntityManagerFactory, Referenceable, Serializable, java.io.Closeable {
   // ...
}

기존에는 다음과 같은 형태의 인터페이스 였다.

public interface SessionFactory extends Referenceable, Serializable, java.io.Closeable {
   //...
}

Hibernate 의 Session 또한 EntityManager를 상속 받고 있다. 기존의 Session 인터페이스는 다음과 같다.

public interface Session extends SharedSessionContract, java.io.Closeable {
   //..
}

하지만 현재 Hibernate 5.2에서는 다음과 같다.

public interface Session extends SharedSessionContract, EntityManager, HibernateEntityManager, AutoCloseable {
   //...
}

그것도 모르고 필자가 Hibernate API 를 사용하고 싶어서 문서에 있는 그대로 설정하였으나 제대로 동작하지 않았다. 아마도 그이유는 Spring boot의 자동 설정 때문일 것 같다.

@ConditionalOnMissingBean({ LocalContainerEntityManagerFactoryBean.class,
        EntityManagerFactory.class })
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
        EntityManagerFactoryBuilder factoryBuilder) {
  // ...
}

딱히 필자가 중요한 내용은 아니라서 그냥 알고만 있는 중이다. 나중에 필요하다면 좀 더 자세히 살펴보려고 한다. 혹시나 그 이유와 해결책을 알고 있다면.. 댓글로!
위의 예제 코드들은 여기에 있으니 참고하면 되겠다.

이상으로 오늘은 Hibernate 5.2의 JPA2.2를 알아봤다.
JPA 는 어렵다.