1. 스프링 데이터 JPA 퀵스타트

1.1.1 프로젝트 생성 및 기본 설정

 

스프링부트는 JPA 연동에 필요한 라이브러리들과 복잡한 XML 설정을 자동으로 처리하기 위해 JPA 스타터를 제공한다.

 

스프링 부트 프로젝트는 https://start.spring.io/ 에서 생성하면 된다.

DevTools, Lombok, Web, JPA, H2 의존성을 추가하면 된다.

 

src/main/resources 소스 폴더에 있는 application.properties 파일에 설정을 다음과 같이 추가하면 된다.

(DataSource Setting은 개인 환경에 맞게 수정 필요)

# DataSource Setting
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=1234

# JPA Setting
# ddlAuto는 처음 테이블 생성시에 create, 이후엔 update로 바꿔주면 된다.
spring.jpa.hibernate.ddlAuto=create 
spring.jpa.generateDdl=false
spring.jpa.showSql=true
spring.jpa.databasePlatform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.format_sql=true

# Logging Setting
logging.level.org.hibernate=info

 

1.1.2 엔티티 매핑과 리포지토리 작성하기

 

1) 엔티티 클래스 매핑

JPA는 엔티티를 통해서 데이터를 관리하기 때문에 가장 먼저 테이블과 매핑할 엔티티 클래스를 작성해야 한다.

 

package com.jpa.practice.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@ToString(exclude = "member")
@Entity
public class Board {

    @Id
    @GeneratedValue
    private Long seq;
    private String title;
    private String writer;
    private String content;
    @Temporal(value = TemporalType.TIMESTAMP)
    private Date createDate;
    private Long cnt;

}

seq 변수에 @Id와 @GeneratedValue를 추가하여 seq를 식별자 변수로 매핑했다.

따라서 JPA가 적절하게 자동으로 증가된 값을 seq에 할당한다.

(현재 실습에 사용 중인 데이터베이스는 H2이므로 시퀀스 전략을 기본으로 사용한다)

 

2) 테이블 생성 확인

package com.jpa.practice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PracticeApplication {

   public static void main(String[] args) {
      SpringApplication application = new SpringApplication(PracticeApplication.class);
      application.setWebApplicationType(WebApplicationType.NONE);
      application.run(args);
//    SpringApplication.run(PracticeApplication.class, args);

   }

}

먼저 스프링 애플리케이션을 실행할 때 웹 애플리케이션이 아닌 일반 Java 애플리케이션으로 실행하기 위해 다음과 같이 수정하였다(WebApplicationType을 NONE으로 설정)

 

수정 후 애플리케이션을 실행하면 테이블이 생성되는 것을 확인할 수 있다.

 

그런데 Board 엔티티에서 날짜에 해당하는 createDate 변수에 해당하는 컬럼이 create_date로 매핑되는 것을 볼 수 있다.

Camel Case를 사용하면 대문자가 나올 때마다 '_'(언더바)로 구분이 된다.

 

 

3) Repository 인터페이스 작성

엔티티를 작성했으면 이제 엔티티를 이용하여 CRUD 기능을 처리할 Repository 인터페이스를 작성해야 한다.

Repository는 기존의 DAO(Data Access Object)와 동일한 개념으로 비즈니스 클래스에는 이 Repository를 이용하여 실질적인 데이터베이스 연동을 처리한다.

Repository 인터페이스는 스프링에서 제공하는 Repository 중 하나를 상속하여 작성하면 된다.

 

Repository 인터페이스들의 상속 구조는 다음과 같다.

 

가장 상위에 있는 Repository는 기능이 거의 없으므로 일반적으로는 CrudRepository를 주로 사용한다.

만약 검색 기능이 필요하고 검색 결과 화면에 대해 페이징 처리를 하고자 할 경우에는 PagingAndSortingRepository를 사용하고,

스프링 데이터 JPA에서 추가한 기능을 사용하고 싶으면 마지막 인터페이스인 JpaRepository를 사용하면 된다.

모든 인터페이스들은 공통적으로 두 개의 제네릭 타입을 지정하도록 되어있다.

 

ex) CrudRepository<T, ID>

T : 엔티티의 클래스 타입

ID : 식별자 타입(@Id로 매핑한 식별자 변수의 타입)

 

 

이제 CrudRepository를 상속하여 BoardRepository 인터페이스를 작성해본다.

 

package com.jpa.practice.persistence;

import com.jpa.practice.domain.Board;

import org.springframework.data.repository.CrudRepository;

public interface BoardRepository extends CrudRepository<Board, Long> {

}

일반적으로 인터페이스를 정의한다는 것은 인터페이스를 구현한 클래스를 만들어 사용하겠다는 의미이다.

하지만 스프링 데이터 JPA를 사용하는 경우는 별도의 구현 클래스를 만들지 않고 인터페이스만 정의함으로서 기능을 사용할 수 있다.

스프링부트가 내부적으로 인터페이스에 대한 구현 객체를 자동으로 생성해주기 때문이다.

 

또한 JPA를 단독으로 사용했을 때, JPA를 이용해서 데이터베이스를 연동하기 위해서 사용했었던 EntityManagerFactory, EntityManager, EntityTransaction 같은 객체도 필요 없다. 이 모든 객체들의 생성과 활용이 스프링 데이터 JPA에서는 내부적으로 처리되기 때문이다.

 

1.1.3 CRUD 기능 테스트하기

 

1) 등록 기능 테스트

 

package com.jpa.practice;

import com.jpa.practice.domain.Board;
import com.jpa.practice.persistence.BoardRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Date;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {
    @Autowired
    private BoardRepository boardRepository;

    @Test
    public void testInsertBoard() {
        Board board = new Board();
        board.setTitle("첫 번째 게시글");
        board.setWriter("테스터");
        board.setContent("잘 등록되나요?");
        board.setCreateDate(new Date());
        board.setCnt(0L);

        boardRepository.save(board);

    }
    
}

src/test/java에 다음과 같이 테스트 클래스와 메서드를 작성하였다.

 

이전에는 JPA의 persist() 메서드를 사용했었지만, CrudRepostitory 인터페이스를 사용할 때는 save() 메서드를 이용한다.

그리고 테스트를 실행하기 전에 application.properties 파일에서 테이블 자동생성 기능을 update로 수정하여 테이블이 매번 새로 생성되지 않고 기존의 테이블을 사용하도록 한다.

 

