네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'web'에 해당되는 글 82건


반응형

Maven이란?

자바 프로젝트의 빌드(build)를 자동화해주는 빌드 툴(build tool)이다. 즉, 자바 소스를 compile하고 package해서 deploy하는 일을 자동화해주는 것이다.

 

Maven이 참조하는 설정 파일

1) settings.xml

maven tool 자체에 관련된 설정을 담당한다.

MAVEN_HOME/conf/ 아래에 있다. 

 

2) pom.xml

하나의 자바 프로젝트에 빌드 툴로 maven을 선택했다면, 프로젝트 최상위 디렉토리에 pom.xml 파일이 생성된다.

pom.xml은 POM(Project Objet Model)을 설정하는 부분으로 프로젝트 내 빌드 옵션을 설정하는 부분이다.

꼭 pom.xml이라는 이름을 가진 파일이 아니라 다른 파일로 설정할 수 있지만 권장하진 않는다. (mvn -f ooo.xml test)

 

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

1. 프로젝트 정보

  • <modelVersion> 4.0.0이라고 써있는 것은 maven의 pom.xml의 모델 버전이다. 형식이 4.0.0 버전이라고 이해하면 된다.
  • <groupId> 프로젝트를 생성한 조직 또는 그룹명으로 보통, URL의 역순으로 지정한다.
  • <artifactId> 프로젝트에서 생성되는 기본 아티팩트의 고유 이름이다. maven에 의해 생성되는 일반적인 artifact는 <artifact>-<version>.<extension> (예) demo-0.0.1-SNAPSHOT.jar
  • <version> 어플리케이션의 버전. SNAPSHOT이 붙으면 아직 개발 단계라는 의미이다.
  • <packaging> jar, war, war, pom 등 패키지 유형을 나타낸다.
  • <name> 프로젝트명
  • <description> 프로젝트 설명
  • <url> 프로젝트를 찾을 수 있는 url
  • <properties> pom.xml에서 중복해서 사용되는 설정(상수) 값들을 지정해놓는 부분. 다른 위치에서 ${...}로 표기해서 사용할 수 있다. (java.version에 1.8을 적용하고 다른 위치에서 ${java.version}이라고 쓰면 "1.8"이라고 쓴 것과 같다.

2. 의존성 라이브러리 정보

최소한 groupId, artifactId, version 정보가 필요하다.

스프링 부트의 spring-boot-starter-* 같은 경우 부모 pom.xml에 이미 버전 정보가 있어서 version을 지정할 필요가 없다.

 

3. 빌드 정보

build tool: maven의 핵심인 빌드와 관련된 정보를 설정할 수 있는 곳이다.

maven에는 라이프 사이클이 존재한다. default, clean, site 라이프 사이클로 나누고 세부적으로 phase(페이즈)가 있다.

maven의 모든 기능은 plugin(플러그인)을 기반으로 동작한다. 플러그인에서 실행할 수 있는 각각의 작업을 goal(골)이라 하고 하나의 페이즈는 하나의 골과 연결되며, 하나의 플러그인에는 여러 개의 골이 있을 수 있다.

 

Life-Cycle

  • mvn process-resource: resources:resources의 실행으로 resource 디렉토리에 있는 내용을 target/classes로 복사한다.
  • mvn compile: compiler:compile의 실행으로 src/java 밑의 모든 자바 소스를 컴파일해서 target/classes로 복사
  • mvn process-testResources, mvn test-compile: test/java의 내용을 target/test-classes로 복사 (참고로 test만 mvn test 명령을 내리면 라이프 사이클상 원본 소스로 컴파일된다.)
  • mvn test: surefire:test의 실행으로 target/test-classes에 있는 테스트케이스의 단위 테스트를 진행한다. 결과를 target/surefire-reports에 생성한다.
  • mvn package: target 디렉터리 하위에 jar, war, ear 등 패키지 파일을 생성하고 이름은 <build>의 <finalName>의 값을 사용한다. 지정되지 않았을 때는 <artifactId>-<version>.<extension> 이름으로 생성한다.
  • mvn install: 로컬 저장소로 배포
  • mvn deploy: 원격 저장소로 배포
  • mvn clean: 빌드 과정에서 생긴 target 디렉터리 내용 삭제
  • mvn site: target/site에 문서 사이트 생성
  • mvn site-deploy: 문서 사이트를 서버로 배포

 

위와 같은 진행 순서로 라이프 사이클이 진행된다.

 

<build>에서 설정할 수 있는 값을 확인해보자.

  • <finalName>: 빌드 결과물(ex .jar) 이름 설정
  • <resources>: 리소스(각종 설정 파일)의 위치를 지정할 수 있다. <resource> 없으면 기본으로 src/main/resources
  • <testResources>: 테스트 리소스의 위치를 지정할 수 있다. <testResource> 없으면 기본으로 src/test/resources
  • <Repositories>: 빌드할 때 접근할 저장소의 위치를 지정할 수 있다. 기본적으로 메이븐 중앙 저장소인 http://repo1.maven.org/maven2로 지정되어있다.   
  • <outputDirectory> : 컴파일한 결과물 위치 값 지정, 기본 target/classes
  • <testOutputDirectory> : 테스트 소스를 컴파일한 결과물 위치 값 지정, 기본 target/test-classes
  • <plugin> : 어떠한 액션 하나를 담당하는 것으로 가장 중요하지만 들어가는 옵션은 제 각각이다. 다행인 것은 플러그인 형식에 대한 것은 안내가 나와있으니 그것을 참고해서 작성하면 된다. <executions> : 플러그인 goal과 관련된 실행에 대한 설정 / <configuration> : 플러그인에서 필요한 설정 값 지정

 

 출처 

https://jeong-pro.tistory.com/168

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

Dirty Checking

Spring Data JPA와 같은 ORM 구현체를 사용하다보면 더티체킹을 만나볼 수 있다.

@Slf4j
@RequiredArgsConstructor
@Service
public class PayService {

    private final EntityManager em;

    public void updateNative(Long id, String tradeNo) {
        EntityTransaction tx = em.getTransaction();
        
        tx.begin();
        
        Pay pay = em.find(Pay.class, id);
        pay.changeTradeNo(tradeNo);
        
        tx.commit();
    }
}

save()를 하지 않는다.

1. 트랜잭션이 시작되고

2. 엔티티를 조회하고

3. 엔티티의 값을 변경하고 

4. 트랜잭션을 커밋한다.

 

update 쿼리는 없다.

 

@RunWith(SpringRunner.class)
@SpringBootTest
public class PayServiceTest {
    
    @Autowired
    PayRepository payRepsitory;
    
    @Autowired
    PayService payService;
    
    @After
    public void tearDown() throws Exception {
        payRepository.deleteAll();
    }
    
    @Test
    public void updateDirtyChecking() {
        // given
        Pay pay = payRepository.save(new Pay("test1", 100));
        
        // when
        String updateTradeNo = "test2";
        payService.updateNative(pay.getId(), updateTradeNo);
        
        // then
        Pay saved = payRepository.findAll().get(0);
        assertThat(save.getTradeNo()).isEqualTo(updateTradeNo);
    }
}

save() 메서드로 저장하지 않아도 dirty checking으로 update 쿼리가 실행되었다.

 

JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다.

상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용된다.

 

detach된 엔티티(준영속), DB에 반영되기 전 처음 생성된 엔티티(비영속) 상태의 엔티티는 Dirty Checking 대상이 아니다.

 

변경 부분만 update 하고 싶을 때는

Dirty Checking으로 생성되는 update 쿼리는 기본적으로 모든 필드를 업데이트한다.

 

JPA는 전체 필드를 업데이트하는 방식을 기본값으로 사용한다.

전체 필드를 업데이트하는 방식의 장점은 다음과 같다.

- 생성되는 쿼리가 같아 부트 실행시점에 미리 만들어서 재사용이 가능하다.

- 데이터베이스 입장에서 쿼리 재사용이 가능하다. (동일한 쿼리를 받으면 이전에 파싱된 쿼리를 재사용한다.)

 

필드가 20~30개 이상인 경우에 전체 필드 update가 부담스러운 경우 (이런 경우 정규화가 잘못된 확률이 높다.)

@DynamicUpdate로 변경 필드만 반영되도록 할 수 있다.

 

엔티티 최상단에 @DynamicUpdate를 선언해준다.

@Getter
@NoArgsConstructor
@Entity
@DynamicUpdate // 변경한 필드만 대응
public class Pay {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String tradeNo;
    private long amount;
}
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

@Modifying 이란?

  • @Query 어노테이션(JPQL Query, Native Query)을 통해 작성된 INSERT, UPDATE, DELETE (SELECT 제외) 쿼리에서 사용되는 어노테이션이다.
  • 기본적으로 JpaRepository에서 제공하는 메서드 혹은 메서드 네이밍으로 만들어진 쿼리에는 적용되지 않는다.
  • clearAutomatically, flushAutomatically 속성을 변경할 수 있으며 주로 벌크 연산과 같이 이용된다.
  • JPA Entity Life-cycle을 무시하고 쿼리가 실행되기 때문에 해당 어노테이션을 사용할 때는 영속성 컨텍스트 관리에 주의해야 한다. (clearAutomatically, flushAutomatically를 통해 간단하게 해결할 수 있다.)

Bulk 연산

벌크 연산이란, 단건 UPDATE, DELETE 연산을 제외한 다건의 UPDATE, DELETE 연산을 하나의 쿼리로 하는 것을 의미한다. JPA에서 단건 UPDATE 같은 경우에는 Dirty Checking(변경 감지)를 통해서 수행되거나 save()를 통해 수행할 수 있다. DELETE 경우 단건, 다건 쿼리 메서드로 제공된다.

 

여기서는 @Modifying과 @Query를 사용한 벌크연산을 알아본다.

이 방법의 장점은 JPQL을 자유롭게 정의하여 사용할 수 있고, 하나의 쿼리로 많은 데이터를 변경할 수 있다는 점이다.

 

@Query에 벌크 연산 쿼리를 작성하고, @Modifying 어노테이션을 붙이지 않으면 InvalidDataAccessApiUsageException이 발생한다.

 

clearAutomatically

이 attribute는 @Modifying이 붙은 해당 쿼리 메서드 실행 직후, 영속성 컨텍스트를 clear할 것인지 지정하는 attribute이다. default 값은 false이다. 하지만 default 값은 false이다. 하지만 default 값인 false로 사용할 경우, 영속성 컨텍스트의 1차 캐시와 관련된 문제점이 발생할 수 있다.

문제점

JPA에서는 영속성 컨텍스트에 있는 1차 캐시를 통해 엔티티를 캐싱하고, DB의 접근 횟수를 줄임으로써 성능을 개선한다. 1차 캐시는 @Id값을 key 값으로 엔티티를 관리한다. 그래서 findById 등을 통해 엔티티를 조회했을 시, @Id 값이 1차 캐시에 존재한다면 DB에 접근하지 않고, 캐싱된 엔티티를 반환한다. 

 

그렇다면, 벌크 연산을 통해 데이터 변경 쿼리를 실행하고, 해당 엔티티를 조회하면 어떤 일이 발생할지 예측해보자.

1. 엔티티 객체를 하나 생성한다. (transient 상태)

2. 객체의 초기값을 설정한다.

3. 해당 엔티티의 Repository를 통해 해당 엔티티 객체를 save() 메서드를 통해 저장한다. (persistent 상태)

4. 벌크 연산을 통해 기존에 설정한 초기값을 변경한다. (SQL 실행 / 이때, flush가 발생하여 해당 쿼리 메서드가 실행되기 전의 쿼리들이 모두 flush 된다.)

5. findById()를 통해 해당 엔티티를 조회한다.

 

예제

// Article.java
@Entity
@Getter
@Setter
public class Article {

    @Id
    @GeneratedValue
    private Long id;
    
    private String title;

    private String content;
}
// ArticleRepository.java
public interface ArticleRepository extends JpaRepository<Article, Long> {

    @Modifying
    @Query("UPDATE Article a SET a.title = :title WHERE a.id = :id")
    int updateTitle(Long id, String title);
}

title를 update하는 쿼리 메서드를 정의했다. @Modifying과 @Query를 통해 직접 JPQL을 정의했다. clearAutomatically는 default인 false이다.

 

@ExtendWith(SpringExtension.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class Test {

    @Autowired
    private ArticleRepository articleRepository;
    
    @org.junit.jupiter.api.Test
    @Rollback(false)
    void update() {
        Article article = new Article(); // article - Transient 상태
        article.setTitle("before");
        articleRepository.save(article); // article - Persistent 상태
        
        int result = articleRepository.updateTitle(1L, "after"); // UPDATE SQL
        assertThat(result).isEqualTo(1);
        
        System.out.println(articleRepository.findById(1L).get().getTitle()); // "before"이 출력
    }
}

참고) @DataJpaTest를 사용하면 자동으로 embeded DB로 대체되어서 실제 DB를 확인할 수 없게 된다. AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)은 embeded DB가 아닌 properties에 정의한 DataSource를 사용할 수 있게 한다.

