JPA 표준 예외들은
javax.persistence.PersistenceException
의 자식 클래스이다.
모두RuntimeException
으로 Unchecked Exception 이다.
트랜잭션 롤백을 표시하는 예외 | 설명 |
---|---|
javax.persistence.EntityExistsException | EntityManager::persist 호출 시, 이미 같은 엔티티가 있으면 발생 |
javax.persistence.EntityNotFoundException | Entity가 존재하지 않으면 발생. |
javax.persistence.OptimisticLockException | 낙관적 락 충돌 시 발생 |
javax.persistence.PessimisticLockException | 비관적 락 충돌 시 발생 |
javax.persistence.RollbackException | EntityManager::commit 실패 시 발생. |
롤백이 표시되어 있는 트랜잭션 커밋 시에도 발생한다. | |
javax.persistence.TransactionRequiredException | 트랜잭션이 필요할 때 트랜잭션이 없으면 발생. |
트랜잭션 없이 Entity를 변경할 때 주로 발생. |
트랜잭션 롤백을 표시하는 예외 | 설명 |
---|---|
javax.persistence.NoResultException | Query::getSingleResult 호출 시 결과가 하나도 없을 때 발생 |
javax.persistence.NonUniqueResultException | Query::getSingleResult 호출 시 결과가 둘 이상일 때 발생 |
javax.persistence.LockTimeoutException | 비관적 락에서 시간 초과 시 발생 |
javax.persistence.QueryTiemoutException | 쿼리 실행 시간 초과 시 발생 |
JPA 예외 | Spring 변환 예외 |
---|---|
javax.persistence.PersistenceException | org.springframework.orm.jpa.JpaSystemException |
javax.persistence.NoResultException | org.springframework.dao.EmptyResultDataAccessException |
javax.persistence.NonUniqueResultException | org.springframework.dao.IncorrectResultSizeDataAccessException |
javax.persistence.LockTimeoutException | org.springframework.dao.CannotAcquireLockException |
javax.persistence.QueryTimeoutException | org.springframework.dao.QueryTimeoutException |
javax.persistence.EntityExistsException | org.springframework.dao.DataIntegrityViolationException |
javax.persistence.EntityNotFoundException | org.springframework.orm.jpa.JpaObjectRetrievalFailureException |
javax.persistence.OptimisticLockException | org.springframework.orm.jpa.JpaOptimisticLockingFailureException |
javax.persistence.PessimisticLockException | org.springframework.dao.PessimisticLockingFailureException |
javax.persistence.TransactionRequiredException | org.springframework.dao.InvalidDataAccessApiUsageException |
javax.persistence.RollbackException | org.springframework.transaction.TransactionSystemException |
java.lang.IllegalStateException | org.springframework.dao.InvalidDataAccessApiUsageException |
java.lang.IllegalArgumentException | org.springframework.dao.InvalidDataAccessApiUsageException |
PersistenceExceptionTranslationPostProcessor
Bean 등록하면 된다.@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
throws
명시하면 된다.EntityManager::clear
메서드를 호출해서 영속성 컨텍스트를 초기화 한 다음에 사용해야 한다.==
)equals
)@Id
)같은 영속성 컨텍스트에서는 모두 적용 가능.
그렇지 않다면equals
혹은@Id
를 비교해야한다.
@SpringBootTest
@Transactional // 트랜잭션 안에서 테스트를 실행하고, Rollback 처리 해준다.
class MemberServiceTest {
@Autowired
private MemberService memberService;
@Autowired
private MemberRepository memberRepository;
@DisplayName("회원가입이 정상적으로 작동한다.")
@Test
void 회원가입() {
// Given
String givenMemberName = "rolroralra";
Member member = MemberDataSet.testData(givenMemberName);
// When
Long savedId = memberService.insertMember(member);
// Then
assertThat(memberRepository.findById(savedId)).isPresent()
.get()
.isEqualTo(member)
.hasFieldOrPropertyWithValue("name", givenMemberName);
}
}
@SpringBootTest
//@Transactional
class MemberServiceTest {
@Autowired
private MemberService memberService;
@Autowired
private MemberRepository memberRepository;
@DisplayName("회원가입이 정상적으로 작동한다.")
@Test
void 회원가입() {
// Given
String givenMemberName = "rolroralra";
Member member = MemberDataSet.testData(givenMemberName);
// When
Long savedId = memberService.insertMember(member);
// Then
assertThat(memberRepository.findById(savedId)).isPresent()
.get()
.isEqualTo(member) // Test 실패
.hasFieldOrPropertyWithValue("name", givenMemberName);
}
}
// org.opentest4j.AssertionFailedError:
// expected: com.example.springbootwithjpa.domain.Member@686ee555
// but was: com.example.springbootwithjpa.domain.Member@2bcaec2e
// Expected :com.example.springbootwithjpa.domain.Member@686ee555
// Actual :com.example.springbootwithjpa.domain.Member@2bcaec2e
영속성 컨텍스트는 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면, 원본 엔티티가 아닌 처음 조회된 프록시를 반환한다.
Member refMember = em.getRefrence(Member.class, 1L);
Member findMember = em.find(Member.class, 1L);
System.out.println("refMember Type = " + refMember.getClass());
System.out.println("findMember Type = " + findMember.getClass());
assertThat(refMember == findMember).isTrue();
assertThat(refMember).isInstanceOf(Member.class);
// refMember Type = class com.example.Member_$$_jvst843_0
// findMember Type = class com.example.Member_$$_jvst843_0
원본 엔티티를 먼저 조회하면 영속성 컨텍스트는 원본 엔티티를 이미 데이터베이스에서 조회했으므로 프록시를 반환할 이유가 없다.
Member findMember = em.find(Member.class, 1L);
Member refMember = em.getRefrence(Member.class, 1L);
System.out.println("findMember Type = " + findMember.getClass());
System.out.println("refMember Type = " + refMember.getClass());
assertThat(refMember == findMember).isTrue();
// findMember Type = class com.example.Member
// refMember Type = class com.example.Member
==
를 사용하면 안됨instanceof
를 사용해야 한다.em.clear();
Member refMember = em.getReference(Member.class, 1L);
assertThat(refMember.getClass() == Member.class).isFalse();
assertThat(refMember instanceof Member).isTrue();
equals
)instanceof
연산이 false
리턴ClassCastException
발생)HibernateProxy::getHibernateLazyInitializer::getImplmentation
)@Test
void inheritanceProxyTest() {
Book book = Book.createBook("book1", 10000L, 20L, "rolroralra", "isbn1");
em.persist(book);
OrderItem orderItem = OrderItem.createOrderItem(book, 10000L, 5L);
em.persist(orderItem);
em.flush();
em.clear();
OrderItem findOrderItem = em.find(OrderItem.class, orderItem.getId());
Item item = findOrderItem.getItem();
// 2. HibernateProxy 클래스를 활용하여 프록시 벗기기
Item unProxyItem = unProxy(item);
assertThat(item)
.isNotInstanceOf(Book.class) // 상관관계 프록시 객체 문제점 발생
.isInstanceOf(Item.class);
assertThat(unProxyItem)
.isInstanceOf(Book.class) // proxy를 벗김으로써 문제 해결
.isNotSameAs(item);
item.accept(new PrintVisitor()); // 4. Visitor 패턴을 활용한 해결
}
/**
* Hibernate가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메서드
* @param entity proxy entity
* @return original entity
* @param <T>
*/
@SuppressWarnings("unchecked")
private static <T> T unProxy(Object entity) {
if (entity instanceof HibernateProxy) {
entity = ((HibernateProxy) entity)
.getHibernateLazyInitializer()
.getImplementation();
}
return (T) entity;
}
@BatchSize(size = 100)
import org.hibernate.annotations.BatchSize;
@BatchSize(size = 100)
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
spring.jps.properties.hibernate.default_batch_fetch_size: 500
@Fetch(FetchMode.SUBSELECT)
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
@Transactional(readOnly = true)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
spring.jpa.properties.hibernate.jdbc.batch_size: 1000
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 1000
em.persiste(new Member()); // 1
em.persiste(new Member()); // 2
em.persiste(new Member()); // 3
em.persiste(new Member()); // 4
em.persiste(new Child()); // 5, 다른 연산
em.persiste(new Member()); // 6
em.persiste(new Member()); // 7
// 1,2,3,4 를 모아서 하나의 SQL 실행
// 5를 한번 SQL 실행
// 6, 7을 모아서 하나의 SQL 실행
// 총 3번의 SQL 실행