JPA 에서 entity를 삭제하는 여러가지 방법과 차이
JPA에서 entity를 삭제하는 방법은 여러가지가 존재합니다.
각각의 삭제하는 방법의 내부코드와 사용방법, 차이점을 알아보도록 하겠습니다.
1. delete
delete method를 사용하여 entity를 삭제할 수 있습니다.
1.1 how to use?
repository.delete(entity);
entity를 전달하므로써 해당 entity를 삭제하게 됩니다.
1.2 delete 구현 코드 참고
@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null!");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = (T) em.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOP
if (existing == null) {
return;
}
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
delete method는 SimpleJpaRepository class에 존재합니다.
null체크와 새로 생성한 entity인지 확인 후에 entity manager를 통해 remove를 진행합니다.
그냥 간단히 이렇게 구현되어있구나를 보고 deleteById method를 보도록 하겠습니다.
2. deleteById
deleteById method를 사용하여 entity의 id를 넘겨 삭제할 수 있습니다.
2.1 how to use?
repository.deleteById(type id);
entity의 id 타입에 맞는 값을 넘겨 삭제를 진행하게 됩니다.
2.2 deleteById 구현 코드
@Transactional
@Override
public void deleteById(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));
}
deleteById에서 delete method를 호출하여 삭제하는 것을 확인하실 수 있습니다.
둘의 차이는 위의 코드에서 알수 있듯이 findById를 사용하여 조회 후 삭제를 할 것 인지 아닌지의 차이입니다.
물론 delete의 경우 entity를 삭제하기 위해선 entity를 조회하고 있을 경우 삭제한다의 코드를 작성하지만, deleteById를 사용하면 entity를 조회할 필요가 없어지죠.
3. delete or deleteById 의 예시 코드
예시 코드를 보겠습니다.
delete 삭제의 경우
Optional<UserEntity> entity = findById(String userId);
if(entity.isPresent()){
userRepository.delete(entity);
}
or
userRepository.delete(userRepository.findById(String userId).orElseThrow(() -> new EntityNotFoundException()));
deleteById의 경우
repository.deleteById(String userId);
4. delete 전송 쿼리
위의 delete, deleteById method를 실행했을 때 DB로 전송하는 쿼리는 아래와 같습니다.
delete USER_TABLE from where USER_ID = userId
JPA를 사용하지 않았을 경우와 별반 차이가 없죠.
그럼 복수의 데이터를 삭제할 때는 어떻게 될까요?
5.복수의 데이터 삭제
일반적으로 복수의 데이터를 삭제할 때는 간단하게 @Transactional을 붙이고 반복문을 돌리면 됩니다.
@Transactional
public void delUser(List<String> list){
list.forEach(userId -> {
userRepository.deleteById(userId);
});
}
이 코드를 실행 할 경우 list.size() 개수 만큼의 delete 쿼리가 발생하게 됩니다.
delete USER_TABLE from where USER_ID = userId1
delete USER_TABLE from where USER_ID = userId2
delete USER_TABLE from where USER_ID = userId3
delete USER_TABLE from where USER_ID = userId4
delete USER_TABLE from where USER_ID = userId5
...
...
...
그리고 위에서 본 것처럼 deleteById의 경우 select(findById), delete가 발생하게됩니다.
관계성 설정이 많거나, 데이터의 개수가 많아질수록 성능상의 문제가 발생하게될 수도 있겠죠.
음 그럼 한번에 삭제할 수 있는 방법이 없을까 라는 의문이 듭니다.
6. deleteAllById
SimpleJpaRepository class를 보면 deleteAllById method가 있습니다. 뭔가 한번에 삭제해 줄 것 같은 느낌이 들죠.
6.1 how to use?
repository.deleteAllById(List<String> list);
6.2 deleteAllById 구현 코드 참고
@Override
@Transactional
public void deleteAllById(Iterable<? extends ID> ids) {
Assert.notNull(ids, "Ids must not be null!");
for (ID id : ids) {
deleteById(id);
}
}
여전히 반복문을 돌며 삭제를 하고 있습니다.
select, delete, select delete.....가 개수만큼 증가됩니다.
만일 관계설정이 된 경우라면 관계 테이블의 조회 개수만큼도 반복이 되겠죠. cascade가 설정된 경우라면 말이죠.
좀 더 좋은 method가 있는지 찾아봅시다.
7. deleteAllInBatch
deleteAllInBatch method가 있습니다. 뭔가 이름만 보면 원하는 동작을 할 것 같습니다.
구현 코드를 보도록 하죠.
7.1 deleteAllInBatch 구현 코드 참고
@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {
Assert.notNull(entities, "Entities must not be null!");
if (!entities.iterator().hasNext()) {
return;
}
applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
.executeUpdate();
}
method의 파라메터를 보니 entities를 전달 받고 있네요, 그렇다는 것은 entity들을 한번 가져와야 한다는 말이 됩니다.
그냥 delete만 한번 보내고 싶은데 말이죠.
어떤 쿼리를 만드나 찾아보겠습니다.
public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
DELETE_ALL_QUERY_STRING을 보다가 아래 원하는 쿼리를 찾았습니다.
public static final String DELETE_ALL_QUERY_BY_ID_STRING = "delete from %s x where %s in :ids";
해당 쿼리를 사용하는데를 찾아보니 deleteAllByIdInBatch 메소드가 있네요.
8.deleteAllByIdInBatch
QueryString을 사용하여 in문으로 id를 받아 삭제 처리를 하는 메소드 입니다.
8.1 deleteAllByIdInBatch 구현 코드 참고
@Override
@Transactional
public void deleteAllByIdInBatch(Iterable<ID> ids) {
Assert.notNull(ids, "Ids must not be null!");
if (!ids.iterator().hasNext()) {
return;
}
if (entityInformation.hasCompositeId()) {
List<T> entities = new ArrayList<>();
// generate entity (proxies) without accessing the database.
ids.forEach(id -> entities.add(getReferenceById(id)));
deleteAllInBatch(entities);
} else {
String queryString = String.format(DELETE_ALL_QUERY_BY_ID_STRING, entityInformation.getEntityName(),
entityInformation.getIdAttribute().getName());
Query query = em.createQuery(queryString);
/**
* Some JPA providers require {@code ids} to be a {@link Collection} so we must convert if it's not already.
*/
if (Collection.class.isInstance(ids)) {
query.setParameter("ids", ids);
} else {
Collection<ID> idsCollection = StreamSupport.stream(ids.spliterator(), false)
.collect(Collectors.toCollection(ArrayList::new));
query.setParameter("ids", idsCollection);
}
query.executeUpdate();
}
}
@EmbededId를 갖냐 아니냐에 따라 다르게 동작하도록 구현되어있는데요. 복합키의 경우 deleteAllInBatch를 사용하는 것을 보실 수 있습니다.
복합키의 경우 entity manager를 사용하여 entity를 삭제하고, 단일키의 경우 SimpleJpaRepository 내부의 IN문을 사용하여 한번의 쿼리만 실행되게 되어있습니다.
드디어 찾았네요!
deleteAllByIdInBatch 메소드를 사용하면 단일키의 경우 원하는데로 한번에 쿼리 호출로 삭제를 할 수 있습니다.
JPA를 좀 다뤄보신 분이라면 여기서 한가지 의문이 드실 수 도 있습니다.
'그냥 JPQL 쓰면 되는거 아닌가?'
맞습니다. JPQL을 사용해서 같은 동작을 하게 구현할 수 있습니다.
9.JPQL
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = "delete from user where userId in :ids")
void deleteAllByUserId(@Param("ids") List<String> list);
영속성 컨텍스트와 DB 싱크를 위해 clearAutomatically=true, flushAutomatically=true 두 옵션을 추가해주었습니다.
10.마무리
JPA의 delete 기능을 하는 method들과 방법에 대해 알아보았습니다.
기능에 맞게 원하는 method, 방법으로 구현을 하시면 좋을 것 같습니다.
그리고 항상 성능에 의문을 갖고 더 좋은 방법을 찾기위해 노력했으면 좋겠습니다.
감삼당~~