@Test
void embedTest(){
...
User user1 = new User();
user1.setName("joshua");
user1.setHomeAddress(null);
user1.setCompanyAddress(null);
userRepository.save(user1);
User user2 = new User();
user2.setName("jordan");
user2.setHomeAddress(new Address());
user2.setCompanyAddress(new Address());
userRepository.save(user2);
entityManager.clear(); //영속성 컨텍스트 초기화 (준영속)
userRepository.findAll().forEach(System.out::println);
userRepository.findAllRowRecords().forEach(a -> System.out.println(a.values()));
}
영속성 컨텍스트 캐쉬에는 Address가 null인 user1과 빈 객체를 가진 user2 모두 존재
entityManager.clear()를 통해서 캐쉬를 지우고 새로 엔티티를 로딩해서 데이터 확인
clear()를 하지 않는 경우
@Test
void embedTest(){
...
//entityManager.clear();
assertAll(
() -> assertNull(userRepository.findById(7L).get().getHomeAddress()),
() -> assertEquals(userRepository.findById(8L).get().getHomeAddress().getClass(), Address.class)
);
}
clear를 통해 성능은 향상 되지만 캐시를 지움으로서 테스트가 실패됨
-> 영속성 캐시에서 가지고 있는 자바 엔티티와 DB 레코드간 불일치가 발생
@Test
void embedTest(){
...
entityManager.clear();
assertAll(
() -> assertNull(userRepository.findById(7L).get().getHomeAddress()),
() -> assertEquals(userRepository.findById(8L).get().getHomeAddress().getClass(), Address.class)
);
}
//실행결과
Multiple Failures (1 failure)
java.lang.NullPointerException: <no message>
...
영속성 컨텍스트 캐쉬는 ms까지 표시 데이터베이스는 초까지 표시 (@Column(columnDefinition = "datetime" )
EntityManager.clear()를 하면 데이터베이스에 값을 조회함으로 초까지만 표시
Comment.java (commentedAt 추가)
...
public class Comment extends BaseEntity{
...
@Column(columnDefinition = "datetime")
private LocalDateTime commentedAt;
}
CommentRepository.java 생성
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
CommentRepositoryTest.java
@Test
@Transactional
void commentTest() {
Comment comment = commentRepository.findById(3L).get();
comment.setCommentedAt(LocalDateTime.now());
commentRepository.saveAndFlush(comment);
entityManager.clear(); //clear 여부로 결과가 다름 (주석여부로 테스트)
System.out.println(commentRepository.findById(3L).get());
}
//실행 결과
//entityManager.clear()가 있는 경우 (데이터베이스 조회)
Comment(super=BaseEntity(createdAt=2021-08-18T21:45:31.205062, updatedAt=2021-08-18T21:45:31.907460), Id=3, comment=그냥 그랬습니다., commentedAt=2021-08-18T21:45:32)
//entityManager.clear()가 없는 경우 (영속성 컨텍스트 캐쉬 조회)
Comment(super=BaseEntity(createdAt=2021-08-18T21:49:13.881998, updatedAt=2021-08-18T21:49:14.463898700), Id=3, comment=그냥 그랬습니다., commentedAt=2021-08-18T21:49:14.402901700)
앞선 컬럼 Definition을 "dateTime(6) default now(6) 설정 후
// 이 경우 DB 테이블에 디폴트 값을 이렇게 지정하면 commentedAt 값을 세팅하지 않으면
// 디비 에서 자동적으로 timestamp 값 넣도록
@Column(columnDefinition = "datetime(6) default now(6)")
private LocalDateTime commentedAt; // 댓글 남긴 시간 추가했다고 가정
@Test
@Transactional
void commentTest(){
Comment comment = new Comment();
comment.setComment("별로에요");
commentRepository.saveAndFlush(comment); // 그냥 save 시에는 다름, 테스트 위해 flush로 먼저 반영하엿음
// entityManager.clear(); //clear 여부로 결과가 다름 (주석여부로 테스트), 영속성 캐쉬값 날리는 코드
System.out.println(comment);
commentRepository.findAll().forEach(System.out::println);
}
// 결과
Comment(super=BaseEntity(createdAt=2022-11-24T17:06:12.892273, updatedAt=2022-11-24T17:06:12.892273), id=4, comment=별로에요, commentedAt=null)
// DB 반영 내용
Comment(super=BaseEntity(createdAt=2022-11-24T17:06:12.076920, updatedAt=2022-11-24T17:06:12.076920), id=1, comment=저도 좋았어요, commentedAt=2022-11-24T17:06:12.076920)
Comment(super=BaseEntity(createdAt=2022-11-24T17:06:12.077634, updatedAt=2022-11-24T17:06:12.077634), id=2, comment=저는 별로 였습니다., commentedAt=2022-11-24T17:06:12.077634)
Comment(super=BaseEntity(createdAt=2022-11-24T17:06:12.077940, updatedAt=2022-11-24T17:06:12.077940), id=3, comment=저는 그냥 그랬습니다., commentedAt=2022-11-24T17:06:12.077940)
Comment(super=BaseEntity(createdAt=2022-11-24T17:06:12.892273, updatedAt=2022-11-24T17:06:12.892273), id=4, comment=별로에요, commentedAt=null)
commented_at 에 null 값이 들어감
-> 엔티티에는 Setter를 쓰지않았기에
Hibernate:
insert
into
comment
(created_at, updated_at, comment, commented_at, review_id)
values
(?, ?, ?, ?, ?)
data.sql의 insert comment
commented_at 칼럼을 넣지 않았음
-> 디비의 디폴트 값을 처리하게 됨 (원래라면)
-> 반면 insert시 null을 넣은것을 볼 수 있다
-> insert into시 쿼리에 commented_at 칼럼을 넣지 않으면 DB 디폴트 값이 반영되지만 칼럼을 포함하여 null 입력 시 디폴트 값을 받지 않는 것을 볼 수 있다
jpa는 모든 칼럼을 insert 칼럼으로 사용하기로 처리하기 때문
반면 Comment 클래스에 DynamicInsert
지정 시
@DynamicInsert
public class Comment extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String comment;
@ManyToOne // 하나의 리뷰에 여러 댓글
@ToString.Exclude
private Review review;
// 이 경우 DB 테이블에 디폴트 값을 이렇게 지정하면 commentedAt 값을 세팅하지 않으면
// 디비 에서 자동적으로 timestamp 값 넣도록
@Column(columnDefinition = "datetime(6) default now(6)")
private LocalDateTime commentedAt; // 댓글 남긴 시간 추가했다고 가정
}
insert 시점에 동적으로 반영
insert 문에 데이터가 존재하는것만 포함시켜 진행
-> set CommentedAt 메서드가 실행되지 않았기에 insert문에서 제외
이후 위에 테스트 코드에서 clear()문을 실행 시
Comment(super=BaseEntity(createdAt=2022-11-24T17:18:35.495753, updatedAt=2022-11-24T17:18:35.495753), id=4, comment=별로에요, commentedAt=null)
Comment(super=BaseEntity(createdAt=2022-11-24T17:18:34.800511, updatedAt=2022-11-24T17:18:34.800511), id=1, comment=저도 좋았어요, commentedAt=2022-11-24T17:18:34.800511)
Comment(super=BaseEntity(createdAt=2022-11-24T17:18:34.801287, updatedAt=2022-11-24T17:18:34.801287), id=2, comment=저는 별로 였습니다., commentedAt=2022-11-24T17:18:34.801287)
Comment(super=BaseEntity(createdAt=2022-11-24T17:18:34.801687, updatedAt=2022-11-24T17:18:34.801687), id=3, comment=저는 그냥 그랬습니다., commentedAt=2022-11-24T17:18:34.801687)
Comment(super=BaseEntity(createdAt=2022-11-24T17:18:35.495753, updatedAt=2022-11-24T17:18:35.495753), id=4, comment=별로에요, commentedAt=2022-11-24T17:18:35.513536)
commentedAt 에 null이 들어가있지만 DB에는 값이 반영됨
-> 엔티티 캐시로 인해 DB의 실제 값과, 엔티티 간에 불일치가 발생하는 케이스
실제로 해당 캐쉬와 실제 값이 틀어지는 경우 많은 문제
-> ex) 이미 탈퇴한 회원이지만, 캐쉬에 값이 남아있어 로그인이 가능해짐
영속성 컨텍스트는 @Transactional안에 있는 로직을 관리
영속화 되어 관리되는 엔티티에서 수정이 발생하면 save()가 없어도 수정문 실행
@Transactional(session 설정)을 제거하면 그 안에 영속성 컨텍스트를 관리하지 않으므로 수정문 실행X
-> comment 엔티티는 findAll 직후 영속성 관리가 끝남
//CommentService.java
@Transactional
public void updateSomething() {
List<Comment> comments = commentRepository.findAll();
for (Comment comment : comments){
comment.setComment("별로에요");
// commentRepository.save(comment);
}
}
//CommentServiceTest.java
@Test
void commentTest() {
commentService.init();
commentService.updateSomething();
}
//실행결과
//조회 후 수정쿼리 실행
...
Hibernate:
update
comment
set
updated_at=?,
comment=?
where
id=?
만약 CommentService.java에
@Transactional
public void insertSomething(){
Comment comment = new Comment();
comment.setComment("이건 뭐죠?");
// commentRepository.save(book);
}
이러한 메서드를 만든 후 테스트에서 실행된다 해도 더티 체크 진행 안됨
-> DB 영속화하는 과정이 없기 때문
-> 하고자한다면 영속화(ex : save) : 주석 풀기
-> 또는 코드 첫줄에 Comment comment = commentRepository.findById(1L).get();
-> 이러면 findById에 의해 영속성 컨텍스트에 의해 관리되는 상태가 됨
영속성 컨텍스트의 변경 감지를 통해 예상치 못한 수정이 발생 (save() 없는데 수정문 실행)
성능적인 이슈 발생
트랜잭션 내에서 조회를 한 엔티티에 대해선 Dirty Check하는 과정 발생 (대용량 데이터 경우 위험)
@Transactional(readOnly = true)
를 사용, 영속성 컨텍스트(seesion)의 Flush Mode가 MANUAL
로 설정 (default Auto)
Flush가 Auto로 발생하지 않으므로 Dirty Check가 생략
//CommentService.java
@Transactional(readOnly = true)
public void updateSomething() {
List<Comment> comments = commentRepository.findAll();
for (Comment comment : comments){
comment.setComment("별로에요");
// commentRepository.save(comment);
}
}
//CommentServiceTest.java
@Test
void commentTest() {
commentService.init();
commentService.updateSomething();
}
//실행결과
//조회 쿼리만 실행
select
comment0_.id as id1_4_,
comment0_.created_at as created_2_4_,
comment0_.updated_at as updated_3_4_,
comment0_.comment as comment4_4_,
comment0_.commented_at as commente5_4_,
comment0_.review_id as review_i6_4_
from
comment comment0_
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID>{
...
}
JPA 구현체에 readOnly true가 설정되어있기에 findAll()과 같은 메서드에 기본적으로 적용되어 있음
@Override
public List<T> findAll() {
return getQuery(null, Sort.unsorted()).getResultList();
}
save의 경우에는 별도의 트랜잭션을 적용하고있음
-> dirty check 적용
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}