참고) @DataJpaTest는 기본적으로 @Transactional을 포함하고 있다. 그래서 각 테스트 메서드를 실행할 때, 데이터가 Rollback이 된다. 데이터가 Rollback이 실행된다면 실제 DB에서 테이블을 확인할 수 있으므로 @Rollback(false)으로 처리해주었다.

 

실제 DB에서는 title이 "after"로 변경되었지만, 메서드 안에서는 여전히 "before"이다.

// ArticleRepository.java
public interface ArticleRepository extends JpaRepository<Article, Long> {

    @Modifying(clearAutomatically = true)
    @Query("UPDATE Article a SET a.title = :title WHERE a.id = :id")
    int updateTitle(Long id, String title);
}

clearAutomatically를 true로 설정해주었다.

after가 출력되는 것을 알 수 있다. after로 출력되기 전에 SELECT Query가 실행된다. 이는 1차 캐시가 아닌 DB에서 Article을 조회했기 때문이다.

 

결론

JPA의 1차 캐시는 DB에 접근 횟수를 줄여 성능을 개선하는 좋은 기능이지만 @Modifying과 @Query를 이용한 벌크 연산에서는 이 기능때문에 예측하지 못한 결과가 나올 수 있다. 

JPA에서 조회를 실행할 때, 1차 캐시를 확인해서 해당 엔티티가 1차 캐시에 존재한다면 DB에 접근하지 않고, 1차 캐시에 있는 엔티티를 반환한다. 하지만 벌크 연산은 1차 캐시를 포함한 영속성 컨텍스트를 무시하고 바로 Query를 실행하기 때문에 영속성 컨텍스트는 데이터 변경을 알 수 없다. 즉, 벌크 연산을 실행할 대 1차 캐시(영속성 컨텍스트)와 DB의 데이터 싱크가 맞지 않게 되는 것이다.

