일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Spring Boot
- STS
- Docker
- MySQL
- myBatis
- IntelliJ
- Open Source
- AJAX
- SpringBoot
- 설정
- Python
- JavaScript
- Thymeleaf
- 문서
- MSSQL
- Eclipse
- 오픈소스
- PostgreSQL
- Source
- jpa
- git
- oracle
- ubuntu
- Exception
- spring
- JDBC
- maven
- Tomcat
- Core Java
- error
- Today
- Total
헤르메스 LIFE
[JPA] JPA Data Save, Delete 시 Select 쿼리가 먼저 실행 된다. 본문
요즘 JPA를 공부하고 있습니다. MyBatis 만 하다가, 개인적으로 공부를 진행하고 있습니다. 하다보니 막히고, 이해가 안되는 부분이 많네요. 그때마다 검색과 삽질로 해결하고 있습니다. 여기 그 삽질의 자취를 남겨봅니다.
개발환경
Spring Boot 2.7.9
H2 2.1.214
p6spy 1.8.1
slf4j 1.7.36
swagger2 2.6.1
lombok
devtools
postgresql
save 시 select 를 수행하는 이유는 아래의 링크를 참조하면 될 것 같습니다.
1. save 를 실행하면 select를 수행됩니다.
2. select 후 있으면 update, 없으면 insert 를 수행 합니다.
기본적으로 있으면 중복 메시지를 보여주는게 맞습니다.
그래서 중복메시지를 보여주도록 수정했습니다.
핵심은
1. Persistable<String> 을 구현해야 합니다.
2.아래의 내용을 구현해야 합니다.
/* save 시 Select 날리는 것을 방지 */
@Transient
private boolean isNew = true;
@Override
public boolean isNew() {
return isNew;
}
@PrePersist
@PostLoad
void markNotNew() {
this.isNew = false;
}
@Override
public String getId() {
return this.pCd;
}
BaseEntity.java
package octopus.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.Transient;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.domain.Persistable;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.Getter;
/**
* <pre>
* 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만을 제공
* abstract 클래스로 생성해야 합니다.
* </pre>
*/
@Getter
@MappedSuperclass // BaseEntity를 상속한 Entity들은 아래의 필드들을 컬럼으로 인식한다.
@EntityListeners(AuditingEntityListener.class) // Audting(자동으로 값 Mapping) 기능 추가
public abstract class BaseEntity implements Serializable, Persistable<String> {
private static final long serialVersionUID = 1L;
/* save 시 Select 날리는 것을 방지 */
@Transient
private boolean isNew = true;
@Override
public boolean isNew() {
return isNew;
}
@PrePersist
@PostLoad
void markNotNew() {
this.isNew = false;
}
/**
* 생성자
*
* @CreatedBy // implements AuditorAware<Long>를 구현한 Class를 생성해야 한다.
*/
// private String crtId;
@Column(updatable = false)
protected String crtId = "admin";
/**
* 생성일자 : Entity가 생성되어 저장될 때 시간이 자동 저장된다.
*/
@CreatedDate
@Column(updatable = false)
private LocalDateTime crtDt;
/**
* 수정자
*
* @LastModifiedBy // implements AuditorAware<Long>를 구현한 Class를 생성해야 한다.
*/
// private String mdfId;
protected String mdfId = "admin";
/**
* 수정일자 : Entity가 생성되어 저장될 때 시간이 자동 저장된다.
*/
@LastModifiedDate
private LocalDateTime mdfDt;
}
TCodeM.java
package octopus.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.Proxy;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Entity // jpa entity임을 알립니다.
@Getter // getter를 자동으로 생성합니다.
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 인자없는 생성자를 자동으로 생성합니다.
@Table(name = "T_CODE_M")
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" }) // Post Entity에서 User와의 관계를 Json으로 변환시 오류 방지를 위한 코드
@Proxy(lazy = false)
public class TCodeM extends BaseEntity {
private static final long serialVersionUID = 1L;
@Override
public String getId() {
return this.pCd;
}
@Builder
public TCodeM(String pCd, String pCdNm, String useYn, String rmk) {
this.pCd = pCd;
this.pCdNm = pCdNm;
this.useYn = useYn;
this.rmk = rmk;
}
/**
* 상위 코드
*/
@Id // pk
@Column(nullable = false, unique = true, length = 50)
private String pCd;
/**
* 상위 코드명
*/
@Column(nullable = false, length = 200)
private String pCdNm;
/**
* 사용여부
*/
@Column(nullable = false, length = 1)
private String useYn = "Y"; // Default 값
/**
* 비고
*/
@Column(length = 1000)
private String rmk;
}
Spring Data JPA를 사용할 때, deleteById, delete 사용 시 Select를 피해가기는 쉽지 않아 보입니다.
Spring Data JPA 2.7.10 기준, SimpleJpaRepository.deleteById 함수
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable)
*/
@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)));
}
내부적으로 delete 로직을 call 하고 있고, 그 안에 findById 를 call 해서 결국은 조회를 하도록 하고 있습니다.
Spring Data JPA 2.7.10 기준, SimpleJpaRepository.delete 함수
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object)
*/
@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 는 이미 Entity 객체를 Parameter 로 넘겨야 하기 때문에, 결국은 id를 이용해서 조회를 할 수 밖에 없을 것 같습니다. 오히려 회피하려는 노력이 괴로울 것 같습니다.
RDB 관점에서 보면, 없는 객체를 삭제하는 로직에 대한 결과는 결국 0 입니다. 삭제된 데이터가 없다는 말이죠.
왜 EmptyResultDataAccessException 같은 Exception 을 발생하는지 의문스럽긴합니다.
https://programmer-chocho.tistory.com/80
https://taesan94.tistory.com/266
https://hermeslog.tistory.com/705
'Spring Boot Framework' 카테고리의 다른 글
[JPA] 정렬 Sort (0) | 2023.03.21 |
---|---|
[JPA] update (Persistence Context) (0) | 2023.03.18 |
JPA @CreatedDate Column Update시 Null 되는 현상 해결방법 (0) | 2023.03.12 |
AuditingEntityListener.class (0) | 2023.03.12 |
[Spring Boot] Application.yml 설정 - SQL Script를 활용한 DB 초기화 (0) | 2023.03.05 |