헤르메스 LIFE

[JPA] JPA Data Save, Delete 시 Select 쿼리가 먼저 실행 된다. 본문

Spring Boot Framework

[JPA] JPA Data Save, Delete 시 Select 쿼리가 먼저 실행 된다.

헤르메스의날개 2023. 3. 12. 21:10
728x90

요즘 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

 

JPA에서 save할때 select 쿼리가 먼저 실행되는 이유

스프링 데이터 JPA의 JpaRepository로 save를 해보다가 이상한 점을 발견했다. MEMBER 테이블에 회원 객체를 저장하는 테스트 코드를 작성했다. @Test @DisplayName("회원 객체 등록 테스트") void insertMemberTest()

programmer-chocho.tistory.com

https://taesan94.tistory.com/266

 

Spring Data Jpa Insert 할 때 Select가 나가네..

문제 상황 설계한 Entity의 id가 Auto Increament값이 아니다. 생성자가 호출되는 시점에 fk의 조합으로 생성된다. makeReservedSeatId 함수에서 만들어진다. @Entity @Table(name = "reserved_seat") public class ReservedSeat

taesan94.tistory.com

https://hermeslog.tistory.com/705

 

[Error] The return type is incompatible with Persistable<String>.getId()

아래와 같은 오류가 발생했습니다. The return type is incompatible with Persistable.getId() 내용은 getId() 의 ID 의 Return Type이 String 이기 때문에 발생하는 오류입니다. ( ID의 Type은 Long 형입니다. ) Persistable 는 S

hermeslog.tistory.com

 

728x90