하지만, @Modifying의 clearAutomatically를 true로 변경해준다면, 벌크 연산 직 후 자동으로 영속성 컨텍스트를 clear 해준다. 그러면 조회를 실행하면 1차 캐시에 해당 엔티티가 존재하지 않기 때문에 DB 조회 쿼리를 실행하게 된다. 이렇게 함으로써 데이터 동기화 문제를 해결할 수 있다.

 

flushAutomatically

이 Attribute는 @Query와 @Modifying을 통한 쿼리 메서드를 사용할 때, 해당 쿼리를 실행하기 전, 영속성 컨텍스트의 변경사항을 DB에 flush할 것인지를 결정하는 Attribute이다. default 값은 false이다.

Hibernate의 FlushModeType

Spring Data JPA는 구현체로 Hibernate를 사용하고 있다. Spring Data JPA를 통해 Hibernate를 사용한다. 그 Hibernate에는 FlushModeType enum class가 있다. 그 값으론 AUTO와 COMMIT이 있고 default 값은 AUTO이다.

 

AUTO(default): flush가 쿼리 실행 시 발생한다.

COMMIT: flush가 트랜잭션이 commit 될 때 발생한다.

 

Hibernate의 FlushModeType가 default로 AUTO 였기 때문에 flushAutomatically가 false여도 쿼리 실행 전 flush가 나간다.

 

