[JPA] JPA가 Entity를 판별하는 방법과 save()의 비밀(entityInformation.isNew(entity))
JPA
를 사용하여 엔티티를 매핑하면 저도 모르게@Id
@GeneratedValue
을 사용하는 모습을 봤습니다.
그런데 @GeneratedValue
을 사용하지 않으면 어떻게 될까요? 🤔
이점이 궁금하게 되어 해당 결과를 공유하고자 포스팅 합니다.^^
SimpleJpaRepository.save()
Spring data jpa
에서 제공하는 CrudRepositroy
인터페이스의 구현체인 SimpleJpaRepository
save()
메소드는 다음과 같이 구현되어 있습니다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
@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);
}
}
}
여기서 주목해야 할것은 entityInformation.isNew(entity)
입니다.isNew()
의 반환 타입에 따라 persist()
를 실행할지 merge()
를 실행할지 결정합니다.
persist()
와 merge()
의 기능을 까먹으신 분들을 위해 설명하면 다음과 같습니다. 🤗
persist()
는 비영속 상태의 객체를 영속 상태로 만듭니다.merge()
는 준영속 상태의 객체를 영속 상태로 만듭니다.
isNew()
isNew()
메소드는 새로운 Entity
를 판단하기 위해 Id
값을 확인 합니다.
아래와 같은 경우에 새로운 Entity
라고 판단하고 true
를 반환 합니다.
(그외는 flase
를 반환 합니다.)
Id
의 타입이Object
타입이고null
Id
의 타입이Primitive
타입이고0
Persistable
인터페이스를 구현한 경우
Persistable
인터페이스
public interface Persistable<ID> {
/**
* Returns the id of the entity.
*
* @return the id. Can be {@literal null}.
*/
@Nullable
ID getId();
/**
* Returns if the {@code Persistable} is new or was persisted already.
*
* @return if {@literal true} the object is new.
*/
boolean isNew();
}
잠깐 여기에 isNew()
가 보이나요? 👀
이 메소드가 바로 save()
에서 사용된 메소드 입니다.
@GeneratedValue
를 사용하면 생성 전략에 따라서 자동으로 Id
를 생성합니다.
- IDENTITY
- PK 생성 전략을 데이터 베이스에 위임(MySQL)
- 쓰기 지연 SQL 저장소 사용 불가
- SEQUENCE
- 데이터베이스 시퀀스 오브젝트 사용(ORACLE)
@SequenceGenerator
필요
- TABLE
- 키 생성용 테이블 사용(모든 데이터베이스 사용 가능)
@TableGenerator
필요
- AUTO
- 방언에 따라 자동 지정(기본값)
결국 save()
를 호출하는 시점에 isNew()
를 통해
해당 Entity
의 Id
의 생성 전략을 판단한 뒤persist()
를 수행하거나merge()
를 수행하게 됩니다.
이말이 사실인지 확인해 볼까요?
@GeneratedValue 사용 🙆♂️
먼저 @GeneratedValue
를 포함한 Entity
를 작성합니다.
@Entity
@Setter @Getter
public class TestEntityGeneratedValue {
@Id
@GeneratedValue
private Long id;
private String name;
}
Repository
public interface TestEntityGeneratedValueRepository
extends JpaRepository<TestEntityGeneratedValue, Long> {
}
테스트 코드
@SpringBootTest
public class SaveTestGeneratedValue {
@Autowired
TestEntityGeneratedValueRepository repository;
@Autowired
EntityManager em;
@Test
@Transactional
@Rollback(value = false) // sql을 확인하기 위함
void saveTest() {
TestEntityGeneratedValue entity = new TestEntityGeneratedValue();
entity.setName("안녕!");
repository.save(entity);
System.out.println("id="+entity.getId());
System.out.println("name="+entity.getName());
entity.setName("변경감지!");
System.out.println("id="+entity.getId());
System.out.println("name="+entity.getName());
}
}
saveTest()
는 하나의 트랜잭션에서 실행되기 때문에 entity
가 영속성 상태를 유지하게 되어 변경감지가 일어나게 됩니다.
결과
id=1
name=안녕!
id=1
name=변경감지!
Hibernate:
insert
into
test_entity_generated_value
(name, id)
values
(?, ?)
binding parameter [1] as [VARCHAR] - [안녕!]
binding parameter [2] as [BIGINT] - [1]
Hibernate:
update
test_entity_generated_value
set
name=?
where
id=?
binding parameter [1] as [VARCHAR] - [변경감지!]
binding parameter [2] as [BIGINT] - [1]
트랜잭션이 커밋되기 전까지 쓰기 지연 SQL 저장소
에 저장되었다가
커밋 시점에 SQL
이 발생하는것을 확인할 수 있습니다. 👍
그렇다면 @GeneratedValue
를 사용하지 않으면 어떻게 될까요? 🤔
@GeneratedValue 사용 🙅♀️
먼저 @GeneratedValue
를 포함하지 않은 Entity
를 작성합니다.
@Entity
@Setter @Getter
public class TestEntityOnlyId {
@Id
private String id;
private String name;
}
Repository
public interface TestEntityOnlyIdRepository
extends JpaRepository<TestEntityOnlyId, String> {
}
테스트 코드
@SpringBootTest
public class SaveTestOnlyId {
@Autowired
TestEntityOnlyIdRepository repository;
@Test
@Transactional
@Rollback(value = false)
void saveTest() {
String id = UUID.randomUUID().toString();
TestEntityOnlyId entity = new TestEntityOnlyId();
entity.setId(id);
entity.setName("반가워!");
// merge 수행
TestEntityOnlyId newEntity = repository.save(entity);
System.out.println("id="+entity.getId());
System.out.println("name="+entity.getName());
// entity는 영속상태가 아니다.!
entity.setName("entity는 영속상태 아니라서 변경감지가 안된다.!");
System.out.println("id="+entity.getId());
System.out.println("name="+entity.getName());
// newEntity는 영속상태 입니다.!
newEntity.setName("newEntity는 변경감지가 된다!");
System.out.println("id="+newEntity.getId());
System.out.println("name="+newEntity.getName());
}
}
save()
에서 persist()
가 아니고merge()
를 수행하는 이유는
앞에서 설명드린 isNew()
에서 flase
가 반환되기 때문입니다.
merge(entity)
가 수행되고 반환되는 객체(newEntity
)는 영속상태 입니다.
여기서 주의깊게 보셔야할 것이 entity
는 영속상태가 아니라는 것 입니다.
새롭게 반환되는 newEntity
가 영속상태 입니다.!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
따라서 entity
는 비영속 상태 이기때문에 변경감지가 발생하지 않는 것을 확인할 수 있습니다.
그에 반해 newEntity
는 영속 상태 이기 때문에 변경감지가 발생합니다.
결과
Hibernate:
select
testentity0_.id as id1_6_0_,
testentity0_.name as name2_6_0_
from
test_entity_only_id testentity0_
where
testentity0_.id=?
binding parameter [1] as [VARCHAR] - [a36419f4-b7ec-493d-afe8-c5cde8c87a5b]
id=1b94dec5-58f3-4881-9e86-ccf121c3f2ae
name=반가워!
id=1b94dec5-58f3-4881-9e86-ccf121c3f2ae
name=entity는 영속상태 아니라서 변경감지가 안된다.!
id=1b94dec5-58f3-4881-9e86-ccf121c3f2ae
name=newEntity는 변경감지가 된다!
Hibernate:
insert
into
test_entity_only_id
(name, id)
values
(?, ?)
binding parameter [1] as [VARCHAR] - [반가워!]
binding parameter [2] as [VARCHAR] - [1b94dec5-58f3-4881-9e86-ccf121c3f2ae]
Hibernate:
update
test_entity_only_id
set
name=?
where
id=?
binding parameter [1] as [VARCHAR] - [newEntity는 변경감지가 된다!]
binding parameter [2] as [VARCHAR] - [1b94dec5-58f3-4881-9e86-ccf121c3f2ae]
@GeneratedValue
가 없으면 JPA
에서 PK
를 관리하지 않고,Id
의 값이 null
이 아니라isNew()
에서 false
를 반환했고,
그 결과 merge()
호출했습니다.
(merge()
의 기본 동작은 select -> insert(또는 update)
입니다.)
여기에서는 merge()
로 인하여 select -> insert
가 발생했음을 확인할 수 있습니다.
추가로 entity
는 변경감지가 안되고, newEntity
는 변경감지가 되는 것을 확인할 수 있습니다.^^
merge() 주의 사항
merge() 뭐지 뭐지? ㅎㅎㅎmerge()
를 사용할 때 주의사항에 대해서 말씀 드리겠습니다. 🤣
merge()
의 기본 동작은 select -> insert(또는 update)
입니다.
여기에서 update
는 우리가 알고있는 변경감지 기능이 아닙니다.
변경감지는 변경된 필드만 update
가 되는데,merge()
로 발생하는 update SQL
은 전체 필드를 갱신합니다…..!
예제를 보여드리겠습니다.
@SpringBootTest
public class MergeUpdateTest {
@Autowired
TestEntityOnlyIdRepository repository;
@Test
@Rollback(value = false)
void testMergeUpdate() {
String id = UUID.randomUUID().toString();
TestEntityOnlyId testEntity = new TestEntityOnlyId();
testEntity.setId(id);
testEntity.setName("테스트");
testEntity.setAge(10);
// 트랜잭션 시작
repository.save(testEntity);
// 트랜잭션 종료
System.out.println("testEntity = " + testEntity);
TestEntityOnlyId testEntity2 = new TestEntityOnlyId();
testEntity2.setId(id);
testEntity2.setName("테스트2");
// 트랜잭션 시작
repository.save(testEntity2);
// 트랜잭션 종료
System.out.println("testEntity2 = " + testEntity2);
}
}
첫 번째 save()
에서 merge()
가 실행되고, select -> insert
를 수행합니다.
두 번째 save()
에서 merge()
가 실행되고, select -> update
를 수행합니다.
(save()
구현부를 보면 @Trasactional
이 있습니다.)
Hibernate:
select
testentity0_.id as id1_6_0_,
testentity0_.age as age2_6_0_,
testentity0_.name as name3_6_0_
from
test_entity_only_id testentity0_
where
testentity0_.id=?
binding parameter [1] as [VARCHAR] - [21999815-5ce5-4937-b25e-cadc52826b99]
Hibernate:
insert
into
test_entity_only_id
(age, name, id)
values
(?, ?, ?)
binding parameter [1] as [INTEGER] - [10]
binding parameter [2] as [VARCHAR] - [테스트]
binding parameter [3] as [VARCHAR] - [21999815-5ce5-4937-b25e-cadc52826b99]
testEntity = TestEntityOnlyId(id=21999815-5ce5-4937-b25e-cadc52826b99, name=테스트, age=10)
Hibernate:
select
testentity0_.id as id1_6_0_,
testentity0_.age as age2_6_0_,
testentity0_.name as name3_6_0_
from
test_entity_only_id testentity0_
where
testentity0_.id=?
binding parameter [1] as [VARCHAR] - [21999815-5ce5-4937-b25e-cadc52826b99]
extracted value ([age2_6_0_] : [INTEGER]) - [10]
extracted value ([name3_6_0_] : [VARCHAR]) - [테스트]
Hibernate:
update
test_entity_only_id
set
age=?,
name=?
where
id=?
binding parameter [1] as [INTEGER] - [null]
binding parameter [2] as [VARCHAR] - [테스트2]
binding parameter [3] as [VARCHAR] - [21999815-5ce5-4937-b25e-cadc52826b99]
testEntity2 = TestEntityOnlyId(id=21999815-5ce5-4937-b25e-cadc52826b99, name=테스트2, age=null)
Hibernate:
select
testentity0_.id as id1_6_0_,
testentity0_.age as age2_6_0_,
testentity0_.name as name3_6_0_
from
test_entity_only_id testentity0_
where
testentity0_.id=?
binding parameter [1] as [VARCHAR] - [834a3d64-7096-47b6-9652-fc86c8fdd8f5]
extracted value ([age2_6_0_] : [INTEGER]) - [null]
extracted value ([name3_6_0_] : [VARCHAR]) - [테스트2]
findEntity=TestEntityOnlyId(id=834a3d64-7096-47b6-9652-fc86c8fdd8f5, name=테스트2, age=null)
insert SQL
에서 age = 10 이었습니다…
그런데 merge()
를 통해서 생성된 update SQL
을 확인하면 age = null
인것을 확인할 수 있습니다.
또한 조회한 결과 age = null
인것을 확인할 수 있습니다.merge()
로 발생하는 update SQL
은 전체 필드를 갱신하는 것을 확인할 수 있습니다.!
이렇게 의도치 않게 필드 전체를 갱신하면 데이터 정합성에 문제가 생길수 있습니다.
따라서 저장은 persist()
를 호출하도록 하고,
갱신은 변경 감지를 사용할 수 있도록 코드를 작성해야 합니다.
우리 회사는 @GeneratedValue를 사용 못해요….
ㅎㅎ고객님 난감하셨죠?🤗
이럴때는 기존 엔티티가 Persistable
인터페이스를 상속받아 isNew()
를 직접 구현하여 사용하면 됩니다. 🧑💻
@Entity
@Setter @Getter
@ToString
@EntityListeners(AuditingEntityListener.class)
public class TestEntityOnlyId implements Persistable<String> {
@Id
private String id;
private String name;
private Integer age;
@CreatedDate
private LocalDateTime createDate;
//id가 null이 아니고, createDate가 null이면 true
@Override
public boolean isNew() {
return id != null && createDate == null;
}
}
Id
필드만으로는 isNew()
를 올바르게 구현하는데 판단하기 어려울것 같아Auditing
기술을 사용하여 구현했습니다.
(@EntityListeners(AuditingEntityListener.class)
, @CreatedDate
를 추가했습니다.)
@SpringBootTest
@EnableJpaAuditing
public class SaveTestOnlyId {
@Autowired
TestEntityOnlyIdRepository repository;
@Test
@Rollback(value = false)
void saveTestAddPersistable() {
String id = UUID.randomUUID().toString();
TestEntityOnlyId entity = new TestEntityOnlyId();
entity.setId(id);
entity.setName("반가워!");
entity.setAge(10);
repository.save(entity);
}
}
Hibernate:
insert
into
test_entity_only_id
(age, create_date, name, id)
values
(?, ?, ?, ?)
binding parameter [1] as [INTEGER] - [10]
binding parameter [2] as [TIMESTAMP] - [2023-02-22T23:22:43.752286]
binding parameter [3] as [VARCHAR] - [반가워!]
binding parameter [4] as [VARCHAR] - [abaaf71f-0552-49ac-a070-2f7d0033d09c]
persist()
가 실행되어 insert SQL
만 생성된 것을 확인할수 있습니다. 👍
정리
- 동일한 트랜잭션 안에서만
EntityManager
가 유효합니다.- 동일한 트랜잭션 안에서 영속성이 보장됩니다.
save()
는isNew()
의 반환값에 따라서persist()
또는merge()
를 호출합니다.Id
의 타입이Object
타입이고null
->true
Id
의 타입이Primitive
타입이고0
->true
Persistable
인터페이스를 구현한 경우 ->true
merge(entity)
는 새로운 객체(newEntity
)를 반환하고 새로운 객체(newEntity
)가 영속상태입니다.merge()
는 변경 감지와 다르게 전체 필드를 갱신하는update SQL
이 발생합니다.- 저장은
persist()
, 갱신은 변경 감지를 사용합시다. @GeneratedValue
를 사용할 수 없을때는Persistable
을 상속받아isNew()
를 직접 구현합니다.
참고 https://jaime-note.tistory.com/65
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.(값 타입, 엔티티 타입) (0) | 2023.02.27 |
---|---|
[JPA] CASCADE(영속성 전이)와 고아객체 (0) | 2023.02.24 |
[JPA] 하이버네이트 프록시와 지연로딩(Lazy), 즉시로딩(Eager) (0) | 2023.02.22 |
[JPA] @MappedSuperclass(공통 매핑 정보 해결 + 스프링 적용) (0) | 2023.02.20 |
[JPA] 상속관계 매핑 (0) | 2023.02.20 |