오늘은 필자가 자주 까먹거나 기억을 하지 못하는 부분들을 좀 정리좀 해보려고 한다. 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.REMOVE
와
orphanRemoval = 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 밴더들마다 기능과 기본값이 조금씩 다를 수 있다.
오늘은 이렇게 필자가 자주 기억이 안나는 것을 정리해봤다. 헷갈린다. 자주자주 해야 되는데 사용하다 말다 그러다 보니까 맨날 잊어버린다.
필자가 테스트 해보면서 쓴 내용이니 정확하게 맞지 않을 수 도 있다.