Hibernate로 직접 테스트

@Entity
@Getter
@Setter
public class Article {
    
    @Id
    @GeneratedValue
    private Long id;
    
    private String title;
    
    private String content;
    
    private Boolean isPublished = false;
    
    public void publish() {
        isPublished = true;
    }
}
public interface ArticleRepository extends JpaRepository<Article, Long> {

    @Modifying
    @Query("DELETE FROM Article a WHERE a.isPublished = TRUE")
    int deletePublic();
}

EntityManager는 JPA에서 컨텍스트 매니저를 관리하는 interface이다. 

Session은 Hibernate의 핵심이 되는 API이다. EntityManager를 상속받고 있다. 

entityManager.unwrap(Session.class)를 통해 Session을 직접 사용할 수 있다.

@Test
@DisplayName("JPA(Hibernate)를 통한 테스트 - FlushModeType.AUTO")
@Rollback(false)
void delete2() {
    Session session = entityManager.unwrap(Session.class);
    
    Article article = new Article(); // Transient
    session.save(article); //Persist
    
    Article byId = session.find(Article.class, 1L); // get by Persistence Context (NOT DB)
    byId.publish();
    
    int delete = session.createQuery("DELETE FROM Article a WHERE a.isPublished = TRUE").executeUpdate();
    
    assertThat(delete).isEqualTo(0);
}

