1. Issue Description
ํ์ฌ LicenseCategory ๋ licenseType
๊ณผ analyzeType
์ CombinedKey ๋ก ์ฌ์ฉํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ๋ ๊ฐ์ ํค๋ LicenseCategoryId ๋ผ๋ IdClass ๋ก ์ ์ธ๋์ด ์์ด์.
๊ทธ๋์ ๊ฐ์ licenseType
๊ณผ analyzeType
์ ๊ฐ์ง๊ฒ ๋๋ค๋ฉด, ์ค๋ณตํค ์๋ฌ๋ฅผ ํ์ํด์ผ๋งํฉ๋๋ค.
ํ์ง๋ง ์๋ฌ๊ฐ ํ์๋์ง ์๊ณ ๊ทธ๋๋ก ์งํ๋๋ฒ๋ ธ์ด์!
1.1 Screenshots
1.2 Error Code
- ํ ์คํธ ์ฝ๋
@Test
@DisplayName("๋ผ์ด์ผ์ค ์นดํ
๊ณ ๋ฆฌ ์ค๋ณต ์ ์ฅ ๋ฐฉ์ง")
void function2() {
// given
LocalDateTime now = LocalDateTime.now();
LicenseCategoryId lcId = LicenseCategoryId.builder().licenseType("basic").analyzeType("์
์ฑ์ฝ๋").build();
LicenseCategoryId lcIdDuplicate = LicenseCategoryId.builder().licenseType("basic").analyzeType("์
์ฑ์ฝ๋").build();
LicenseCategory lc = LicenseCategory.builder().licenseType(lcId.getLicenseType()).analyzeType(lcId.getAnalyzeType()).createdAt(now).build();
LicenseCategory lcDuplicate = LicenseCategory.builder().licenseType(lcIdDuplicate.getLicenseType()).analyzeType(lcIdDuplicate.getAnalyzeType()).createdAt(now).build();
// when
licenseCategoryRepository.save(lc);
licenseCategoryRepository.save(lcDuplicate);
// then
assertThatThrownBy(() -> {
licenseCategoryRepository.save(lcDuplicate);
}).isInstanceOf(DuplicateKeyException.class);
}
- ํ ์คํธ ๊ฒฐ๊ณผ ์ค๋ฅ
Expecting code to raise a throwable.
java.lang.AssertionError:
Expecting code to raise a throwable.
at foxee.product.mainservice.domain.repository.LicenseCategoryRepositoryTest.function2(LicenseCategoryRepositoryTest.java:63)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
...
2. Problem
์ ๊ฐ ์์ํ๋ ํ๋ก์ฐ๋ JPA ์ ์ฒซ save() ํธ์ถ ์ insert ๋๋ฉฐ, ๋ ๋ฒ์งธ ๊ฐ์ ID ๋ก ๋ค๋ฅธ ๊ฐ์ฒด๋ฅผ ์ฝ์ ์ ๋ง์ฐฌ๊ฐ์ง๋ก insert ๋๋ ํ๋ก์ฐ์์.
ํ์ง๋ง JPA save() ์ค๋ณต ํธ์ถ ์ ๊ธฐ์กด ์ํฐํฐ๋ฅผ ์ ๋ฐ์ดํธํ๊ฒ ๋ฉ๋๋ค.
save() ์ db์ ๊ฐ์ id ๊ฐ ์์ผ๋ฉด ๊ทธ๋๋ก ์ํฐํฐ๋ฅผ ๋ค๊ณ ์ค๊ณ , ์๋ค๋ฉด insert ํด์ฃผ๊ณ ์์์ด์.
์ ๊ทธ๋ด๊น์? JPA ์ save() ๋ฉ์๋ ๋์๊ณผ์ ์ ๋ณด๋ฉด ์ ์ ์์ด์!
3. JPA ์์ save() ๋์ ์์
3.1 save() ๋์ ์ฝ๋ ํ์ธ
save()
๋merge()
์persist()
๋ ์ค ํ๋๋ก ๋์ํ๊ฒ ๋ฉ๋๋ค.
JPA save() ๋ด๋ถ ์ฝ๋
@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);
}
}
- ์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์์๊ฒ ์ง๋ง, isNew() ์ ๋ฐํ์กฐ๊ฑด์ ๋ฐ๋ผ์,
persist()
์merge()
๋ก ๋ถ๊ธฐ๊ฐ ๋๋๋๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.persist : ์ฆ์ DB ์ insert ์ฟผ๋ฆฌ๋ฅผ ์ ์กํฉ๋๋ค.
merge : detached ๋์ด์๋ ์ํฐํฐ๋ฅผ manage ํ ์ด๋ธ(1์ฐจ ์บ์)๋ก ๊ฐ์ ธ์ค๋๋ฐ, ๋ง์ฝ detached ์ ์์ ๊ฒฝ์ฐ์๋ DB์ select ์ฟผ๋ฆฌ๊ฐ ์ถ๊ฐ์ ์ผ๋ก ๋๊ฐ๊ฒ ๋ฉ๋๋ค.
- ๊ทธ๋ ๋ค๋ฉด isNew() ๋ ๋ฌด์์ผ๊น์?
isNew() ๋ ํด๋น Entity๊ฐ ์๋กญ๊ฒ ๋ง๋ค์ด์ง Entity์ธ์ง, ํน์ ๊ธฐ์กด์ ์ฌ์ฉ๋๋ Entity ์ธ์ง๋ฅผ ๊ตฌ๋ถํฉ๋๋ค.
- ๊ทธ๋ ๋ค๋ฉด ์ด๋ค์์ผ๋ก Entity๊ฐ ์๋ก์ด Entity์ธ์ง ์๋์ง๋ฅผ ๊ตฌ๋ถํ ๊น์? ์ฝ๋๋ก ํ์ธํด๋ณด๊ฒ ์ต๋๋ค.
public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
Primitive vs Wrapper ์ฐธ๊ณ
primitive ์๋ฃํ์ด๋? : int, float, long, double, boolean ๊ณผ ๊ฐ์ ์์์ ์๋ฃํ
wrapper ์๋ฃํ์ด๋? : Integer, Float, Long, Double, Boolean, UUID, ์ฌ์ฉ์ ์ ์ ํด๋์ค
getId
๋ ์ํฐํฐ๋ฅผ ์ ์ํ ๋ ์ฌ์ฉํ๋@Id
ํ๋๋ฅผ ๊ฐ์ ธ์ต๋๋ค.ํ์ฌ ์ฐ๋ฆฌ ํ๋ก์ ํธ๋ LicenseCategoryId ๋ฅผ PK ๋ก ์ฌ์ฉํ๊ธฐ ์ํด
@id
์ด๋ ธํ ์ด์ ์ ๋ถ์์ต๋๋ค. ๊ทธ๋ ๋ค๋ฉดidType
์ LicenseCategoryId ๊ฐ ๋๊ฒ ์ฃ ?- isPrimitive ๋ int, float, long, double ์ ๊ฐ์ ํ๋์ primitive ์๋ฃํ์ธ์ง ํ๋จํ๋ ๋ด์ฅํจ์์
๋๋ค.
ํ์ฌ ์ฐ๋ฆฌ๋ LicenseCategoryId ๋ฅผ PK ๋ก ์ฌ์ฉํ๊ณ ์๊ธฐ๋๋ฌธ์ primitive ์๋ฃํ์ด ์๋ wrapper ์๋ฃํ์ ๋๋ค. ๊ทธ๋ ๋ค๋ฉด id==null ์ธ์ง ํ์ธํ๊ฒ ๋๊ฒ ์ฃ ? ์ด ๋, ์ด๋ฏธ id ๊ฐ์ ์ ํด์ ์ฝ์ ํ๊ธฐ๋๋ฌธ์ false ๋ฅผ ๋ฐํํ๊ฒ ๋ฉ๋๋ค.
์! ๊ทธ๋ผ ๋ค์ ์ฌ๋ผ๊ฐ์ isNew๊ฐ false ๋ผ๋ฉด ์ด๋ค ๊ณผ์ ์ ๊ฐ์ง๊ฒ ๋ ๊น์?
- JPA save() ๋ด๋ถ ์ฝ๋-๋ค์
public <S extends T> S save(S entity) { if (entityInformation.isNew(entity)) { ... } else { return em.merge(entity); } }
- ๋ค. ๋ฐ๋ก merge ๋ฅผ ์ํํ๊ฒ ๋ฉ๋๋ค. ์ฆ, detached ๋์ด์๋ ์ํฐํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฒ ๋๊ฒ ์ฃ !
์ฆ, save ํธ์ถ์ ์๋ก์ด ์ํฐํฐ์์๋ ๋ถ๊ตฌํ๊ณ UUID ์๋ฃํ์ผ๋ก PK ๋ฅผ ์ค์ ํ๊ธฐ์ ํญ์ persist ๊ฐ ์๋ merge๋ฅผ ํธ์ถํ๊ฒ ๋ฉ๋๋ค
- ์ฌ๊ธฐ์ ์๋ฌธ์ด ์๊ธฐ์ฃ . detached ์๋ ์ด๋ค์ ๋ค์ด ์กด์ฌํ ๊น?๋ผ๋ ์๋ฌธ๋ง์ ๋๋ค.
An entity becomes detached (unmanaged) on following actions:
- after transaction commit/rollback
- by calling EntityManager.detach(entity)
- by clearing the persistence context with EntityManager.clear()
- by closing an entity manager with EntityManager.close()
- serializing or sending an entity remotely (pass by value).
reference : https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/detaching.html
์์ ์๋ฃ์์ ๋์ฌ๊ฒจ ๋ณผ ๊ฒ์ after transaction commit/rollback ์ ๋๋ค. detach ์์ญ์๋ Transaction ๋ด ์ฟผ๋ฆฌ๊ฐ ๋๋๋ฉด ์ฌ์ฉ๋ ์ํฐํฐ๋ค์ detached ์ ๋ณด๊ดํ๋ค๋ ๋ง์ด์ฃ ! ๊ทธ๋ฆฌ๊ณ ์ด detached ๋ ์ค์์์ํ ๋ผ๊ณ ํฉ๋๋ค.
- ์ข ํฉํ๊ธฐ ์ด์ ์ JPA manage ์ํ์ ๋ํด์ ์ ๊น ์ค๋ช ํด๋ณผ๊นํด์.
์ด์ ์์์ merge ๋ detached ์ํ์์ manage ์ํ๋ก ๋ณ๊ฒฝํ๋ ์ญํ ์ ์ํํ๋ค๊ณ ๋ง์๋๋ ธ์ต๋๋ค. ์กฐ๊ธ ๋ ์์ธํ ๋งํ๋ฉด, id ๊ฐ์ ๊ฐ์ง๊ณ ์์ ์ปจํ ์คํธ์ 1์ฐจ ์บ์ ๋ด id ๊ฐ์ด ์ผ์นํ๋ ์ํฐํฐ๊ฐ ์กด์ฌํ๋ค๋ฉด ์ด๋ฅผ ๊ทธ๋๋ก ์ ๋ฐ์ดํธํฉ๋๋ค. ๋ฐ๋ฉด 1์ฐจ ์บ์ ๋ด ์๋ค๋ฉด, DB์ select ์ฟผ๋ฆฌํ๊ณ ๊ฒฐ๊ณผ๋ฅผ 1์ฐจ ์บ์์ ์ง์ด๋ฃ์ต๋๋ค.
์ด๋ ๊ฒ id ๊ฐ์ ๊ฐ์ง๊ณ ์์ ์ปจํ ์คํธ ๋ด 1์ฐจ ์บ์์ ์ํฐํฐ๋ฅผ ๋ฃ์ด์ฃผ๋ ๊ณผ์ ์ด ๋ฐ๋ก ์์ํ ๊ณผ์ ์ด๋ฉฐ, 1์ฐจ ์บ์์ ์ฝ์ ๋ ์ํ๋ฅผ manage ์ํ๋ผ๊ณ ๋ถ๋ฆ ๋๋ค.
3.2 ํธ๋์ ์ ์ ๋ฐ๋ฅธ ์ค์ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ํ ์คํธ
๋์ผ ํธ๋์ ์ ๋ด ๋์ผ Id ๋ฅผ ๊ฐ์ง๋ ์ํฐํฐ ์ ์ฅ
- ์ฝ๋
transactionTemplate.execute((status)->{
System.out.println("์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์์");
licenseCategoryRepository.save(lc);
System.out.println("์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ");
System.out.println("๋ ๋ฒ์งธ LC ์ ์ฅ ์์");
licenseCategoryRepository.save(lcDuplicate);
System.out.println("๋ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ");
return null;
});
- ๊ฒฐ๊ณผ
์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์์
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
// ์ด ์ํฐํฐ๋ 1์ฐจ ์บ์์ ์ฝ์
๋ฉ๋๋ค
์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ
๋ ๋ฒ์งธ LC ์ ์ฅ ์์
// ๊ทธ๋ฆฌ๊ณ detached ์์ญ๊ณผ 1์ฐจ ์บ์ ์์ญ์ ํ์ธ ํ๊ณ , 1์ฐจ ์บ์์ ๋์ผ ID๊ฐ ์กด์ฌํ๋ ๊ฒ์ ํ์ธํ์์ผ๋ ์คํตํฉ๋๋ค
๋ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ
Hibernate: insert into license_category (created_at,analyze_type,license_type) values (?,?,?)
๋ค๋ฅธ ํธ๋์ ์ ๋ด ๋์ผ Id ๋ฅผ ๊ฐ์ง๋ ์ํฐํฐ ์ ์ฅ
- ์ฝ๋
transactionTemplate.execute((status)->{
System.out.println("์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์์");
licenseCategoryRepository.save(lc);
System.out.println("์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ");
return null;
});
transactionTemplate.execute((status)->{
System.out.println("๋ ๋ฒ์งธ LC ์ ์ฅ ์์");
licenseCategoryRepository.save(lcDuplicate);
System.out.println("๋ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ");
return null;
});
- ๊ฒฐ๊ณผ
์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์์
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
// ์ด ์ํฐํฐ๋ 1์ฐจ ์บ์์ ์ฝ์
๋ฉ๋๋ค
์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ
Hibernate: insert into license_category (created_at,analyze_type,license_type) values (?,?,?)
// ํธ๋์ ์
์ด ๋๋๊ณ flush์ commit ์ด ์คํ๋ฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ์ํฐํฐ๋ 1์ฐจ ์บ์์์ ์ ๊ฑฐ๋ฉ๋๋ค
๋ ๋ฒ์งธ LC ์ ์ฅ ์์
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
// ์ค์์์ ์กด์ฌํ๋ ์ ๊ฐ 1์ฐจ ์บ์์ ์๋์ง ๋จผ์ ํ์ธํ๊ณ ์์ผ๋ฉด ๋ค์ select ์ฟผ๋ฆฌ๋ฅผ ์ ์กํฉ๋๋ค
๋ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ
์ด select ์ฟผ๋ฆฌ์ ๋ป์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. db ์ ์ ์ฅ๋ row ๋ฅผ ๊ฐ์ ธ์์ ๋ณ๊ฒฝ์ ์ด ์๋์ง ์๋์ง ํ์ธํ๊ณ ์๋ค๋ฉด no ์ฟผ๋ฆฌ, ์๋ค๋ฉด update ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆด๊ฒ์ด๋ค!
์๋๋ ๋์ผ ID ์ ๋ค๋ฅธ createdAt๊ฐ์ ๋ฃ์์ ๋ ์ฟผ๋ฆฌ๋๋ ๊ตฌ๋ฌธ๋ค์ ํ์ธํ ์ ์์ด์.
@Test
@DisplayName("๋ผ์ด์ผ์ค ์นดํ
๊ณ ๋ฆฌ ์ค๋ณต ์ ์ฅ ๋ฐฉ์ง - JPA Seperated Transaction")
void function2() {
// given
LocalDateTime now = LocalDateTime.now();
LocalDateTime now2 = LocalDateTime.now().plusDays(5);
LicenseCategoryId lcId = LicenseCategoryId.builder().licenseType("basic0").analyzeType("์
์ฑ์ฝ๋").build();
LicenseCategoryId lcIdDuplicate = LicenseCategoryId.builder().licenseType("basic0").analyzeType("์
์ฑ์ฝ๋").build();
LicenseCategory lc = LicenseCategory.builder().licenseType(lcId.getLicenseType()).analyzeType(lcId.getAnalyzeType()).createdAt(now).build();
LicenseCategory lcDuplicate = LicenseCategory.builder().licenseType(lcIdDuplicate.getLicenseType()).analyzeType(lcIdDuplicate.getAnalyzeType()).createdAt(now2).build();
// when
transactionTemplate.execute((status)->{
System.out.println("์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์์");
licenseCategoryRepository.save(lc);
System.out.println("์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ");
return null;
});
transactionTemplate.execute((status)->{
System.out.println("๋ ๋ฒ์งธ LC ์ ์ฅ ์์");
licenseCategoryRepository.save(lcDuplicate);
System.out.println("๋ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ");
return null;
});
}
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0
์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์์
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
์ฒซ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ
Hibernate: insert into license_category (created_at,analyze_type,license_type) values (?,?,?)
๋ ๋ฒ์งธ LC ์ ์ฅ ์์
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
๋ ๋ฒ์งธ LC ์ ์ฅ ์๋ฃ
Hibernate: update license_category set created_at=? where analyze_type=? and license_type=?
4. ์์ ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ์ข ํฉํด๋ณด๊ฒ ์ต๋๋ค
- ์ฒซ ๋ฒ์งธ save()๋ ์ํฐํฐ์
@id
๊ฐ UUID ๋ก wrapper ์๋ฃํ์ด๊ณ id๊ฐ์ ๊ฐ์ด ์ ๋ ฅํด์ฃผ์๊ธฐ๋๋ฌธ์ ์กด์ฌํ๊ธฐ ๋๋ฌธ์em.merge(entity)
๊ฐ ์คํ๋ฉ๋๋ค. - ํ์ง๋ง detached ์ ์กด์ฌํ์ง ์๊ณ , 1์ฐจ ์บ์๋ํ ์กด์ฌํ์ง ์๊ธฐ ๋๋ฌธ์ db ์
SELECT
์ฟผ๋ฆฌ๋ฅผ ์ ์กํ์ฌ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.em.persist() ์ ๊ฒฝ์ฐ 1์ฐจ ์บ์์ ์กด์ฌํ๋ entity๋ก ์ธ์งํ๊ณ , ์ผ๋ฐ์ ์ผ๋ก ๋ฐ๋ก INSERT ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๊ฒ ๋ฉ๋๋ค.
- ๋์ผ ID ๋ฅผ ๊ฐ์ง๋ ์ํฐํฐ๊ฐ db์ ์กด์ฌํ๋ค๋ฉด, UPDATE ๋ฌธ์ ๋ฐฐ์นํ๊ณ ์ต์ ์ํฐํฐ๋ฅผ 1์ฐจ ์บ์์ ์ฝ์
๋ฐ ํฉ๋๋ค.
๋์ผ ID ๊ฐ db์ ์๋ค๋ฉด INSERT ๋ฌธ์ ๋ฐฐ์นํฉ๋๋ค
- ํธ๋์ ์ ์ด ๋๋ ๋ค ๋ฐฐ์น๋ SQL ๋ฌธ์ flush, commit, detach ํฉ๋๋ค.
- ๋ ๋ฒ์งธ save() ๋ํ ๋ง์ฐฌ๊ฐ์ง๋ก em.merge() ๊ฐ ์ํ๋ฉ๋๋ค.
- ๊ทธ๋ฆฌ๊ณ detached ์์ญ์ ์กด์ฌํ๋ ๋์ผ ID ์ํฐํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
- ๊ทธ๋ฆฌ๊ณ 1์ฐจ ์บ์์ ์กด์ฌํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ค์ SELECT ์ฟผ๋ฆฌ๋ฅผ ์ ์กํฉ๋๋ค.
- ์ด ID ๋ db์ ์กด์ฌํ๋ ID ์ด๊ธฐ ๋๋ฌธ์, UPDATE ์ฟผ๋ฆฌ๋ฅผ ์ ์กํฉ๋๋ค.
5. ๊ฒฐ๋ก ์?
- save() ๋
isNew()
๋ฅผ ํตํด์ ์๋ก์ด ๊ฐ์ฒด์ธ ๊ฒฝ์ฐ persist(), ์๋๋ฉด merge()๋ก ์๋ํฉ๋๋ค. - isNew() ๋ primitive + Number type ์ด๋ฉด 0, IdClass์ ๊ฐ์ด un-primitive ์ธ ๊ฒฝ์ฐ์๋ null ์ธ์ง๋ฅผ ํ์ธํจ์ผ๋ก์จ ์๋ก์ด ๊ฐ์ฒด์ธ์ง๋ฅผ ํ์ธํฉ๋๋ค.
- LicenseCategory๋ un-primitive ์ธ IdClass ๋ฅผ ์ฌ์ฉํ๊ณ ๊ฐ์ด ์กด์ฌํฉ๋๋ค.
- ๊ทธ๋ฌ๋ฏ๋ก ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์์ ์ด๋ฅผ ๋ณ๊ฒฝํ๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๊ฒฐ๋ก ์ Persistable ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ์ฌ isNew() ํ๋จ ์กฐ๊ฑด์ ๋ณ๊ฒฝํ๋ฉด ๋ฉ๋๋ค!
- isNew()๋ฅผ ํตํด Entity๊ฐ ์๋กญ๊ฒ ๋ง๋ค์ด์ง entity๋ก ์ธ์ง, ํน์ ๊ธฐ์กด์ ์ฌ์ฉ๋๋ Entity ์ธ์ง๋ฅผ ๊ตฌ๋ถํฉ๋๋ค. persist() ์ ๊ฒฝ์ฐ ๊ธฐ์กด์ ์กด์ฌํ๋ entity๋ก ์ธ์งํ๊ณ , ์ผ๋ฐ์ ์ผ๋ก ๋ฐ๋ก insert ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๊ฒ ๋ฉ๋๋ค. ํ์ง๋ง merge() ์ ๊ฒฝ์ฐ, ๋ฐ์ด ๋ฃ์ผ๋ ค๋ ๊ฐ์ id๊ฐ ํ ์ด๋ธ์ ์๋์ง๋ฅผ ์๋์ง๋ฅผ ํ์ธํด๋ณด๊ธฐ ์ํด์ select ์ฟผ๋ฆฌ๊ฐ ์ถ๊ฐ์ ์ผ๋ก 1ํ ๋๊ฐ ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค. (1์ฐจ ์บ์์ ์๋ ๊ฒฝ์ฐ) ์ด ๋๋ฌธ์ merge ์ฌ์ฉ์ saveAll() ๊ณผ ๊ฐ์ด N๊ฐ์ ์ํฐํฐ๋ฅผ save ํ๊ฒ ๋๋๊ฒฝ์ฐ, ๋ถํ์ํ ์ฟผ๋ฆฌ(select) N๋ฒ์ด ์ถ๊ฐ์ ์ผ๋ก ๋ฐ์ํ๊ฒ ๋์ด ์ฑ๋ฅ์ ์ด์๊ฐ ๋ ์ ์์ต๋๋ค. ๋ํ merge๋ pk๊ฐ ๊ฐ์ ๊ธฐ์กด์ entity๋ฅผ ๋์ฒดํด๋ฒ๋ฆฌ๊ธฐ ๋๋ฌธ์, entity๋ด์ ํ๋๊ฐ๋ค์ด ์๋์น ์๊ฒ ์ฌ๋ผ์ง๊ฑฐ๋ ๋ณ๊ฒฝ๋๋ ์ฌ์ด๋ ์ดํํธ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
reference : ID UUID ์ ์ฅ์ ๊ณ ๋ ค