# JPA Setting
spring.jpa.hibernate.ddlAuto=update

 

테스트 메서드를 실행하면 데이터베이스에 데이터가 저장이 되는 것을 확인할 수 있다.

 

 

(2) 상세 조회 기능 테스트

 

데이터 조회 기능을 테스트 하는 메서드를 추가해본다.

 

@Test
public void testGetBoard() {
    Board board = boardRepository.findById(1L).get();
    System.out.println(board);
}

데이터를 하나 조회하기 위해 findById() 메서드를 사용하였다.(PK 값을 통해 조회)

그러면 Optional 타입의 객체가 리턴되고, get() 메서드를 통해 영속성 컨텍스트에 저장된 Board 객체를 받을 수 있다.

 

테스트 메서드를 실행하면 PK가 1L 에 해당하는 데이터가 조회되는 것을 확인할 수 있다.

 

(3) 수정 기능 테스트

 

데이터 수정 기능을 테스트하는 메서드를 추가해본다.

 

@Test
public void testUpdateBoard() {
    System.out.println("=== 1번 게시글 조회");
    Board board = boardRepository.findById(1L).get();

    System.out.println("=== 1번 게시글 제목 수정");
    board.setTitle("제목을 수정했습니다.");
    boardRepository.save(board);

}

엔티티를 수정하려면 우선 수정할 엔티티를 영속성 컨텍스트에 올리기 위해 조회를 해야한다.

그리고 나서 수정할 값들을 setting하고 save() 메서드를 이용하여 수정작업을 반영하면 된다.

 

테스트 메서드를 실행해보면 먼저 데이터를 SELECT해온 후 UPDATE를 실행하는 것을 확인할 수 있다.

 

(4) 삭제 기능 테스트

 

데이터 삭제 기능을 테스트하는 메서드를 추가해본다.

 

@Test
public void testDeleteBoard() {
    boardRepository.deleteById(1L);
}

삭제도 수정과 마찬가지로 삭제하기 전에 삭제할 엔티티를 영속성 컨텍스트에 올리는 SELECT가 먼저 처리되고 나서 DELETE문이 실행된다.

 

 

1.2 쿼리 메서드 사용하기

(1) 쿼리 메서드란?

목록 검색과 관련된 기능을 사용하기 위해, JPA를 이용해서 목록 기능을 구현할 때 JPQL(Java Persistence Query Language)을 이용하면 된다.

 

JPQL은 검색 대상이 테이블이 아닌 엔티티라는 것만 제외하고는 기본 구조와 문법이 기존의 SQL과 유사하다.

스프링 JPA에서는 이런 복잡한 JPQL을 메서드로 대신 처리할 수 있도록 쿼리 메서드라는 특별한 기능을 제공한다.

 

쿼리 메서드는 몇 가지 네이밍 룰을 이용해 메서드를 만들면 된다.

쿼리 메서드를 사용할 때 가장 많이 사용하는 문법은 검색하려는 엔티티에서 특정 변수의 값으로 조회하는 것이다.

메서드 이름을 find로 시작하면서 조회에 이용할 변수들을 적절하게 조합하면 된다.

 

형식)

find + 엔티티 이름(생략시 CrudRepository 인터페이스에 선언된 제네릭 타입이 자동으로 들어감) + By + 변수 이름

 

ex)

findBoardByTitle() : Title로 Board 엔티티들을 검색

 

public interface BoardRepository extends CrudRepository<Board, Long> 로 인터페이스가 선언되어 있는 경우

Board가 엔티티 타입으로 정해져 있으므로 findByTitle() 을 사용해도 된다.

 

쿼리 메서드의 리턴타입으로 많이 사용하는 것은 Page<T>와 List<T>가 있다.

 

(2) 쿼리 메서드 사용하기

 

BoardRepository 인터페이스에 게시글 제목으로 목록을 조회하는 메서드를 추가해본다.

 

package com.jpa.practice.persistence;

import com.jpa.practice.domain.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface BoardRepository extends CrudRepository<Board, Long> {

	List<Board> findByTitle(String searchKeyword);

}

그리고 테스트 클래스를 src/test/java 소스 폴더에 작성한다.

 

package com.jpa.practice;

import com.jpa.practice.domain.Board;
import com.jpa.practice.persistence.BoardRepository;
import com.querydsl.core.util.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Arrays;
import java.util.Date;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class QueryMethodTest {

    @Autowired
    private BoardRepository boardRepository;

	// 테스트 메서드가 실행되기 전에 동작
    @Before
    public void dataPrepare() {
        for(int i = 1; i <= 200; i++) {
            Board board = new Board();
            board.setTitle("테스트 제목 " + i);
            board.setWriter("테스터");
            board.setContent("테스트 내용 " + i);
            board.setCreateDate(new Date());
            board.setCnt(0L);
            boardRepository.save(board);
       }
    }

    @Test
    public void testFindByTitle() {
        List<Board> boardList = boardRepository.findByTitle("테스트 제목 10");

        System.out.println("검색 결과");
        for(Board board : boardList) {
            System.out.println("---> " + board);
        }
    }
    
}

 

먼저 테스트를 위해 200개의 테스트 데이터를 등록하는 메서드를 작성하였다.

@Before 가 붙은 메서드는 테스트 메서드가 실행되기 전에 동작하여 테스트에서 사용할 데이터를 세팅한다.

 

그리고 나서 testFindByTitle() 메서드로, title이 정확히 "테스트 제목 10"인 게시글을 검색하였다.

실행하면 해당 제목을 가진 게시글을 검색할 수 있다.

 

(3) 쿼리 메서드 유형

 

쿼리 메서드를 작성할 때 일반적으로 많이 사용하는 키워드 들이 있다.

쿼리 메서드 유형

 

1.2.2 쿼리 메서드 활용하기

(1) LIKE 연산자 사용하기

 

게시글 내용에 특정 단어가 포함된 목록을 검색하려면 Containing 키워드를 사용한다.

BoardRepository 인터페이스에 findByContentContaining() 메서드를 추가해본다.

 

public interface BoardRepository extends CrudRepository<Board, Long> {
	...
    List<Board> findByContentContaining(String searchKeyword);
    
}

테스트 클래스에 이전에 작성했던 Prepare 메서드를 주석처리 하고 새로운 테스트를 추가해본다.

 