FlushModeType.AUTO를 그대로 사용해보았다. 테스트는 실패한다.

 

@Test
@DisplayName("JPA(Hibernate)를 통한 테스트 - FlushModeType.AUTO")
@Rollback(false)
void delete2() {
    Session session = entityManager.unwrap(Session.class);
    session.setFlushMode(FlushModeType.COMMIT); // COMMIT으로 변경
    
    Article article = new Article(); // Transient
    session.save(article); //Persist
    
    Article byId = session.find(Article.class, 1L); // get by Persistence Context (NOT DB)
    byId.publish(); // isPublished를 true로 변경
    
    int delete = session.createQuery("DELETE FROM Article a WHERE a.isPublished = TRUE").executeUpdate();
    
    assertThat(delete).isEqualTo(0);
}

DELETE 쿼리가 실행되기 전 flush가 되지 않기 때문에 INSERT, UPDATE 쿼리는 실행되지 않았고 바로 DELETE 쿼리가 실행된다. 그러므로 쿼리 메서드의 반환값이 0이 된다. commit이 되는 시점에 flush가 되고 이때 INSERT, UPDATE 쿼리가 실행된다. 

 

Spring Data JPA로 테스트

application.yml에서 flushMode를 COMMIT으로 설정해줄 수 있다.

spring:
  jpa:
    properties:
      org:
        hibernate:
          flushMode: COMMIT

 

 

결론

Spring Data JPA는 구현체로 Hibernate를 사용한다. Hibernate의 FlushModeType의 default값은 AUTO이다.

@Modifying(flushAutomatically = false)여도 해당 쿼리 메서드 실행 전 flush가 실행된다.

 

flushAutomatically가 의미를 가리게 하려면 Hibernate의 FlushModeType의 값이 COMMIT이어야 한다.

 

 

 

 

 출처 

https://devhyogeon.tistory.com/4 

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

jpql delete query with join condition

DELETE FROM Post p
 WHERE p IN 
 (SELECT post FROM Project project
  JOIN project.posts post
  WHERE project.id = post.project.id)

sql delete query with join condition

DELETE posts
FROM posts
INNER JOIN projects ON projects.project_id = posts.project_id
WHERE projects.client_id = :client_id
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

JPA에서 대량의 데이터를 삭제할 때 주의할 점이 있습니다. 결론은 @Query 어노테이션을 사용하여 직접 삭제 쿼리를 작성한다.

 

예제

의존 관리는 Gradle를 사용하고 코드 간결성을 위해 lombok을, 테스트 프레임워크로 Spock을 사용하였다.

 

build.gradle

apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('com.h2database:h2')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.spockframework:spock-core:1.1-groovy-2.4')
    testCompile('org.spockframework:spock-spring:1.1-groovy-2.4')
}

application.yml 에 설정값을 추가한다.

spring:
  jpa:
    show-sql: true

사용할 엔티티 클래스는 Customer, Shop, Item이다.

// Customer.java
@Entity
@Getter
@NoArgsConstructor
public class Customer {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public Customer(String name) {
        this.name = name;
    }
}

// Shop.java
@Getter
@NoArgsConstructor
@Entity
public class Shop {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private String address;

