JPA 영속성 컨텍스트

메모리에 존재하는 데이터는 서비스가 종료되면 사라지게 되어있다.
이를 영속화 해서 사용하는 방법에 대해 알아봅시다
영속성에서 가장 주체적인 컨테이너 역할: EntityManager
■ Spring JPA / Hibernate의 초기화 전략
Spring JPA를 사용하시는 분들이라면 spring.jpa.hibernate.ddl-auto옵션을 자주 사용하셨을 것입니다. JAVA의 Entity설정을 참고하여 Spring Application 실행시점에 Hibernate에서 자동으로 DDL을 생성하여, 필요한 Database의 Table설정들을 자동으로 수행해주는 기능입니다.
설정 가능한 값은 true , false 둘중 하나 입니다.
spring.jpa.hibernate.ddl-auto옵션의 경우에는 Jpa의 Database 초기화 전략을 의미합니다.
none: ddl-auto 실행하지 않는 옵션
create: 항상 새로 생성하는 옵션 ( drop → create )
create-drop: 생성하고 종료될 때 자동으로 drop ( create → drop )
update: 실제 스키마와 엔티티를 비교해서 변경된 부분만 반영 ( drop X )
validate: 스키마와 엔티티를 비교만하고 다른 경우 오류 발생
■ Error executing DDL "alter table user drop foreign key
create_drop 설정은 실행시 table을 모두 drop합니다. 따라서 alter할 테이블을 찾지 못하여 나타나는 에러입니다. 초기화 옵션을 update로 변경하면 에러가 해결됩니다.
■ 현업에서도 놓치기 쉬운 옵션들 간의 차이 및 우선순위
1. 테스트 환경
H2 데이터베이스를 사용해서 자동으로 DDL 처리를 설정
generate-ddl, ddl-auto 옵션을 사용할 필요가 없음
2. 운영 환경
자동화 된 ddl을 사용하는 경우 리스크가 발생
generate-ddl: false, ddl-auto: none 으로 지정
간혹 ddl-auto: validate 는 사용
■ generate-ddl - ddl-auto 옵션의 차이
1. generate-ddl
구현체와 상관없이 자동화된 ddl을 사용하도록 하는 옵션
범용적인 옵션
2. ddl-auto
hibernate에 특화된 구체적인 옵션
스프링에선 임베디드 DB(H2)에선 default로 ddl-auto: create-drop
스프링에 우선순위는 ddl-auto > generate-ddl
ddl-auto 옵션이 존재하면 generate-ddl 옵션은 무시 → ddl-auto: false,
ddl-auto: create-drop 인 경우 ddl-auto 옵션으로 실행
■ spring.sql.init.mode 옵션
(Spring 2.4 spring.datasource.initialization-mode)
운영 데이터베이스(Mysql, 등)가 실행될 때 초기 SQL 실행 여부(data.sql, schema.sql)
spring.sql.init.mode(schema.sql), ddl-auto 옵션 충돌이 발생할 수 있음 → 스프링에 우선순위
옵션 spring.sql.init.mode > ddl-auto
use book_manager;
start transaction;
commit;
rollback;
비영속(new/transient)
영속성 컨텍스트와 전혀 관계가 없는 상태
객체를 생성’만’ 한 상태
// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
영속(managed) : 영속성 컨텍스트에 저장된 상태
Entity가 영속성 컨텍스트에 의해 관리되는 상태
EntityManager.persist(entity);
영속 상태가 된다고 바로 DB에 쿼리가 날라가지 않는다.
( DB 저장 X)
Merge : Detached Entity ⇒ Managed Entity
Persist : New Entity ⇒ Managed Entity
EntityManager.find();
조회할 때 영속성 컨텍스트 1차 캐시에 없어서 DB에서 조회한 후
해당 Entity를 1차 캐시에 올라간 상태
transaction.commit();
트랜잭션의 commit 시점에 영속성 컨텍스트에 있는 정보들이
DB에 쿼리로 날라간다.
userRepository.flush();
개발자가 의도적으로 영속성 캐시 데이터를 DB에 반영( 동기화 )
// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
// 객체를 저장한 상태 (영속)
//userRepository.save(user);
entityManager.persist(member);
Member member = entityManager.find(Member.class, 150L);
member.setName("AAAAA");
:: 개발자가 의도적으로 영속성 캐시 데이터를 DB에 반영( 동기화 )
entityManager.flush();
:: 영속성 컨텍스트를 완전히 초기화
clear는 1차 캐시에 상관없이 쿼리를 확인하고 싶을 때 즉, testcase 작성 시에 도움이 된다.
entityManager.clear();
:: 영속성 컨텍스트에서 떼넨다. (더 이상 JPA 의 관리 대상이 아님.)
member.detach(findMember);
:: DB에 insert query 넘김(Entity가 변경이 되었는데 실제로 UPDATE Query가 나가지 않는다.
transaction.commit();
준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
영속성 컨텍스트에서 지운 상태
준영속 상태로 만드는 방법 (영속 -> 준영속)
entityManager.detach(entity): 특정 엔티티만 준영속 상태로 전환
entityManager.clear(): 영속성 컨텍스트를 완전히 초기화
entityManager.close(): 영속성 컨텍스트를 종료
삭제(removed)
// 객체를 삭제한 상태
entityManager.remove(member);
@Transactional을 사용하는 경우 종료시 롤백처리가 된다.
전체 로직에 대해서@Transactional을 걸지 않으면
라인 하나가 하나의Transactional을 갖게 되며
자동으로 flush가 일어나게 된다.
전체 로직에 대해 @Transactional을 걸면
전체 로직이 실행되고 Transactional이 끝나는 시점에
자동으로 flush가 일어나게 된다.
Transaction


Transaction Rollback

public void putBookAndAuthor() {
Book book = new Book();
book.setName("JPA 시작하기");
bookRepository.save(book);
try {
authorService.putAuthor();
} catch (RuntimeException e) {
}
throw new RuntimeException("오류가 발생하였습니다. transaction은 어떻게 될까요?");
}
Rollback
트랜젝션으로 인한 하나의 묶음 처리가 시작되기 이전의 상태로 되돌린다.
isolation
격리수준이라는 옵션이다. 트랜잭션에서 일관성이 없는 데이터를 허용하도록
하는 수준을 말하는데 옵션은 다음과 같다.
READ_UNCOMMITTED (level 0)
- 트랜잭션에 처리중인 혹은 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용
- 어떤 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는
B라는 아직 완료되지 않은(Uncommitted 혹은 Dirty) 데이터 B를 읽을 수 있다.
- Dirty read : 위와 같이 다른 트랜잭션에서 처리하는 작업이 완료되지 않았는데도
다른 트랜잭션에서 볼 수 있는 현상을 dirty read 라고 하며,
READ UNCOMMITTED 격리수준에서만 일어나는 현상
READ_COMMITTED (level 1)
- dirty read 방지 : 트랜잭션이 커밋되어 확정된 데이터만을 읽는 것을 허용
어떠한 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안
다른 사용자는 해당 데이터에 접근할 수 없다.
REPEATABLE_READ (level 2)
- 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로
다른 사용자는 그 영역에 해당되는 데이터에 대한 수정이 불가능하다.
- 선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지
후행 트랜잭션이 갱신하거나 삭제하는 것을 불허함으로써
같은 데이터를 두 번 쿼리했을 때 일관성 있는 결과를 리턴함
SERIALIZABLE (level 3)
- 완벽한 읽기 일관성 모드를 제공
데이터의 일관성 및 동시성을 위해 MVCC(Multi Version Concurrency Control)을 사용하지 않음
(MVCC는 다중 사용자 데이터베이스 성능을 위한 기술로 데이터 조회 시 LOCK을 사용하지 않고
데이터의 버전을 관리해 데이터의 일관성 및 동시성을 높이는 기술)
트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에
shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에
대한 수정 및 입력이 불가능하다.
_______________________________________________________________________________
propagation ( 전파옵션)
REQUIRED : 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성
REQUIRES_NEW : 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성
SUPPORT : 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 nontransactionally로 실행
MANDATORY : 부모 트랜잭션 내에서 실행되며 부모 트랜잭션이 없을 경우 예외가 발생
NOT_SUPPORT : nontransactionally로 실행하며 부모 트랜잭션 내에서 실행될 경우 일시 정지
NEVER : nontransactionally로 실행되며 부모 트랜잭션이 존재한다면 예외가 발생
NESTED : 해당 메서드가 부모 트랜잭션에서 진행될 경우 별개로 커밋되거나
롤백될 수 있음. 둘러싼 트랜잭션이 없을 경우 REQUIRED와 동일하게 작동
transaction Isolation
- transaction 격리 단계
- 동시에 발생하는 transaction 간에 데이터를 어떻게 정의할것인지
public enum Isolation {
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
private final int value;
Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
transaction Propagation
- 전파
public enum Propagation {
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
NEVER(TransactionDefinition.PROPAGATION_NEVER),
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
Cascade (영속성 전이)
★ 영어로는 폭포를 의미하며 단계별로 전이가 발생
★ @OneToOne, @OneToMany, @ManyToOne 연관 관계가 있는 어노테이션에 cascade() 지원
★ CascadeType.java 에는 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH속성 지원
DETACH: 준영속(detach)으로 변경할 때 연관 엔티티도 준영속
REFRESH: 엔티티를 다시 로드할 때, 연관 엔티티도 재로드
ALL: 모든 경우에 연관 엔티티 전파
★ JPA는 자바코드를 SQL쿼리로 번역해주는 ORM
save()를 자주 사용함으로 자바관점에선 불필요한 코드 사용(save는 영속화 시키기 위함)
★ 영속성 전이를 이용해 객체 중심의 코드 수정이 가능
영속화하지 않아도 엔티티간 관계 설정이 가능 ( save() )
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) 로 연관관계 적용
PERSIST(등록), MERGE(수정) 등을 할 때 영속성 전이가 발생
public class Book extends BaseEntity{
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@ToString.Exclude
private Publisher publisher;
}
삭제처리, 고아제거 속성
★ 고아제거 속성
연관관계가 없는 엔티티를 제거하는 속성
연관관계를 제거하는 방법은 setter에 null값을 주입
book.setPublisher(null);
★ CascadeType.REMOVE, orphanRemoval의 특징
1. 설정하는 방법 (엔티티 속성)
CascadeType.REMOVE: @ManyToOne(cascade = {CascadeType.REMOVE}) 설정
orphanRemoval: @OneToMany(orphanRemoval = true) 설정
2. 동작의 차이
1. CascadeType.REMOVE:
상위객체의 remove 이벤트를 하위 엔티티에 영속성 이벤트를 전파하여 삭제
setter를 통해 null을 실행하면 관련된 엔티티가 삭제 안됌 (데이터 보존)
2. orphanRemoval: setter를 통해 null을 실행하면 관련된 엔티티가 삭제
소프트 delete 처리 (flag 사용)
1. 상용화 된 시스템에는 delete 삭제를 하지 않고 삭제 flag 컬럼을 생성 후 조건으로 검색
2. 아래의 예시는 deleted flag 컬럼을 설정하고 true는 삭제로 판단
java boolean타입은 db에 0과 1로 표시 (0: false, 1: true)
Repository에 매번 메소드를 추가하는 것은 불편하고 다른 버그가 발생할 확률 다수
Entity에 @Where를 추가하고 조건문 생성
@Where(clause = "deleted = false")
public class Book extends BaseEntity{ ... }
@Query
1. JPA Repository에 메소드로 조건을 여러개 설정하는 경우 이름 길어지는 단점
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
...
List<Book> findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(String name, LocalDateTime createdAt, LocalDateTime updatedAt);
}
_________________________________________
@SpringBootTest
class BookRepositoryTest {
...
@Test
void queryTest(){
System.out.println("findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual : "
+ bookRepository.findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(
"JPA 초격자 패키지",
LocalDateTime.now().minusDays(1L),
LocalDateTime.now().minusDays(1L)
));
}
}
날짜 관련 정보 및 default 설정 (data.sql)
@Column 속성에 columnDefinition 이용
(현업은 AutoDDL을 안쓰므로 잘 안쓰는 속성)
@Data
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public class BaseEntity implements Auditable {
@CreatedDate
@Column(columnDefinition = "datetime(6) default now(6)", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(columnDefinition = "datetime(6) default now(6)", nullable = false)
private LocalDateTime updatedAt;
}
주의 ! columnDefinition 은 지정 값 표시 이후, 설정 값은 이어서 표시 (타입 삭제 됌)
1. columnDefinition = "default now(6)"
created_at default now(6) not null,
updated_at default now(6) not null,
2. @Column(columnDefinition = "datetime(6) default now(6) null", nullable = false, updatable = false)
created_at datetime(6) default now(6) null not null,
3. @Column(columnDefinition = "datetime(6) default now(6) comment '수정시간'", nullable = false)
updated_at datetime(6) default now(6) comment '수정시간' not null,
JPQL쿼리
JPQL쿼리 사용하여 긴 메소드 문제 해결
@Test
void queryTest(){
List<Book> findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(String name, LocalDateTime createdAt, LocalDateTime updatedAt);
@Query(value = "select b from Book b "
+ "where name = ?1 and createdAt >= ?2 and updatedAt >= ?3 and category is null")
List<Book> findByNameRecently(String name, LocalDateTime createdAt, LocalDateTime updatedAt);
}
__________________________________________________
//동일한 결과 출력
findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual : [Book(super=BaseEntity(createdAt=2021-08-13T05:41:42.728762, updatedAt=2021-08-13T05:41:42.728762), id=1, name=JPA 패키지, category=null, deleted=false)]
findByNameRecently : [Book(super=BaseEntity(createdAt=2021-08-13T05:41:42.728762, updatedAt=2021-08-13T05:41:42.728762), id=1, name=JPA 패키지, category=null, deleted=false)]
__________________________________________________
@Query에 사용된 쿼리 문법을 JPQL이라고 부름 (데이터베이스 쿼리 X)
JPA 엔티티를 기반으로 하는 쿼리
Book은 데이터베이스가 아닌 엔티티를 사용
################################
1. JPQL에 동적으로 파라미터를 설정하는 방법 2가지
물음표와 숫자 기반에 파라미터 매핑
################################
2. 네임 기반에 파라미터 매핑
@Param과 :을 사용하여 값을 매핑
순서와 상관이 없어 파라미터의 변경여부와 상관없이 결과값 동일
예) @Param("name")로 선언 된 파라미터와 :name이 연결 상태
@Query(value = "select b from Book b "
+ "where name = :name and createdAt >= :createdAt and updatedAt >= :updatedAt and category is null")
List<Book> findByNameRecently(
@Param("name") String name,
@Param("createdAt") LocalDateTime createdAt,
@Param("updatedAt") LocalDateTime updatedAt);
}
_________________________________________
2.1 Tuple 사용
BookRepository.java
- @Query(value = "select b.name as name, b.category as category from Book b")
- List<Tuple> findBookNameAndCategory();
BookRepositoryTest.java
- bookRepository.findBookNameAndCategory().forEach(tuple -> {
- System.out.println(tuple.get(0) + " : " + tuple.get(1));
- });
_________________________________________
2.2 Interface 사용
BookRepository.java
- @Query(value = "select b.name as name, b.category as category from Book b")
- List<BookNameAndCategory> findBookNameAndCategory();
BooknameAndCategory.java
- public interface BookNameAndCategory {
- String getName();
- String getCategory();
- }
BookRepositoryTest.java
- bookRepository.findBookNameAndCategory().forEach(b -> {
- System.out.println(b.getName() + " : " + b.getCategory());
- });
_________________________________________
2.3 DTO 사용
BookRepository.java
- @Query(value = "select new com.example.bookmanager.repository.dto.BookNameAndCategory(b.name, b.category) from Book b")
- List<BookNameAndCategory> findBookNameAndCategory();
BookNameAndCategory.java
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class BookNameAndCategory {
- private String name;
- private String category;
- }
BookRepositoryTest.java
- bookRepository.findBookNameAndCategory().forEach(b -> {
- System.out.println(b.getName() + " : " + b.getCategory());
- });
################################
3. JPQL을 사용해서 Paging처리
Repository에 findAll()과 같은 기능을 제공 (Pagable 파라미터 필요)
BookRepository.java
- @Query(value = "select new com.example.bookmanager.repository.dto.BookNameAndCategory(b.name, b.category) from Book b")
- List<BookNameAndCategory> findBookNameAndCategory(Pageable pageable);
BookRepositoryTest.java
- bookRepository.findBookNameAndCategory(PageRequest.of(1, 1)).forEach(
bookNameAndCategory -> System.out.println(bookNameAndCategory.getName() + " : " + bookNameAndCategory.getCategory()));
//결과: Spring : null
- bookRepository.findBookNameAndCategory(PageRequest.of(0, 1)).forEach(
bookNameAndCategory -> System.out.println(bookNameAndCategory.getName() + " : " + bookNameAndCategory.getCategory()));
//결과: JPA 패키지 : null
Native 쿼리 사용
- 가급적 사용을 피하기.
- @Query 어노테이션에서 nativeQuery 속성을 true로 설정하면 사용 가능
- JPQL쿼리와 다르게 Entity 속성을 사용할 수 없음
@Query(value = "select new com.example.bookmanager.repository.dto.BookNameAndCategory
(b.name, b.category) from Book b")
- JPA에 Dialect(방언)을 사용할 수 없음
- 테스트(h2)와 운영(MySQL)에서 이종 데이터베이스를 사용할 경우 문제 발생
BookRepository.java
- @Query(value = "select * from book", nativeQuery = true)
- List<Book> findAllCustom();
BookRepositoryTest.java
- @Test
- void nativeQueryTest() {
- bookRepository.findAll().forEach(System.out::println); // JPA
- bookRepository.findAllCustom().forEach(System.out::println); //native Query
- }
실행결과
- JPA Repositry 사용
- 쿼리에 alias가 자동생성
- Entity 설정에 Where 설정으로 인해 조건이 생성
Hibernate:
select
book0_.id as id1_2_,
book0_.created_at as created_2_2_,
book0_.updated_at as updated_3_2_,
book0_.category as category4_2_,
book0_.deleted as deleted5_2_,
book0_.name as name6_2_,
book0_.publisher_id as publishe7_2_
from
book book0_ where ( book0_.deleted = 0 )
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.941847, updatedAt=2021-08-14T08:34:13.941847), id=1, name=JPA 패키지, category=null, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.948709, updatedAt=2021-08-14T08:34:13.948709), id=2, name=Spring, category=null, deleted=false)
//native Query 사용
//작성한 native Query만 실행
Hibernate: select * from book
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.941847, updatedAt=2021-08-14T08:34:13.941847), id=1, name=JPA 패키지, category=null, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.948709, updatedAt=2021-08-14T08:34:13.948709), id=2, name=Spring, category=null, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.954006, updatedAt=2021-08-14T08:34:13.954006), id=3, name=Spring Security, category=null, deleted=true)
_________________________________________________________
Native 쿼리를 사용하는 이유
1. 성능에 대한 문제를 해결하는데 이용 (대표적으로 update)
# delete는 한번에 삭제하는 메소드를 제공
# deleteAllInBatch : 조건 없는 삭제 (delete from book;)
# deleteInBatch : findAll 한 후 레코드 하나하나를 id값으로 delete
# update는 한개씩 처리하는 방법만 제공 (saveAll도 findAll 후 id값으로 수정)
BookRepository.java
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
@Transactional
@Modifying
@Query(value = "update book set category = 'IT전문서'", nativeQuery = true)
int updateCategories();
}
# JPA Repository를 사용하지 않으므로 insert, update, delete에는 @Transactional이 필요
# @Transactional은 interface보다 구체 클레스에서 사용하기를 권고 interface는 interface-base-proxies경우에 사용 (JpaRepository는 proxies)
# Repository에 사용하지 않은 경우 각 클레스마다 설정해야하는 불편함과 문제 발생
# @Modifying을 통해 insert, update, delete임을 명시하고 실행 결과를 int로 반환 가능
BookRepositoryTest.java
@Test
void nativeQueryTest() {
System.out.println("affected rows : " + bookRepository.updateCategories());
bookRepository.findAllCustom().forEach(System.out::println);
}
실행결과
//bookRepository.updateCategories()
Hibernate: update book set category = 'IT전문서'
affected rows : 3
//bookRepository.findAllCustom()
Hibernate: select * from book
Book(super=BaseEntity(createdAt=2021-08-14T09:05:52.431619, updatedAt=2021-08-14T09:05:52.431619), id=1, name=JPA 초격자 패키지, category=IT전문서, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T09:05:52.438086, updatedAt=2021-08-14T09:05:52.438086), id=2, name=Spring, category=IT전문서, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T09:05:52.442247, updatedAt=2021-08-14T09:05:52.442247), id=3, name=Spring Security, category=IT전문서, deleted=true)
_______________________________________________
2. JPA에서 제공하지 않는 기능을 사용하는 경우
# show tables;, show databases; 과 같이
JPA에서 제공하지 않는 쿼리를 이용해야 할 경우
BookRepository.java
@Query(value = "show tables", nativeQuery = true)
List<String> showTables();
BookRepositoryTest.java
@Test
void nativeQueryTest() {
System.out.println(bookRepository.showTables());
}
//실행결과
Hibernate: show tables
[address, author, book, book_and_author, book_review_info, publisher, review, user, user_history]
Converter 사용하기
- JPA ORM은 일종에 interface : 데이터베이스의 레코드를 자바에 객체화 시켜주는 역할
- 자바의 객체화와 DB 데이터 형식이 다른경우
1. JPA에서 Enum을 사용하는 경우 Convert를 사용
- OrdinalEnumValueConverter 구현체를 통해 JPA는 Enum, DB엔 Integer로 표현
2. Enum, 인베디드 데이터를 못쓰는 경우 발생 → 레거시 시스템, 다른 시스템과 연동,
Integer 코드로 존재하는 경우
- ORM과 같이 Convert를 하기 위해 필요한 class가 AttributeConverter
사용 예시
1. Convert할 대상인 BookStatus.java를 생성
@Data
public class BookStatus {
private int code;
private String description;
public BookStatus(int code){
this.code = code;
this.description = parseDescription(code);
}
public boolean isDisplayed(){
return code == 200;
}
private String parseDescription(int code) {
switch (code) {
case 100 :
return "판매종료";
case 200:
return "판매중";
case 300:
return "판매보류";
default:
return "미지원";
}
}
}
2. DB-Entity에 Covnert역할을 하는 BookStatusConverter.java를 추가
# AttributeConverter interface 구현이 필요
# AttributeConverter<BookStatus, Integer>는 Convert할 Entity와 DB타입을 표시
# @Converter 어노테이션 추가, Convert할 클래스임을 표시
@Converter
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {
# convertToDatabaseColumn는 Entity → 데이터베이스로 변환할 메소드
@Override
public Integer convertToDatabaseColumn(BookStatus attribute) {
return attribute.getCode();
}
# convertToEntityAttibute는 데이터베이스 → Entity로 변환할 메소드
@Override
public BookStatus convertToEntityAttribute(Integer dbData) {
return dbData != null ? new BookStatus(dbData) : null;
}
}
3. Book Entity에 Convert할 속성인 BookStatus를 추가
# @Convert(converter = BookStatusConverter.class)는 어떤 class를 통해 Convert 할지 지정
public class Book extends BaseEntity{
@Convert(converter = BookStatusConverter.class)
private BookStatus status; // 판매상태
}
4. 결과 확인
- data.sql >> insert문에 status값을 추가
insert into book(`id`, `name`, `publisher_id`, `deleted`, `status`) values(1, 'JPA 패키지', 1, false, 100);
insert into book(`id`, `name`, `publisher_id`, `deleted`, `status`) values(2, 'Spring', 1, false, 200);
insert into book(`id`, `name`, `publisher_id`, `deleted`, `status`) values(3, 'Spring Security', 1, true, 100);
- DB변환 된 status (Integer) 확인을 위해 native쿼리사용, JPA에선 BookStatus객체 반환
@Query(value = "select * from book order by id desc limit 1", nativeQuery = true)
Map<String, Object> findRowRecord();
@Test
void convertTest(){
bookRepository.findAll().forEach(System.out::println);
Book book = new Book();
book.setName("또 다른 전문서적");
book.setStatus(new BookStatus(200));
bookRepository.save(book);
System.out.println(bookRepository.findRowRecord().values());
}
//결과
//bookRepository.findAll().forEach(System.out::println);
Book(super=BaseEntity(createdAt=2021-08-14T12:08:05.490965, updatedAt=2021-08-14T12:08:05.490965), id=1, name=JPA 초격자 패키지, category=null, deleted=false, status=BookStatus(code=100, description=판매종료))
Book(super=BaseEntity(createdAt=2021-08-14T12:08:05.497410, updatedAt=2021-08-14T12:08:05.497410), id=2, name=Spring, category=null, deleted=false, status=BookStatus(code=200, description=판매중))
//System.out.println(bookRepository.findRowRecord().values());
[4, 2021-08-14 12:08:06.090323, 2021-08-14 12:08:06.090323, null, false, 또 다른 전문서적, 200, null]
______________________________________________
Converter를 사용할 때 나올 수 있는 문제점
- 조회만하는 경우에도 convertToDatabaseColumn를 모두 구현해야 문제가 발생하지 않음
- 영속성 컨텍스트가 구현되지 않은 메소드를 통해 변경감지로 인식 (update실행)
BookStatusConverter.java
@Override
public Integer convertToDatabaseColumn(BookStatus attribute) { return null; }
BookService.java
@Transactional
public List<Book> getAll(){
List<Book> books = bookRepository.findAll();
books.forEach(System.out::println);
return books;
}
BookServiceTest.java
@Test
void converterErrorTest() {
bookService.getAll();
# Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.083990, updatedAt=2021-08-14T12:28:55.083990), id=1, name=JPA 패키지, category=null, deleted=false, status=BookStatus(code=100, description=판매종료))
# Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.092185, updatedAt=2021-08-14T12:28:55.092185), id=2, name=Spring, category=null, deleted=false, status=BookStatus(code=200, description=판매중))
bookRepository.findAll().forEach(System.out::println);
# Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.083990, updatedAt=2021-08-14T12:28:55.778718), id=1, name=JPA 초격자 패키지, category=null, deleted=false, status=null)
# Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.092185, updatedAt=2021-08-14T12:28:55.812716), id=2, name=Spring, category=null, deleted=false, status=null)
# 영속성 컨텍스트가 구현되지 않은 컨버터를 통해 변경감지를 하고 null로 update 실행
}
Convert에 autoApply속성
@Converter(autoApply = true) 를 지정하면 객체 타입을 통해 자동 매핑
IntegerConvert, StringConvert 등 많은 곳에서 사용하는 경우 문제 발생할 확률이 높음
용도가 명확한 클레스(BookStatus)를 사용하는 경우에 사용하는 것을 권장
BookStatusConverter.java
@Converter(autoApply = true)
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> { }
Book.java
:: autoApply속성 사용하는경우 @Convert(converter = BookStatusConverter.class) 없어도 된다.
:: @Convert(converter = BookStatusConverter.class)
private BookStatus status; // 판매상태
Session의 Flush 개념
Hibernate의 Flush 타이밍
- 일반적으로 Transaction이 Commit될 때
Session의 모든 변경사항이 DB로 Flush가 이뤄진다.
하지만 영속성 컨텍스트의 Flush 시점 역시 커스터 마이징이 가능하다
- 기본적으로 Hibernate는 다음과 같은 경우에 Session을 DB에 Flush 한다.
1. session.flush( )를 명시적으로 호출하는 경우
2. Hibernate Transaction이 Commit 되는 경우
3. 쿼리를 실행하기 전에 영속성 컨텍스트의 상태가 쿼리 결과에 영향을 미친다고 판단되는 경우
Session.setFlushMode( )
Session.setFlushMode( ) 메소드를 사용하면 Hibernate의 기본 Flush 규칙을 변경할 수 있다.
Hibernate의 Session Flush 모드는 4가지이다.
기본값은 FlushMode.AUTO 로 위에서 설명한 3가지 경우에 영속성 컨텍스트를 Flush 한다.
1. FlushMode.ALWAYS
모든 쿼리를 실행하기 전에 영속성 컨텍스트를 Flush 한다.
2. FlushMode.AUTO
기본 Flush 모드로 위에서 설명한 3가지 경우에 영속성 컨텍스트를 Flush 한다.
3. FlushMode.COMMIT
쿼리를 실행하기 전에는 Flush하지 않는다.
- session.flush( )를 명시적으로 호출하는 경우
- Hibernate Transaction이 Commit 되는 경우
4. FlushMode.MANUAL
명시적으로 flush( )를 호출할 때만 영속성 컨텍스트를 Flush 한다.
- session.flush( )를 명시적으로 호출하는 경우