@Test
public void testByContentContaining() {
    List<Board> boardList = boardRepository.findByContentContaining("17");

    System.out.println("검색 결과");

    for(Board board : boardList) {
        System.out.println("---> " + board);
    }
}

content에 17이라는 값이 포함된 게시글 목록을 반환하여 출력했다.

 

실행된 쿼리를 보면 WHERE 절에서 LIKE 연산자를 통해 조회한 것을 확인할 수 있다.

 

(2) 여러 조건 사용하기

And나 Or 키워드를 사용하여 여러 제약조건을 기술해 검색할 수 있다.

 

게시글 제목 혹은 내용에 특정 단어가 포함된 글 목록을 조회하는 메서드를 추가해본다.

List<Board> findByTitleContainingOrContentContaining(String title, String content);

-> title 또는 content에서 특정 문자열을 포함하는 게시글을 검색한다.

 

위의 메서드를 실행해볼 테스트 메서드를 작성해본다.

@Test
public void testFindByTitleContainingOrContentContaining() {
    List<Board> boardList = boardRepository.findByTitleContainingOrContentContaining("17", "17");

    System.out.println("=== 검색 결과");
    for(Board board : boardList) {
        System.out.println("---> " + board);
    }
}

실행 결과로 출력된 쿼리를 보면 WHERE 절에 두 개의 조건이 OR 연산으로 결합된 것을 확인할 수 있다.

 

(3) 데이터 정렬하기

데이터를 정렬해서 조회하기 위해서는 OrderBy + 변수 + Asc(또는 Desc)를 이용하면 된다.

글 제목에 특정 단어가 포함된 글 목록을 내림차순으로 조회하는 메서드를 인터페이스에 추가해본다.

List<Board> findByTitleContainingOrderBySeqDesc(String searchKeyword);

 

테스트할 메서드를 테스트 클래스에 작성한다.

@Test
public void testFindByTitleContainingOrderBySeqDesc() {
    List<Board> boardList = boardRepository.findByTitleContainingOrderBySeqDesc("17");

    System.out.println("=== 검색 결과");
    for(Board board : boardList) {
        System.out.println("---> " + board);
    }
}

실행 결과로 출력된 쿼리를 보면 ORDER BY절이 추가되었고 DESC로 내림차순 정렬된 것을 확인할 수 있다.

 

 

1.2.3 페이징과 정렬 처리하기

모든 쿼리 메서드는 마지막 파라미터로 페이징 처리를 위한 Pageable 인터페이스와 정렬을 처리하는 Sort 인터페이스를 추가할 수 있다.

 

(1) 페이징 처리

한 화면에 다섯개의 데이터를 보여주기로 하고 첫 페이지에 해당하는 1번부터 5개의 데이터만 조회하는 메서드를 인터페이스에 추가해본다.

List<Board> findByTitleContaining(String searchKeyword, Pageable paging);

 

테스트 클래스에 새로운 테스트 메서드를 추가해본다.

@Test
public void testFindByTitleContaining() {
    Pageable paging = PageRequest.of(0, 5);

    List<Board> boardList = boardRepository.findByTitleContaining("제목", paging);

    System.out.println("=== 검색 결과");
    for(Board board : boardList) {
        System.out.println("---> " + board);
    }

}

title에 '제목' 이라는 검색어가 포함된 게시글을 조회하되, 1번부터 다섯개의 데이터만 조회하도록 했다.

Pageable 객체를 생성할 때 사용한 PageRequest.of(0, 5) 메서드에서 첫 번째 인자 0은 페이지 번호인데, 0부터 시작하기 때문에 첫 번째 페이지를 의미한다. 두 번째 인자인 5는 검색할 데이터의 개수이다.

 

실행 결과를 보면, 여기서는 H2 데이터베이스를 사용하기 때문에 limit 예약어를 사용하여 페이징 관련 SQL을 처리했다.

만약 오라클을 사용했다면 다른 쿼리가 생성되었을 것이다.

 

그리고 페이징 정렬 기준을 여러개 주고 싶다면 다음과 같이 작성하면 된다.

Pageable paging = PageRequest.of(0, 5, Sort.by("seq").ascending().and(Sort.by("title").descending()));

and를 이용해 정렬 조건을 

 

(2) 정렬 처리

페이징 처리를 할 때, 데이터를 정렬해서 출력하려면 Sort 클래스를 사용하면 된다.

방금 테스트한 testFindByTitleContaining() 메서드를 수정해본다.

 

@Test
public void testFindByTitleContaining() {
    Pageable paging = PageRequest.of(0, 5, Sort.Direction.DESC, "seq", "title");

    List<Board> pageInfo = boardRepository.findByTitleContaining("제목", paging);

    System.out.println("=== 검색 결과");
    for(Board board : boardList) {
        System.out.println("---> " + board);
    }

}

Pageable 객체를 생성할 때, 기존의 두 개의 인자 외에 두 개를 추가로 넘겨준다.

추가된 첫 번째 인자는 정렬 방향이고 두 번째는 정렬 대상이 되는 변수 이름이다.

 

테스트를 실행하면 ORDER BY가 추가된 결과를 얻을 수 있다.

 

이전에 살펴본 쿼리 메서드에 OrderBy를 결합한 것과 같은 결과를 얻을 수 있다.

List<Board> findByTitleContainingOrderBySeqDesc(String searchKeyword, Pageable paging);

 

하지만 쿼리 메서드는 메서드 명이 어쨌든 고정되어 있기 때문에 정렬 조건이 변할 때마다 메서드명을 바꿔줘야 한다.

따라서 정렬 조건을 자주 변경할 때는 Sort 클래스를 활용하는 것이 좋다.

 

 

1.2.4 Page<T> 타입 사용하기

 

스프링 MVC에서 검색 결과를 사용할 목적이라면 List<T> 보다는 Page<T>를 사용하는 것이 좋다.

Page<T> 객체는 페이징 처리할 때 사용할 수 있는 다양한 정보들을 추가로 제공한다.

 

findByTitleContaining() 메서드의 리턴 타입을 수정하여 테스트를 해본다.

Page<Board> findByTitleContaining(String searchKeyword, Pageable paging);

 

테스트 클래스에 있는 메서드도 수정한다.

@Test
    public void testFindByTitleContaining() {

        Pageable paging = PageRequest.of(0, 5, Sort.Direction.DESC, "seq", "title");
        
        Page<Board> pageInfo = boardRepository.findByTitleContaining("제목", paging);

        System.out.println("PAGE SIZE : " + pageInfo.getSize());
        System.out.println("TOTAL PAGES : " + pageInfo.getTotalPages());
        System.out.println("TOTAL COUNT : " + pageInfo.getTotalElements());
        System.out.println("NEXT : " + pageInfo.nextPageable());

        List<Board> boardList = pageInfo.getContent();

        System.out.println("=== 검색 결과");
        for(Board board : boardList) {
            System.out.println("---> " + board);
        }

    }

실행 결과를 보면 일반적인 검색 쿼리와 함께 검색 조건에 부합하는 데이터의 총 개수를 조회하기 위한 쿼리도 실행되는 것을 확인할 수 있다.

 

그리고 Page 객체의 메서드로부터 페이징 처리에 필요한 다양한 정보를 얻을 수 있다.

 

getSize() : 한 페이지의 크기

getTotalPages() : 전체 페이지의 갯수

getTotalElements() : 전체 카운트(여기에서는 게시글 갯수)

nextPageable() : 다음 페이지 객체

getContent() : 조회된 데이터 목록

 

 

1.3 @Query 애너테이션 사용하기

일반적인 쿼리는 스프링 데이터 JPA의 쿼리 메서드 만으로도 충분히 처리할 수 있다. 하지만 조금 복잡한 쿼리를 사용한다거나 연관관계에 기반한 조인(JOIN) 검색을 처리하기 위해서는 JPQL(Java Persistence Query Language)을 사용해야 한다.

또는 성능상 어쩔 수 없이 특정 데이터베이스에 종속적인 네이티브 쿼리를 사용해야 하는 경우도 있다.

이를 위해서 제공되는 것이 @Query 애너테이션이다.

 

(1) 위치 기반 파라미터 사용하기

 

@Query 애너테이션을 사용하는 테스트 메서드를 인터페이스에 추가해본다.

@Query("SELECT b FROM Board b WHERE b.title like %?1% ORDER BY b.seq DESC")
List<Board> queryAnnotationTest1(String searchKeyword);

 

JPQL은 일반적인 SQL과 유사한 문법을 가지고 있지만 검색 대상이 테이블이 아니라 영속성 컨텍스트에 등록된 엔티티다.

따라서 FROM 절에 엔티티 이름을 대소문자를 구분하여 정확하게 지정해야 한다. 그리고 칼럼 대신 엔티티가 가지고 있는 변수를 조회하기 때문에 SELECT나 WHERE절에서 사용하는 변수 이름 역시 대소문자를 구분해야 한다.

 

그리고 JPQL에서는 사용자 입력 값을 바인딩할 수 있도록 위치 기반 파라미터와 이름 기반 파라미터 두 가지를 지원한다.

예제에서는 위치 기반 파라미터를 사용했다. '?1' 이라고 하면 첫 번째 파라미터를 의미한다.

따라서 메서드의 매개변수로 받은 searchKeyword가 첫 번째 파라미터 값으로 바인딩 된다.

 

이제 queryAnnotationTest1() 메서드를 테스트 하기 위한 테스트 메서드를 작성해본다.

@Test
public void testQueryAnnotaionTest1() {
    List<Board> boardList = boardRepository.queryAnnotationTest1("1");

    System.out.println("검색 결과");
    for(Board board : boardList) {
        System.out.println("---> " + board);
    }
}

실행 결과를 보면 WHERE 절에 LIKE 연산자가 사용되었고 사용자 입력 값이 ?1에 바인딩 되어 검색 결과를 가져온 것을 확인할 수 있다.

 

(2) 이름 기반 파라미터 사용하기

 

이번엔 이름 기반 파라미터로 테스트를 해본다.

우선 앞에서 작성했던 @Query를 수정한다.

@Query("SELECT b FROM Board b WHERE b.title like %:searchKeyword% ORDER BY b.seq DESC")
List<Board> queryAnnotationTest1(@Param("searchKeyword") String sk);

 

' ?1 ' 였던 부분을 ' :searchKeyword '로 수정했다.

그리고 문자열 sk 값이 바인딩 되도록 매개변수에 @Param 애너테이션을 붙이고 searchKeyword로 지정했다.

 

실행 결과는 앞과 동일하다.

 

1.3.2 특정 변수만 조회하기

@Query를 이용할 때 엔티티를 통째로 검색하지 않고 특정 변수만 조회할 수도 있다.

 

인터페이스에 새로운 메서드를 추가해본다.

@Query("SELECT b.seq, b.title, b.createDate " +
        "FROM Board b " +
        "WHERE b.title like %?1% " +
        "ORDER BY b.seq DESC")
List<Object[]> queryAnnotationTest2(String searchKeyword);

특정한 변수들을 조회할 때 중요한 점은 검색 결과로 Board 엔티티 객체가 아니라 Object 배열로 조회된다는 점이다.

따라서 리턴 타입을 List<Object[]>로 해야 한다.

 

새로운 테스트 메서드를 작성해본다.

@Test
public void testQueryAnnotaionTest2() {
    List<Object[]> boardList = boardRepository.queryAnnotationTest2("2");

    System.out.println("검색 결과");
    for(Object[] row : boardList) {
        System.out.println("---> " + Arrays.toString(row));
    }
}

메서드의 리턴 타입이 List<Object[]> 이므로 배열 객체를 꺼내서 출력하였다.

 

 

@Query를 사용할 때의 주의사항은 @Query로 등록한 SQL은 프로젝트가 로딩되는 시점에 파싱되어 처리된다는 것이다.

따라서 @Query로 등록한 SQL에 오류가 있으면 무조건 예외가 발생되고 프로그램이 실행되지 않는다. 이는 프로그램이 실행되기 전에 사용할 SQL들을 모두 메모리에 올려둠으로서 성능을 향상시킬 수 있기 때문이다.

 

 

1.3.3 네이티브 쿼리 사용하기

 

@Query를 사용하면 특정 데이터베이스에서만 사용하는 네이티브 쿼리를 실행할 수 있다.

 

인터페이스에 새로운 메서드를 추가해본다.

    @Query(value="SELECT seq, title, create_date " +
            "FROM BOARD WHERE title LIKE '%'||?1||'%' " +
            "ORDER BY SEQ DESC", nativeQuery = true)
    List<Object[]> queryAnnotationTest3(String searchKeyword);

우선 from 절에 엔티티가 아닌 정상적인 테이블 이름이 사용되었고(board), SELECT와 WHERE 절에서도 변수가 아닌 BOARD 테이블의 칼럼 이름을 사용했다. 그리고 WHERE 절에서는 LIKE와 문자열 접합 연산자 '||' 를 사용했다.

그리고 마지막으로 이 쿼리가 JPQL이 아닌 네이티브 쿼리임을 알려주는 nativeQuery=true 속성을 추가했다.

 

테스트 메서드를 추가해본다.

@Test
public void testQueryAnnotationTest3() {
    List<Object[]> boardList = boardRepository.queryAnnotationTest3("1");

    System.out.println("검색 결과");
    for(Object[] row : boardList) {
        System.out.println("---> " + Arrays.toString(row));
    }
}

 

실행 결과를 보면, 작성한 SQL 구문이 그대로 사용된 것을 확인할 수 있다.

 

 

1.3.4 페이징 및 정렬 처리하기

 

@Query를 사용할때에도 페이징 처리를 위한 Pageable 인터페이스는 쿼리 메서드와 동일하게 사용할 수 있다.

그리고 Sort를 이용하여 조회 결과에 대해서 정렬도 추가할 수 있다.

 

인터페이스에 페이징 처리 메서드를 추가해본다.

@Query("SELECT b FROM Board b ORDER BY b.seq DESC")
List<Board> queryAnnotationTest4(Pageable paging);

 

이제 테스트 메서드를 추가해본다.

@Test
public void testQueryAnnotationTest4() {
    Pageable paging = PageRequest.of(0, 3, Sort.Direction.DESC, "seq");
    List<Board> boardList = boardRepository.queryAnnotationTest4(paging);

    System.out.println("검색 결과");
    for(Board board : boardList) {
        System.out.println("---> " + board);
    }
}

결과를 보면 원하는 페이지의 3개의 데이터를 내림차순으로 가져온 것을 확인할 수 있다.

 

 

1.4 QueryDSL을 이용한 동적 쿼리 적용하기

 

1.4.1 QueryDSL이란?

QueryDSL은 오픈소스 프로젝트로서 쿼리를 뭄ㄴ자열이 아닌 자바 코드로 작성할 수 있도록 지원하는 일종의 JPQL 빌더라고 보면 된다.

 

MyBatis처럼 동적으로 쿼리를 처리하려면 JPA에서는 QueryDSL을 이용해야 한다.

 

1.4.2 QueryDSL 설정하기 

 

QueryDSL을 이용하기 위해서는 먼저 프로젝트에 QueryDSL 관련 라이브러리를 추가해야 한다.

pom.xml 에 다음과 같은 의존성을 추가한다.

<dependency>
   <groupId>com.querydsl</groupId>
   <artifactId>querydsl-jpa</artifactId>
   <version>5.0.0</version>
</dependency>

<dependency>
   <groupId>com.querydsl</groupId>
   <artifactId>querydsl-apt</artifactId>
   <version>5.0.0</version>
</dependency>

 

1.4.3 QueryDSL 플러그인 추가하기

QueryDSL을 사용하려면 엔티티 클래스를 기반으로 쿼리 타입이라는 쿼리용 클래스를 생성해야 한다.

따라서 현재 프로젝트에 쿼리 타입 클래스를 생성하기 위한 플러그인을 추가해야 한다.

 

<build>
   <plugins>
      <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
         <configuration>
            <excludes>
               <exclude>
                  <groupId>org.project-lombok</groupId>
                  <artifactId>lombok</artifactId>
               </exclude>
            </excludes>
         </configuration>
      </plugin>
      
      <plugin>
         <groupId>com.mysema.maven</groupId>
         <artifactId>apt-maven-plugin</artifactId>
         <version>1.1.3</version>
         <executions>
            <execution>
               <goals>
                  <goal>process</goal>
               </goals>
               <configuration>
                  <outputDirectory>src/main/querydsl</outputDirectory>
                  <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
               </configuration>
            </execution>
         </executions>
      </plugin>
   </plugins>
</build>

pom.xml 파일 하단에 있는 <build> 안에 plugin으로 com.mysema.maven를 입력하여 추가해준다.

 

apt-maven-plugin 플러그인을 추가했으며, src/main/querydsl이라는 소스 폴더에 쿼리 타입 클래스인 Q클래스를 생성하라는 의미이다.

 

메이픈 프로젝트를 업데이트하고 maven compile을 하면 src/main/querydsl 경로에 QBoard.java 클래스가 생성된다.

 

 

1.4.4 동적 쿼리 사용하기

 

QueryDSL을 적용하기 위해서는 먼저 BoardRepository 인터페이스가 QuerydslPredicateExecutor 인터페이스를 추가로 상속해야 한다.

DynamicBoardRepository 인터페이스를 생성해본다.

 

package com.jpa.practice.persistence;

import com.jpa.practice.domain.Board;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;

public interface DynamicBoardRepository extends CrudRepository<Board, Long>,
        QuerydslPredicateExecutor<Board> {
    
}

QuerydslPredicateExecutor를 추가로 구현했기 때문에 DynamicBoardRepository에서 사용할 수 있는 메서드가 더 추가되었다.

 

추가로 사용할 수 있는 메서드는 다음과 같다.

메서드 메서드 설명
long count(Predicate p) 검색된 데이터의 전체 개수
boolean exists(Predicate p) 검색된 데이터의 존재 여부
Iterable<T> findAll(Predicate p) 조건에 맞는 모든 데이터 목록
Page<T> findAll(Predicate p) 조건에 맞는 모든 데이터 목록
Iterable<T> findAll(Predicate p, Sort s) 조건에 맞는 모든 데이터 목록 정렬
T findOne(Predicate p) 조건에 맞는 하나의 데이터

공통적으로 Predicate 타입의 객체를 매개변수로 받고 있다.

그리고 Predicate 인터페이스를 구현한 클래스는 BooleanBuilder 클래스이다.

BooleanBuilder 클래스를 만들고 내가 원하는 조건을 구성한 후 Repository의 메서드 매개변수에 제공하면 된다.

 

테스트 클래스를 만들고 메서드를 작성해본다.

package com.jpa.practice;

import com.jpa.practice.domain.Board;
import com.jpa.practice.domain.QBoard;
import com.jpa.practice.persistence.DynamicBoardRepository;
import com.querydsl.core.BooleanBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicQueryTest {
    @Autowired
    private DynamicBoardRepository boardRepo;

    @Test
    public void testDynamicQuery() {
        String searchCondition = "CONTENT";
        String searchKeyword = "테스트 내용 10";

        BooleanBuilder builder = new BooleanBuilder();

        QBoard qBoard = QBoard.board;

        if(searchCondition.equals("TITLE")) {
            builder.and(qBoard.title.like("%" + searchKeyword + "%"));
        } else if(searchCondition.equals("CONTENT")) {
            builder.and(qBoard.content.like("%" + searchKeyword + "%"));
        }

        Pageable paging = PageRequest.of(0, 5);

        Page<Board> boardList = boardRepo.findAll(builder, paging);

        showResult(boardList);

    }

    public void showResult(Page<Board> boardList) {
        System.out.println("=== 검색 결과");
        for(Board board : boardList) {
            System.out.println("---> " + board);
        }
    }
}

먼저 BooleanBuilder 객체를 만들고 searchCondition에 따라 동적으로 검색 조건을 추가하였다.

마지막으로는 페이징 처리를 위해 Pageable 객체를 만들어 메서드에 전달하였다.

 

2. 연관관계 매핑

우리는 데이터를 관련된 여러 테이블에 나누어 저장하고 테이블을 조인하여 데이터를 처리한다.

JPA는 테이블과 엔티티를 매핑하는 기술이고, 따라서 엔티티도 다른 엔티티와 연관관계로 매핑을 해야한다.

테이블은 PK와 FK를 기반으로 연관관계를 맺지만 객체는 참조 변수를 통해 연관관계를 맺기 때문에 테이블의 연관과 엔티티의 연관이 정확하게 일치하지 않는다.

 

JPA에서 이 문제를 어떻게 해결해야 할까?

 

2.1 단방향 연관관계 설정하기

2.1.1 연관관계 매핑하기

먼저 매핑과 관련된 몇 가지 용어들을 정리해본다.

용어 설명
방향(Direction) 단방향과 양방향이 있다.
예를 들면 게시판 객체가 참조 변수를 통해 회원 객체를 참조하면 단방향이고, 회원 객체도 게시판 객체를 참조한다면 양방향이 된다.
중요한 점은 방향은 객체에만 존재하고 테이블은 항상 양방향이라는 것이다.
다중성(Multiplicity) 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)가 있다.
예를 들면 회원과 게시글의 관계는
회원이 여러 개의 글을 작성할 수 있기 때문에 일대다 관계이고 , 반대로 게시글 입장에서는 다대일 관계가 된다.
연관관계 주인(Owner) 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다. 일반적으로 다대일(N:1)이나 일대다(1:N) 관계에서 연관관계의 주인은 다(N) 쪽에 해당하는 객체라고 생각하면 쉽다.

 

2.1.2 다대일(N:1) 단방향 매핑하기

(1) 다대일 관계 이해하기

 

다대일 연관 매핑을 테스트 하기 위해 다음과 같은 조건을 가정해본다.

1. 게시판과 회원이 있다.

2. 한 명의 회원은 여러 개의 게시글을 작성할 수 있다

3. 게시판과 회원은 다대일 관계이다.

4. 게시글을 통해서 게시글을 작성한 회원 정보를 조회할 수 있다(반대는 안 됨).

 

게시글 객체와 회원 객체는 단방향 관계로서, 게시글은 Board.member 변수를 통해 회원 정보를 알 수 있지만

반대로 회원은 게시글에 대한 참조 변수를 가지지 않기 때문에 게시글 정보를 알 수 없다.

 

(2) 연관관계 매핑하기

다대일 연관관계를 테스트하기 위해서 회원 클래스를 추가한다.

import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@ToString
@Entity
public class Member {

    @Id
    @Column(name="MEMBER_ID")
    private String id;
    private String password;
    private String name;
    private String role;

}

클래스를 생성하고 Maven Compile을 하면 QMember라는 클래스가 자동으로 생성된다.

 

이제 Board 클래스에 다음과 같이 다대일 연관 매핑 설정을 추가한다.

package com.jpa.practice.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@ToString
@Entity
public class Board {

    @Id
    @GeneratedValue
    private Long seq;
    private String title;
//    private String writer;
    private String content;
    @Temporal(value = TemporalType.TIMESTAMP)
    private Date createDate;
    private Long cnt;

    @ManyToOne 
    @JoinColumn(name="MEMBER_ID") // name은 컬럼 이름이 된다.
    private Member member;

}

 

먼저 writer 대신 Member 객체와 연관 매핑을 처리하기 위해서 Member 타입의 member 변수를 추가하였다.

그리고 다대일(N:1) 관계를 설정하기 위해서 member 변수 위에 @ManyToOne 애너테이션을 추가했다.

 

@ManyToOne 애너테이션에서 사용할 수 있는 속성은 다음과 같다.

속성 기능 기본 값
optional 연관된 엔티티가 반드시 있어야 하는지의 여부를 결정한다.
false로 설정하면 항상 있어야 한다는 의미다.
true
fetch 글로벌 페치 전략을 설정한다. EAGER는 연관되어 있는 엔티티를 항상 같이 조회하며(그 엔티티를 실제로 사용하든 안하든), LAZY는 연관되어 있는 엔티티를 실제로 사용할 때만 조회한다. @ManyToOne : EAGER
@OneToMany : LAZY
cascade 영속성 전이 기능을 설정한다. 연관 엔티티를 같이 저장하거나 삭제할 때 사용한다.  

 

그리고 member에 외래 키(FK) 매핑을 위해 @JoinColumn 애너테이션도 사용했다.

@JoinColumn은 name 속성을 통해 참조하는 테이블의 외래 키 칼럼을 매핑한다.

 

 

새로 추가된 회원 엔티티를 위한 MemberRepository를 추가한다.

package com.jpa.practice.persistence;

import com.jpa.practice.domain.Member;
import org.springframework.data.repository.CrudRepository;

public interface MemberRepository extends CrudRepository<Member, String> {

}

 

2.1.3 다대일 연관관계 테스트하기

(1) 게시글 등록 테스트

연관 매핑 테스트를 위해 먼저 게시글을 등록하는 테스트 메서드를 작성해본다.

package com.jpa.practice;

import com.jpa.practice.domain.Board;
import com.jpa.practice.domain.Member;
import com.jpa.practice.persistence.BoardRepository;
import com.jpa.practice.persistence.MemberRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Date;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RelationMappingTest {
    @Autowired
    private BoardRepository boardRepo;

    @Autowired
    private MemberRepository memberRepo;

    @Test
    public void testManyToOneInsert() {
        Member member1 = new Member();
        member1.setId("member1");
        member1.setPassword("member111");
        member1.setName("둘리");
        member1.setRole("User");
        memberRepo.save(member1);

        Member member2 = new Member();
        member2.setId("member2");
        member2.setPassword("member222");
        member2.setName("도우너");
        member2.setRole("Admin");
        memberRepo.save(member2);

        for(int i = 1; i <= 3; i++) {
            Board board = new Board();
            board.setMember(member1);
            board.setTitle("둘리가 등록한 게시글 " + i);
            board.setContent("둘리가 등록한 게시글 내용 " + i);
            board.setCreateDate(new Date());
            board.setCnt(0L);
            boardRepo.save(board);
        }



        for(int i = 1; i <= 3; i++) {
            Board board = new Board();
            board.setMember(member2);
            board.setTitle("도우너가 등록한 게시글 " + i);
            board.setContent("도우너가 등록한 게시글 내용 " + i);
            board.setCreateDate(new Date());
            board.setCnt(0L);
            boardRepo.save(board);
        }


    }

}

먼저 두 명의 회원 엔티티를 생성하여 저장하고 나서 회원 정보가 설정된 게시글 엔티티를 여러개 저장했다.

왜냐하면 엔티티를 저장할 때 연관관계에 있는 엔티티가 있다면 해당 엔티티도 영속 상태에 있어야 하기 때문이다.

 

 

새로운 테이블 구조와 데이터를 이용해서 테스트를 진행해야 하므로 테이블이 다시 생성되도록 application.properties 파일을 수정한다.

# JPA Setting
spring.jpa.hibernate.ddlAuto=create

 

테스트를 실행하고 H2 데이터베이스에서 테이블을 조회해보면 BOARD 테이블 외래키에 해당하는 MEMBER_ID 컬럼에 자동으로 MEMBER 테이블의 기본 키 값(MEMBER_ID)이 저장되어 있는 것을 확인할 수 있다.

 

테스트가 정상적으로 끝났으면 다시 ddlAuto를 update로 수정하여 등록된 데이터를 유지한다.

 

 

(2) 게시글 상세 조회 테스트

게시글을 상세 조회하는 테스트 메서드를 작성해본다.

@Test
public void testManyToOneSelect() {

    Board board = boardRepo.findById(3L).get();

    System.out.println("[ " + board.getSeq() + "번 게시글 정보 ]");
    System.out.println("제목 : " + board.getTitle());
    System.out.println("내용 : " + board.getContent());
    System.out.println("작성자 : " + board.getMember().getName());
    System.out.println("작성자 권한 : " + board.getMember().getRole());

}

위에서 작성했던 Board 클래스의 iv인 Member 변수에 @ManyToOne 애너테이션의 fetch 속성 기본값이 EAGER이므로,

실행결과를 보면 연관관계에 있는 회원 정보까지 같이 조회되는 것을 확인할 수 있다.

그리고 BOARD 테이블과 MEMBER 테이블이 외부조인(OUTER JOIN)으로 연결되어 있는 것이 보인다.

외부 조인은 성능상 내부조인보다 좋지 않다. 따라서 반드시 참조 키에 값이 설정된다는 전제가 성립된다면 외부조인을 내부조인(INNER JOIN)으로 변경하는 것이 좋다.

그러므로 Member.member 변수와 매핑되는 MEMBER_ID 칼럼이 항상 참조 값을 가진다는 의미로

@JoinColumn에 nullable 속성을 추가하면 된다.

package com.jpa.practice.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@ToString
@Entity
public class Board {

    @Id
    @GeneratedValue
    private Long seq;
    private String title;
//    private String writer;
    private String content;
    @Temporal(value = TemporalType.TIMESTAMP)
    private Date createDate;
    private Long cnt;

    @ManyToOne 
    @JoinColumn(name = "MEMBER_ID", nullable = false) // name은 컬럼 이름이 된다.
    private Member member;

}

저장 후 다시 테스트를 실행하면 외부조인이 내부조인으로 변경된 것을 확인할 수 있다.

 

2.2 양방향 연관관계 매핑하기

2.2.1 양방향 매핑 설정하기

 

이번에는 반대 방향인 회원에서 게시판 정보를 접근할 수 있도록 관계를 추가하여 양방향 연관관계 매핑을 해본다.

 

회원의 관점에서 게시판과 일대다 관계이고, 일대다 관계는 하나의 객체가 여러 객체와 연관관계를 맺을 수 있으므로

여러 객체를 담을 수 있는 List같은 컬렉션을 사용해야 한다.

 

Member 클래스에 List 타입의 boardList 변수를 추가하고 매핑 애너테이션을 설정해본다.

package com.jpa.practice.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@ToString
@Entity
public class Member {

    @Id
    @Column(name="MEMBER_ID")
    private String id;
    private String password;
    private String name;
    private String role;

    // fetch 속성은 회원 정보를 조회할 때 연관관계에 있는 게시판 정보도 같이 조회할 것인지를 결정할 때 사용
    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Board> boardList = new ArrayList<>();

}

 

@OneToMany는 일대다 관계를 매핑할 때 사용한다.

OneToMany의 기본 fetch 속성은 LAZY인데, 위에서는 EAGER로 설정했으므로 회원정보를 가져올 때 회원이 등록한 게시글 목록도 같이 조회될 것이다.

 

가장 중요한 속성은 mappedBy인데, mappedBy는 양방향 연관관계에서 연관관계의 주인을 지정할 때 사용한다.

 

엔티티를 양방향으로 매핑하려면 매핑에 참여하는 참조변수는 두개인데 외래키는 하나이기 때문에 둘 사이에 차이가 발생한다.

따라서 둘 중 어떤 관계를 사용해서 외래키를 관리할지 결정해야 하는데 이것을 연관관계 주인이라고 한다.

연관관계의 주인을 결정한다는 것은 결국 외래 키 관리자를 선택하는 것이다.

그리고 반대로 연관관계의 주인이 아닌 쪽은 자신이 연관관계의 주인이 아님을 알려줘야 하는데 이 때 사용하는 것이 바로 mappedBy 속성이다. 

 

연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 한다.

여기서는 게시판 테이블이 외래키를 가지고 있으므로 Board.member 변수가 주인이 된다.

반대로 주인이 아닌 Member.boardList에는 mappedBy="member" 속성을 사용해서 주인이 아님을 표시해야 한다.

mappedBy 속성 값으로는 연관관계의 주인인 member를 설정하면 된다.

 

이제 테스트 메서드를 작성해본다.

@Test
public void testTwoWayMapping() {

    Member member = memberRepo.findById("member1").get();

    System.out.println("================");
    System.out.println(member.getName() + "가(이) 저장한 게시글 목록");
    System.out.println("================");

    List<Board> list = member.getBoardList();

    for(Board board : list) {
        System.out.println(board);
    }

}

 

실행해보면 StackOverflowError가 발생한다.

롬복에서 제공하는 @ToString이 양방향 참조에서 상호호출을 했기 때문이다.

Board에서 Member에 대한 ToString을 호출하고, 또 Member에서는 Board에 대한 ToString을 호출하게 되어서 그렇다.

그러므로 @ToString 애너테이션에 exclude 속성을 추가하여 상호 호출을 끊어야 한다.

 

각각 @ToString 부분을 수정한다.

@Getter
@Setter
@ToString(exclude = "member")
@Entity
public class Board {
@Getter
@Setter
@ToString(exclude = "boardList")
@Entity
public class Member {

수정 후 테스트를 실행하면 정상적으로 실행된다.

 

 

2.2.2 영속성 전이

특정 엔티티를 영속 상태로 만들거나 삭제 상태로 만들 때 연관된 엔티티도 같이 처리하고 싶을 때 영속성 전이를 사용하면 관리가 편하다.

JPA는 cascade 속성을 이용하여 부모 엔티티를 저장할 때 자식 엔티티도 같이 저장할 수 있고, 부모 엔티티를 삭제할 때 자식 엔티티도 삭제할 수 있다.

 

(1) 영속성 전이를 이용하여 등록하기

회원과 게시판 관계에서 회원은 PK를 가지고 있는 부모 엔티티며, 게시판은 FK를 가지고 있는 자식 엔티티다.

영속성 전이를 적용하기 위해 부모 엔티티에 해당하는 Member 클래스를 수정한다.

 

@Getter
@Setter
@ToString(exclude = "boardList")
@Entity
public class Member {

    @Id
    @Column(name="MEMBER_ID")
    private String id;
    private String password;
    private String name;
    private String role;

    // fetch 속성은 회원 정보를 조회할 때 연관관계에 있는 게시판 정보도 같이 조회할 것인지를 결정할 때 사용
    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<Board> boardList = new ArrayList<>();

}

영속성 전이와 관련해서 사용할 수 있는 CascadeType은 다음과 같다.

ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH

 

CascadeType.ALL을 적용하면 회원 객체가 영속화되거나 수정, 또는 삭제될 때 회원과 관련된 게시판도 같이 변경될 것이다. 

그리고 게시판 객체에 회원 객체를 설정할 때, 회원이 소유한 게시글 컬렉션에 자신(게시글)도 자동으로 저장될 수 있도록 setMember() 메서드를 추가한다. 이렇게 해야 영속 객체가 아닌 단순한 일반 자바 객체 상태에서도 관련된 데이터를 사용할 수 있다.

package com.jpa.practice.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@ToString(exclude = "member")
@Entity
public class Board {

    @Id
    @GeneratedValue
    private Long seq;
    private String title;
//    private String writer;
    private String content;
    @Temporal(value = TemporalType.TIMESTAMP)
    private Date createDate;
    private Long cnt;

    @ManyToOne
    // name은 컬럼 이름이 된다.
    // nullable은 false로 하면 MEMBER_ID 컬럼이 항상 값을 가진다는 의미.
    // -> 이렇게 설정하면 outer join이 inner join으로 바뀐다.
    @JoinColumn(name="MEMBER_ID", nullable = false)
    private Member member;

    public void setMember(Member member) {
        this.member = member;
        member.getBoardList().add(this); // member를 설정함과 동시에 member의 boardList에 이 게시물을 저장한다.
    }
}

 

이제 testManyToOneInsert() 메서드를 수정한다.

@Test
public void testManyToOneInsert() {
    Member member1 = new Member();
    member1.setId("member1");
    member1.setPassword("member111");
    member1.setName("둘리");
    member1.setRole("User");
//        memberRepo.save(member1);

    Member member2 = new Member();
    member2.setId("member2");
    member2.setPassword("member222");
    member2.setName("도우너");
    member2.setRole("Admin");
//        memberRepo.save(member2);

    for(int i = 1; i <= 3; i++) {
        Board board = new Board();
        board.setMember(member1);
        board.setTitle("둘리가 등록한 게시글 " + i);
        board.setContent("둘리가 등록한 게시글 내용 " + i);
        board.setCreateDate(new Date());
        board.setCnt(0L);
//            boardRepo.save(board);
    }

    memberRepo.save(member1);

    for(int i = 1; i <= 3; i++) {
        Board board = new Board();
        board.setMember(member2);
        board.setTitle("도우너가 등록한 게시글 " + i);
        board.setContent("도우너가 등록한 게시글 내용 " + i);
        board.setCreateDate(new Date());
        board.setCnt(0L);
//            boardRepo.save(board);
    }

    memberRepo.save(member2);

}

앞서 Board 엔티티를 매번 영속화했던 코드를 주석처리하고

Member 객체를 영속화함으로서 Member가 가진 boardList 컬렉션에 저장된 모든 Board 객체도 자동으로 영속화 된다.

 

 

(2) 영속성 전이를 이용하여 삭제하기

만약 부모에 해당하는 특정 회원 객체를 삭제하면 삭제된 회원과 관련된 게시판 엔티티도 자동으로 삭제될 것이다.

 

삭제 기능 테스트 메서드를 추가해본다.

@Test
public void testCascadeDelete() {
    memberRepo.deleteById("member2");
}

아이디가 member2인 회원을 삭제하면 member2가 등록한 모든 Board 게시물이 먼저 삭제되고 나서 회원 정보가 삭제되는 것을 확인할 수 있다.

 

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

 

출처 : 스프링부트 Quick Start

'Spring & SpringBoot > JPA 퀵스타트' 카테고리의 다른 글

JPA 퀵스타트 Part 3  (0) 2023.05.11
JPA 퀵스타트 Part 2  (0) 2023.05.10
JPA 퀵스타트 Part 1  (0) 2023.04.27

+ Recent posts