    @OneToMany(mappedBy = "shop", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Item> items = new ArrayList<>();

    public Shop(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public void addItem(Item item){
        if(this.items == null){
            this.items = new ArrayList<>();
        }

        this.items.add(item);
        item.updateShop(this);
    }
}

// Item.java
@Getter
@NoArgsConstructor
@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private long price;

    @ManyToOne
    private Shop shop;

    public Item(String name, long price) {
        this.name = name;
        this.price = price;
    }

    public void updateShop(Shop shop){
        this.shop = shop;
    }
}

이 엔티티 클래스를 다룬 JpaRepository를 구현한 repository를 생성하겠습니다.

// CustomerRepository.java
public interface CustomerRepository extends JpaRepository<Customer, Long>{

    @Modifying
    @Transactional
    long deleteByIdIn(List<Long> ids);

    @Transactional
    @Modifying
    @Query("delete from Customer c where c.id in :ids")
    void deleteAllByIdInQuery(@Param("ids") List<Long> ids);
}

// ShopRepository.java
public interface ShopRepository extends JpaRepository<Shop, Long> {

    @Transactional
    @Modifying
    long deleteAllByIdIn(List<Long> ids);

    @Transactional
    @Modifying
    @Query("delete from Shop s where s.id in :ids")
    void deleteAllByIdInQuery(@Param("ids") List<Long> ids);
}

// ItemRepository.java
public interface ItemRepository extends JpaRepository<Item, Long> {

    @Transactional
    @Modifying
    @Query("delete from Item i where i.shop.id in :ids")
    void deleteAllByIdInQuery(@Param("ids") List<Long> ids);
}

첫번째 메서드인 deleteAllByIdIn은 JpaRepository에서 제공하는 delete 메서드를 활용한 것이다.

두번째 메서드인 deleteAllByIdInQuery는 @Query를 사용하여 직접 delete 쿼리를 사용한 것이다.

 

1. 다른 엔티티와 관계가 없는 엔티티 삭제

다른 엔티티와 관계가 없는 Customer 엔티티 삭제 기능을 테스트한 예제이다.

@SpringBootTest
class CustomerRepositoryTest extends Specification {

    @Autowired
    private CustomerRepository customerRepository;
    
    def "Customer in delete" () {
        given: // 100개의 데이터를 DB에 insert
        for (int i = 0; i < 100; i++) {
            customerRepository.save(new Customer(i + "님"))
        }
        
        when: // 3개의 ID 조건으로 delete
        customerRepository.deleteByIdIn(Arrays.asList(1L, 2L, 3L))
        
        then:
        println "======THEN====="
        customerRepository.findAll().size() == 97
    }
}
Hibernate: insert into customer (id, name) values (null, ?)
Hibernate: insert into customer (id, name) values (null, ?)
Hibernate: insert into customer (id, name) values (null, ?)
...
Hibernate: insert into customer (id, name) values (null, ?)
Hibernate: select customer0_.id as id1_0_, customer0_.name as name2_0_ from customer customer0_ where customer0_.id in (?, ?, ?)
Hibernate: delete from customer where id = ?
Hibernate: delete from customer where id = ?
Hibernate: delete from customer where id = ?
=====THEN=====

in 으로 조회하는 쿼리가 처음으로 실행된다.

id별로 각각 delete가 실행된다.

 

public abstract class AbstractJpaQuery implements RepositoryQuery {
    // ... 생략
    
    @Nullable
    public Object execute(Object[] parameters) {
        return this.doExecute(this.getExecution(), parameters);
    }

    @Nullable
    private Object doExecute(JpaQueryExecution execution, Object[] values) {
        JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(this.method.getParameters(), values);
        Object result = execution.execute(this, accessor);
        ResultProcessor withDynamicProjection = this.method.getResultProcessor().withDynamicProjection(accessor);
        return withDynamicProjection.processResult(result, new AbstractJpaQuery.TupleConverter(withDynamicProjection.getReturnedType()));
    }
    // ... 생략
}
public abstract class JpaQueryExecution {
    private static final ConversionService CONVERSION_SERVICE;

    public JpaQueryExecution() {
    }
    
    // ...생략
    
    static class DeleteExecution extends JpaQueryExecution {
        private final EntityManager em;

        public DeleteExecution(EntityManager em) {
            this.em = em;
        }

        protected Object doExecute(AbstractJpaQuery jpaQuery, JpaParametersParameterAccessor accessor) {
            Query query = jpaQuery.createQuery(accessor);
            List<?> resultList = query.getResultList();
            Iterator var5 = resultList.iterator();

            while(var5.hasNext()) {
                Object o = var5.next();
                this.em.remove(o);
            }

            return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList : resultList.size();
        }
    }
    // ...생략
}
  • jpaQuery.createQuery(values)의 결과로 select ~ from Customer where ~ 쿼리가 생성된다.
  • for loop를 돌면서 1건씩 삭제한다.

 

JpaRepository에서 제공하는 deleteByXXX 등의 메서드는 먼저 조회하고 그 결과로 얻은 엔티티를 1건씩 삭제한다.

2. 관계가 있는 엔티티 삭제

@SpringBootTest
class ShopRepositoryTest extends Specification {

    @Autowired
    private ShopRepository shopRepository;
    
    @Autowired
    private ItemRepository itemRepository;
    
    def setup() {
        for (long i = 0; i <= 2; i++) {
             SHOP_ID_LIST.add(i)
        }
    }
    
    def cleanup() {
        println "======== Clean All ========="
        itemRepository.deleteAll()
        shopRepository.deleteAll()
    }
    
    def "SpringDataJPA에서 제공하는 예약어를 통해 삭제한다 - 부모&자식" () {
        given:
        createShopAndItem()

        when:
        shopRepository.deleteAllByIdIn(SHOP_ID_LIST)

        then:
        shopRepository.findAll().size() == 8
    }

    private void createShop() {
        for (int i = 0; i < 10; i++) { 
            shopRepository.save(new Shop("우아한서점" + i, "우아한 동네" + i))
        }

        println "=======End Create Shop======="
    }

    private void createShopAndItem() {
        for (int i = 0; i < 10; i++) {
            Shop shop = new Shop("우아한서점" + i, "우아한 동네" + i)

            for (int j = 0; j < 3; j++) {
                shop.addItem(new Item("IT책" + j, j * 10000))
            }

            shopRepository.save(shop)
        }

        println "=======End Create Shop & Item======="
    }
}

 

Hibernate: select shop0_.id as id1_2_, shop0_.address as address2_2_, shop0_.name as name3_2_ from shop shop0_ where shop0_.id in (?, ?)
Hibernate: select item0_.shop_id as shop_id4_1_0_, item0_.id as id1_1_0, ... from item item0_ where item0_.shop_id = ?
Hibernate: select item0_.shop_id as shop_id4_1_0_, item0_.id as id1_1_0, ... from item item0_ where item0_.shop_id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from shop where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from item where id = ?
Hibernate: delete from shop where id = ?
  • in 으로 조회하는 쿼리가 처음 실행된다.
  • shop id 별로 item을 조회한다.
  • 조회된 item을 1건씩 삭제한다.
  • 조회된 shop을 1건씩 삭제한다.

 

SpringDataJpa에서 deleteByXXX 등의 메소드 사용시

  • 삭제 대상들을 전부 조회하는 쿼리가 1번 발생한다.
  • 삭제 대상들은 1건씩 삭제 된다.
  • cascade = CascadeType.DELETE으로 하위 엔티티와 관계가 맺어진 경우 하위 엔티티들도 1건씩 삭제가 진행된다.

해결책

직접 범위 조건의 삭제 쿼리를 작성하면 된다.

    @Transactional
    @Modifying
    @Query("delete from Customer c where c.id in :ids")
    void deleteAllByIdInQuery(@Param("ids") List<Long> ids);

이를 실행하면 

Hibernate: delete from customer where id in (?, ?, ?)

만약 Shop Item 같이 서로 연관관계가 있는 경우에는 Shop만 삭제시 에러가 발생할 수 있습니다.

Item을 먼저 삭제 후, Shop을 삭제하면된다.

itemRepository.deleteAllByIdInQuery(SHOP_ID_LIST)
shopRepository.deleteAllByIdInQuery(SHOP_ID_LIST)
Hibernate: delete from item where shop_id in (?, ?, ?)
Hibernate: delete from shop where id in (?, ?, ?)

 

 출처 

https://jojoldu.tistory.com/235

 

JPA에서 대량의 데이터를 삭제할때 주의해야할 점

안녕하세요? 이번 시간엔 JPA에서 대량의 데이터를 삭제할때 주의해야할 점을 샘플예제로 소개드리려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (

jojoldu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !