[스프링 데이터 JPA 퀵 스타트]
스프링 부트는 JPA에 필요한 라이브러리들과 복잡한 XML 설정을 자동으로 처리하기 위해 JPA 스타터를 제공한다.
1. 스프링 데이터 JPA 사용하기
프로젝트를 생성하여 스프링 데이터 JPA를 사용해보자
*JPA 기본설정
application.properties 파일에 데이터 소스, JPA, 로깅 설정을 반영한다.
# DatsSource Setting
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
# JPA Setting
spring.jpa.hibernate.ddl-auto=create
spring.jpa.generate-ddl=false
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# JPA 구현체로 사용할 하이버네이트가 생성한 sql을 콘솔에 출력할지 여부
spring.jpa.properties.hibernate.format_sql=true
# Logging Setting
logging.level.org.hibernate=info
*엔티티 매핑과 리포지터리 작성하기
spring.jpa.hibernate.ddl-auto=create로 설정했으므로, 엔티티를 기준으로 테이블이 자동생성 된다.
매핑할 테이블 이름이 엔티티 클래스이름과 동일하므로 @Table 생략.
현재 실습에 사용중인 DB는 H2이므로 시퀀스 전략을 기본을 사용한다.
package com.studyboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Chapter05Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Chapter05Application.class);
// 웹 어플리케이션이 아닌 일반 자바 어플리케이션으로 실행
// WebApplicationType.NONE 으로 설정했으므로 내장 톰캣을 구동하지 않고 실행
app.setWebApplicationType(WebApplicationType.NONE);
app.run(args);
}
}
createDate 변수가 테이블에 매핑될 때 칼럼 이름에 자동으로 '_'가 추가된다.
createDate -> 칼럼명 CREATE_DATE
만약 변수 이름이 createdate -> 칼럼명 CREATEDATE
* Repository 인터페이스 작성
Repository 인터페이스: 엔티티를 이용하여 CRUD 기능을 처리 한다. 기존의 DAO(Data Access Object) 와 동일한 개념. 비즈니스 클래스에서는 Repository를 이용해서 실질적인 DB 연동을 처리한다.
Repository : Spring Data 모듈에서 제공. 상속구조에서 가장 상위에 있음.
CrudRepository : 일반적으로 주로 사용. 기본적인 CRUD 기능 제공.
PagingAndSortingRepository : 검색 기능, 검색 화면에 대한 페이징 처리시 사용
JPARepository : 스프링 데이터 JPA에서 추가된 기능이 필요시 사용
모든 Repository 인터페이스는 공통적으로 두 개의 제네릭 타입을 지정해야 한다.
CrudRepository<classT, ID>
classT : 엔티티의 클래스 타입
ID : 식별자 타입(@Id로 매핑한 식별자 변수 타입)
일반적으로, 인터페이스 정의 = 인터페이스를 구현한 클래스를 만들어 사용하겠다는 의미
인터페이스는 객체로 생성할 수 없고 다른 클래스의 부모로만 사용되기 때문이다. 하지만, 스프링 데이터 JPA를 사용할 경우 별도의 구현 클래스를 만들지 않고 인터페이스만 정의함으로써 기능 사용이 가능하다. 스프링 부트가 내부적으로 인터페이스에 대한 구현 객체를 자동으로 생성해준다.
또한 JPA 를 사용하기 위해 EntityManagerFactory, EntityManager, EntityTransaction 같은 객체 생성도 생략된다. 이 객체들의 생성/활용이 JPA 내부적으로 처리된다.
package com.studyboot.persistence;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Board;
public interface BoardRepository extends CrudRepository<Board, Long>{
}
CRUD 테스트 -등록기능 테스트
package com.studyboot;
import java.util.Date;
import org.junit.jupiter.api.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 com.studyboot.domain.Board;
import com.studyboot.persistence.BoardRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepo;
/* 등록 기능 테스트 */
@Test
public void testInserBoard() {
Board b = new Board();
b.setTitle("첫번째 게시물");
b.setWriter("테스터");
b.setContent("잘 등록되는가?");
b.setCreateDate(new Date());
b.setCnt(0L);
// JPA persist 메소드와 동일한 역할
boardRepo.save(b);
}
}
spring.jpa.hibernate.ddl-auto=update 설정
시퀀스로부터 일련번호를 얻어서 게시글을 등록하는 SQL이 실행된다.
CRUD 테스트 - 상세 조회 기능 테스트
@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepo;
/* 상세 조회 기능 테스트 */
@Test
public void testGetBoard() {
Board b = boardRepo.findById(1L).get();
//데이터 하나 조회.
//Optional 타입의 객체를 get메소드를 이용해서 영속성 컨텍스트에 저장된 Board 객체로 받아냄
System.out.println(b.toString());
}
}
Hibernate:
select
board0_.seq as seq1_0_0_,
board0_.cnt as cnt2_0_0_,
board0_.content as content3_0_0_,
board0_.create_date as create_d4_0_0_,
board0_.title as title5_0_0_,
board0_.writer as writer6_0_0_
from
board board0_
where
board0_.seq=?
Board(seq=1, title=첫번째 게시물, writer=테스터, content=잘 등록되는가?, createDate=2019-12-27, cnt=0)
BoardRepositoryTest
CRUD 테스트 - 수정 기능 테스트
@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepo;
/* 수정 기능 테스트 */
@Test
public void testUpdateBoard() {
System.out.println("===== 1번 게시글 조회 =====");
Board b = boardRepo.findById(1L).get(); // 영속성 컨텍스트에 올리기 위해 조회
System.out.println("===== 1번 게시글 제목 수정 =====");
b.setTitle("수정한 제목 입니당"); // 수정할 값 설정
boardRepo.save(b);// 수정 작업 반영
}
}
===== 1번 게시글 조회 =====
Hibernate:
select
board0_.seq as seq1_0_0_,
board0_.cnt as cnt2_0_0_,
board0_.content as content3_0_0_,
board0_.create_date as create_d4_0_0_,
board0_.title as title5_0_0_,
board0_.writer as writer6_0_0_
from
board board0_
where
board0_.seq=?
===== 1번 게시글 제목 수정 =====
Hibernate:
select
board0_.seq as seq1_0_0_,
board0_.cnt as cnt2_0_0_,
board0_.content as content3_0_0_,
board0_.create_date as create_d4_0_0_,
board0_.title as title5_0_0_,
board0_.writer as writer6_0_0_
from
board board0_
where
board0_.seq=?
Hibernate:
update board set cnt=?, content=?, create_date=?, title=?, writer=? where seq=?
BoardRepositoryTest
CRUD 테스트 - 삭제 기능 테스트
@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepo;
/* 삭제 기능 테스트 */
@Test
public void testDeleteBoard() {
boardRepo.deleteById(1L);
}
}
Hibernate:
select
board0_.seq as seq1_0_0_,
board0_.cnt as cnt2_0_0_,
board0_.content as content3_0_0_,
board0_.create_date as create_d4_0_0_,
board0_.title as title5_0_0_,
board0_.writer as writer6_0_0_
from
board board0_
where
board0_.seq=?
Hibernate:
delete from board where seq=?
2. 쿼리 메소드 사용하기
쿼리메소드 : 메소드의 이름으로 필요한 쿼리를 만들어 주는 기능.
JPQL(Java Persistence Query Language) : 검색 대상이 테이블이 아닌 엔티티라는 것만 제외하고는 기본구조와 문법이 SQL과 유사. 엔티티를 대상으로 검색을 처리해야 하므로 다소 복잡하고 어려움.
쿼리메소드 네이밍룰
find + 엔티티명 + By + 변수명 > findBoardByTitle() : Board 엔티티에서 title 변수 값만 조회.
쿼리 메소드 작성시 엔티티명 생략 가능. 생략 시 현재 사용하는 Repository 인터페이스에서 선언된 타입 정보를 기줁으로 자동으로 엔티티 이름 적용.
find + By + 변수명 > findByTitle() : Board 엔티티에서 title 변수 값만 조회.
쿼리메소드 리턴 타입
Page<T>, Slice<T> , List<T>이며 모두 Collection<T> 타입이다.
가장 많이 사용하는 것은 Page<T>, List<T>이다. Page<T>는 페이징 처리, List<T>는 단순히 목록 검색에 쓰인다.
쿼리메소드 구현해보기
package com.studyboot.persistence;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Board;
public interface BoardRepository extends CrudRepository<Board, Long>{
// 쿼리메소드 구현
List<Board> findByTitle(String searchKeyword);
}
package com.studyboot;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.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.Sort;
import org.springframework.test.context.junit4.SpringRunner;
import com.studyboot.domain.Board;
import com.studyboot.persistence.BoardRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
public class QueryMethodTest {
@Autowired
private BoardRepository boardRepo;
//테스트 메소드가 실행되기 전에 동작하여 테스트에서 사용할 데이터를 세팅
@BeforeEach
public void dataPrepare() {
for(int i=1; i<=200; i++) {
Board b = new Board();
b.setTitle("테스트 제목"+i);
b.setWriter("테스터");
b.setContent("테스트 내용"+i);
b.setCreateDate(new Date());
b.setCnt(0L);
boardRepo.save(b);
}
1
@Test
public void testFindByTitle() {
List<Board> boardList = boardRepo.findByTitle("테스트 제목10");
System.out.println("검색 결과");
for(Board board : boardList) {
System.out.println("------>" + board.toString());
}
}
}
Hibernate: insert into board (cnt, content, create_date, title, writer, seq) values (?, ?, ?, ?, ?, ?)
Hibernate: call next value for hibernate_sequence
Hibernate: insert into board (cnt, content, create_date, title, writer, seq) values (?, ?, ?, ?, ?, ?)
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.title=?
검색 결과
------>Board(seq=11, title=테스트 제목10, writer=테스터, content=테스트 내용10, createDate=2019-12-27, cnt=0)
# 다양한 쿼리메소드 키워드
Logical keywordKeyword expressions
AND |
And |
OR |
Or |
AFTER |
After, IsAfter |
BEFORE |
Before, IsBefore |
CONTAINING |
Containing, IsContaining, Contains |
BETWEEN |
Between, IsBetween |
ENDING_WITH |
EndingWith, IsEndingWith, EndsWith |
EXISTS |
Exists |
FALSE |
False, IsFalse |
GREATER_THAN |
GreaterThan, IsGreaterThan |
GREATER_THAN_EQUALS |
GreaterThanEqual, IsGreaterThanEqual |
IN |
In, IsIn |
IS |
Is, Equals, (or no keyword) |
IS_NOT_NULL |
NotNull, IsNotNull |
IS_NULL |
Null, IsNull |
LESS_THAN |
LessThan, IsLessThan |
LESS_THAN_EQUAL |
LessThanEqual, IsLessThanEqual |
LIKE |
Like, IsLike |
NEAR |
Near, IsNear |
NOT |
Not, IsNot |
NOT_IN |
NotIn, IsNotIn |
NOT_LIKE |
NotLike, IsNotLike |
REGEX |
Regex, MatchesRegex, Matches |
STARTING_WITH |
StartingWith, IsStartingWith, StartsWith |
TRUE |
True, IsTrue |
WITHIN |
Within, IsWithin |
출처: <https://arahansa.github.io/docs_spring/jpa.html#repositories.query-methods.query-creation>
LIKE 연산자 테스트
public interface BoardRepository extends CrudRepository<Board, Long>{
// LIKE 연산자
List<Board> findByContentContaining(String searchKeyword);
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class QueryMethodTest {
@Autowired
private BoardRepository boardRepo;
@Test // LIKE 연산자
public void testFindByContentContaining() {
List<Board> boardList = boardRepo.findByContentContaining("17");
System.out.println("검색 결과");
for(Board board : boardList) {
System.out.println("------>" + board.toString());
}
}
}
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.content like ? escape ?
검색 결과
------>Board(seq=18, title=테스트 제목17, writer=테스터, content=테스트 내용17, createDate=2019-12-27, cnt=0)
------>Board(seq=118, title=테스트 제목117, writer=테스터, content=테스트 내용117, createDate=2019-12-27, cnt=0)
------>Board(seq=171, title=테스트 제목170, writer=테스터, content=테스트 내용170, createDate=2019-12-27, cnt=0)
------>Board(seq=172, title=테스트 제목171, writer=테스터, content=테스트 내용171, createDate=2019-12-27, cnt=0)
------>Board(seq=173, title=테스트 제목172, writer=테스터, content=테스트 내용172, createDate=2019-12-27, cnt=0)
------>Board(seq=174, title=테스트 제목173, writer=테스터, content=테스트 내용173, createDate=2019-12-27, cnt=0)
------>Board(seq=175, title=테스트 제목174, writer=테스터, content=테스트 내용174, createDate=2019-12-27, cnt=0)
------>Board(seq=176, title=테스트 제목175, writer=테스터, content=테스트 내용175, createDate=2019-12-27, cnt=0)
------>Board(seq=177, title=테스트 제목176, writer=테스터, content=테스트 내용176, createDate=2019-12-27, cnt=0)
------>Board(seq=178, title=테스트 제목177, writer=테스터, content=테스트 내용177, createDate=2019-12-27, cnt=0)
------>Board(seq=179, title=테스트 제목178, writer=테스터, content=테스트 내용178, createDate=2019-12-27, cnt=0)
------>Board(seq=180, title=테스트 제목179, writer=테스터, content=테스트 내용179, createDate=2019-12-27, cnt=0)
여러 조건 사용하기
특정 검색어가 포함된 게시글을 검색하는 경우 AND , OR 를 사용한다.
Where 절에 들어갈 여러 조건을 메소드명으로 표현하므로 이름이 길어질 수 밖에 없다.
두 개의 변수에 대해 제약 조건을 추가했기 때문에 검색 값이 동일하더라도 매개변수는 반드시 두개여야한다.
public interface BoardRepository extends CrudRepository<Board, Long>{
// 여러 조건 사용
List<Board> findByTitleContainingOrContentContaining(String title, String content);
}
QueryMethodTest.class
@Test // 여러 조건
public void testFindByTitleContainigOrContentContaining() {
List<Board> boardList = boardRepo.findByTitleContainingOrContentContaining("17","17");
System.out.println("검색 결과");
for(Board board : boardList) {
System.out.println("------>" + board.toString());
}
}
실행결과로 출력된 쿼리를 확인하면 where절에 두개의 조건이 OR연산으로 결합된 것을 확인할 수 있다.
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.title like ? escape ? or board0_.content like ? escape ?
검색 결과
------>Board(seq=18, title=테스트 제목17, writer=테스터, content=테스트 내용17, createDate=2019-12-27, cnt=0)
------>Board(seq=118, title=테스트 제목117, writer=테스터, content=테스트 내용117, createDate=2019-12-27, cnt=0)
------>Board(seq=171, title=테스트 제목170, writer=테스터, content=테스트 내용170, createDate=2019-12-27, cnt=0)
------>Board(seq=172, title=테스트 제목171, writer=테스터, content=테스트 내용171, createDate=2019-12-27, cnt=0)
------>Board(seq=173, title=테스트 제목172, writer=테스터, content=테스트 내용172, createDate=2019-12-27, cnt=0)
------>Board(seq=174, title=테스트 제목173, writer=테스터, content=테스트 내용173, createDate=2019-12-27, cnt=0)
------>Board(seq=175, title=테스트 제목174, writer=테스터, content=테스트 내용174, createDate=2019-12-27, cnt=0)
------>Board(seq=176, title=테스트 제목175, writer=테스터, content=테스트 내용175, createDate=2019-12-27, cnt=0)
------>Board(seq=177, title=테스트 제목176, writer=테스터, content=테스트 내용176, createDate=2019-12-27, cnt=0)
------>Board(seq=178, title=테스트 제목177, writer=테스터, content=테스트 내용177, createDate=2019-12-27, cnt=0)
------>Board(seq=179, title=테스트 제목178, writer=테스터, content=테스트 내용178, createDate=2019-12-27, cnt=0)
------>Board(seq=180, title=테스트 제목179, writer=테스터, content=테스트 내용179, createDate=2019-12-27, cnt=0)
데이터를 정렬해서 조회하기 위해서는 OrderBy + 변수명 + Asc Or Desc 를 이용한다.
public interface BoardRepository extends CrudRepository<Board, Long>{
// 데이터 정렬
List<Board> findByTitleContainingOrderBySeqDesc(String searchKeyword);
}
title이 17이 포함된 게시글을 검색해서 seq를 내림차순으로 정렬하여 출력했다.
QueryMethodTest.class
@Test // 데이터 정렬
public void testFindByTitleContainingOrderBySeqDesc() {
List<Board> boardList = boardRepo.findByTitleContainingOrderBySeqDesc("17");
System.out.println("검색 결과");
for(Board board : boardList) {
System.out.println("------>" + board.toString());
}
}
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.title like ? escape ? order by board0_.seq desc
검색 결과
------>Board(seq=180, title=테스트 제목179, writer=테스터, content=테스트 내용179, createDate=2019-12-27, cnt=0)
------>Board(seq=179, title=테스트 제목178, writer=테스터, content=테스트 내용178, createDate=2019-12-27, cnt=0)
------>Board(seq=178, title=테스트 제목177, writer=테스터, content=테스트 내용177, createDate=2019-12-27, cnt=0)
------>Board(seq=177, title=테스트 제목176, writer=테스터, content=테스트 내용176, createDate=2019-12-27, cnt=0)
------>Board(seq=176, title=테스트 제목175, writer=테스터, content=테스트 내용175, createDate=2019-12-27, cnt=0)
------>Board(seq=175, title=테스트 제목174, writer=테스터, content=테스트 내용174, createDate=2019-12-27, cnt=0)
------>Board(seq=174, title=테스트 제목173, writer=테스터, content=테스트 내용173, createDate=2019-12-27, cnt=0)
------>Board(seq=173, title=테스트 제목172, writer=테스터, content=테스트 내용172, createDate=2019-12-27, cnt=0)
------>Board(seq=172, title=테스트 제목171, writer=테스터, content=테스트 내용171, createDate=2019-12-27, cnt=0)
------>Board(seq=171, title=테스트 제목170, writer=테스터, content=테스트 내용170, createDate=2019-12-27, cnt=0)
------>Board(seq=118, title=테스트 제목117, writer=테스터, content=테스트 내용117, createDate=2019-12-27, cnt=0)
------>Board(seq=18, title=테스트 제목17, writer=테스터, content=테스트 내용17, createDate=2019-12-27, cnt=0)
페이징과 정렬 처리하기
public interface BoardRepository extends CrudRepository<Board, Long>{
// 페이징 처리
List<Board> findByTitleContaining(String searchKeyword, Pageable paging);
}
QueryMethodTest.class
// 페이징 처리
@Test
public void testFindByTitleContaining() {
// 페이지 번호 0 부터 조회. 검색할 데이터 개수 5개씩 조회
//Pageable paging = PageRequest.of(0, 5);
// 페이징 처리시 정렬을 해야한다면 Sort클래스 사용.
// 첫번째 매개변수는 정렬 방향에 대한 정보. 두번째 매개변수는 정렬 대상이 되는 변수명
Pageable paging = PageRequest.of(0, 5, Sort.Direction.DESC, "seq");
List<Board> boardList = boardRepo.findByTitleContaining("제목",paging);
System.out.println("검색 결과");
for(Board board : boardList) {
System.out.println("------>" + board.toString());
}
}
콘솔에 출력되는 페이징 관련 SQL과 결과
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.title like ? escape ? limit ?
검색 결과
------>Board(seq=2, title=테스트 제목1, writer=테스터, content=테스트 내용1, createDate=2019-12-27, cnt=0)
------>Board(seq=3, title=테스트 제목2, writer=테스터, content=테스트 내용2, createDate=2019-12-27, cnt=0)
------>Board(seq=4, title=테스트 제목3, writer=테스터, content=테스트 내용3, createDate=2019-12-27, cnt=0)
------>Board(seq=5, title=테스트 제목4, writer=테스터, content=테스트 내용4, createDate=2019-12-27, cnt=0)
------>Board(seq=6, title=테스트 제목5, writer=테스터, content=테스트 내용5, createDate=2019-12-27, cnt=0)
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.title like ? escape ? order by board0_.seq desc limit ?
검색 결과
------>Board(seq=201, title=테스트 제목200, writer=테스터, content=테스트 내용200, createDate=2019-12-27, cnt=0)
------>Board(seq=200, title=테스트 제목199, writer=테스터, content=테스트 내용199, createDate=2019-12-27, cnt=0)
------>Board(seq=199, title=테스트 제목198, writer=테스터, content=테스트 내용198, createDate=2019-12-27, cnt=0)
------>Board(seq=198, title=테스트 제목197, writer=테스터, content=테스트 내용197, createDate=2019-12-27, cnt=0)
------>Board(seq=197, title=테스트 제목196, writer=테스터, content=테스트 내용196, createDate=2019-12-27, cnt=0)
Page<T> 타입 사용하기
스프링MVC에서 검색 결과를 사용할 목적이라면 Page<T>를 사용하는 것이 좋다. Page<T> 객체는 페이징 처리할 때 사용할 수 있는 다양한 정보를 추가로 제공하기 때문이다.
public interface BoardRepository extends CrudRepository<Board, Long>{
// 페이징 처리
Page<Board> findBoardByTitleContaining(String searchKeyword, Pageable paging);
}
QueryMethodTest.class
// 페이징 처리
@Test
public void testFindBoardByTitleContaining() {
Pageable paging = PageRequest.of(0, 5, Sort.Direction.DESC, "seq");
Page<Board> pageInfo = boardRepo.findBoardByTitleContaining("제목",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.toString());
}
}
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_, board0_.writer as writer6_0_ from board board0_ where board0_.title like ? escape ? order by board0_.seq desc limit ?
Hibernate: select count(board0_.seq) as col_0_0_ from board board0_ where board0_.title like ? escape ?
PAGE SIZE5
TOTAL PAGES40
TOTAL COUNT200
NEXTPage request [number: 1, size 5, sort: seq: DESC]
검색 결과
------>Board(seq=201, title=테스트 제목200, writer=테스터, content=테스트 내용200, createDate=2019-12-27, cnt=0)
------>Board(seq=200, title=테스트 제목199, writer=테스터, content=테스트 내용199, createDate=2019-12-27, cnt=0)
------>Board(seq=199, title=테스트 제목198, writer=테스터, content=테스트 내용198, createDate=2019-12-27, cnt=0)
------>Board(seq=198, title=테스트 제목197, writer=테스터, content=테스트 내용197, createDate=2019-12-27, cnt=0)
------>Board(seq=197, title=테스트 제목196, writer=테스터, content=테스트 내용196, createDate=2019-12-27, cnt=0)
Page 클래스가 제공하는 인터페이스
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getTotalPages(); //전체 페이지 수
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
long getTotalElements(); //전체 데이터 수
boolean hasPreviousPage(); //이전 페이지 여부
boolean isFirstPage(); //현재 페이지가 첫 페이지 인지 여부
boolean hasNextPage(); //다음 페이지 여부
boolean isLastPage(); //현재 페이지가 마지막 페이지 인지 여부
Pageable nextPageable(); //다음 페이지 객체, 다음 페이지가 없으면 null
Pageable previousPageable(); //다음 페이지 객체, 이전 페이지가 없으면 null
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬정보
출처: <https://ithub.tistory.com/28>
3. @Query 어노테이션 사용하기
성능 상 어쩔 수 없이 특정 데이터베이스에 종속적인 네이티브 쿼리를 사용할때 @Query 어노테이션을 사용한다. JPQL은 검색대상이 테이블이 아니라 영속성 컨텍스트에 등록된 엔티티다. FROM절에 엔티티 이름을 대소문자를 구분해서 정확하게 지정해야 한다. 칼럼 대신 엔티티가 가지고 있는 변수를 조회하기 때문에 SELECT나 WHERE절에서 사용하는 변수 이름 역시 대소문자를 구분해야 한다.
위치기반 파라미터
‘?1’는 첫번째 파라미터를 의미한다
BoardRepository.interface
// 위치 기반 파라미터 사용
@Query("SELECT b FROM Board b WHERE b.title LIKE %?1% ORDER BY b.seq DESC")
List<Board> queryAnnotationTest1(String searchKeyword);
@RunWith(SpringRunner.class)
@SpringBootTest
public class QueryAnnotationTest {
@Autowired
private BoardRepository boardRepo;
@Test
public void testQueryAnnotationTest1() {
List<Board> boardList = boardRepo.queryAnnotationTest1("테스트 제목10");
System.out.println("검색 결과");
for(Board b : boardList){
System.out.println("--------> "+b.toString());
}
}
}
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.title like ?
order by
board0_.seq DESC
검색 결과
--------> Board(seq=109, title=테스트 제목109, writer=테스터, content=테스트 내용109, createDate=2019-12-31, cnt=0)
--------> Board(seq=108, title=테스트 제목108, writer=테스터, content=테스트 내용108, createDate=2019-12-31, cnt=0)
--------> Board(seq=107, title=테스트 제목107, writer=테스터, content=테스트 내용107, createDate=2019-12-31, cnt=0)
--------> Board(seq=106, title=테스트 제목106, writer=테스터, content=테스트 내용106, createDate=2019-12-31, cnt=0)
--------> Board(seq=105, title=테스트 제목105, writer=테스터, content=테스트 내용105, createDate=2019-12-31, cnt=0)
--------> Board(seq=104, title=테스트 제목104, writer=테스터, content=테스트 내용104, createDate=2019-12-31, cnt=0)
--------> Board(seq=103, title=테스트 제목103, writer=테스터, content=테스트 내용103, createDate=2019-12-31, cnt=0)
--------> Board(seq=102, title=테스트 제목102, writer=테스터, content=테스트 내용102, createDate=2019-12-31, cnt=0)
--------> Board(seq=101, title=테스트 제목101, writer=테스터, content=테스트 내용101, createDate=2019-12-31, cnt=0)
--------> Board(seq=100, title=테스트 제목100, writer=테스터, content=테스트 내용100, createDate=2019-12-31, cnt=0)
--------> Board(seq=10, title=테스트 제목10, writer=테스터, content=테스트 내용10, createDate=2019-12-31, cnt=0)
이름기반 파라미터
파라미터명이 동일한 값이 바인딩 되도록 @Param 어노테이션을 추가했다.
BoardRepository.interface
// 이름 기반 파라미터 사용
@Query("SELECT b FROM Board b WHERE b.title LIKE %:searchKeyword% ORDER BY b.seq DESC")
List<Board> queryAnnotationTest1(String searchKeyword);
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
oard0_.writer as writer6_0_
from
board board0_
where
board0_.title like ?
order by
board0_.seq DESC
검색 결과
--------> Board(seq=109, title=테스트 제목109, writer=테스터, content=테스트 내용109, createDate=2019-12-31, cnt=0)
--------> Board(seq=108, title=테스트 제목108, writer=테스터, content=테스트 내용108, createDate=2019-12-31, cnt=0)
--------> Board(seq=107, title=테스트 제목107, writer=테스터, content=테스트 내용107, createDate=2019-12-31, cnt=0)
--------> Board(seq=106, title=테스트 제목106, writer=테스터, content=테스트 내용106, createDate=2019-12-31, cnt=0)
--------> Board(seq=105, title=테스트 제목105, writer=테스터, content=테스트 내용105, createDate=2019-12-31, cnt=0)
--------> Board(seq=104, title=테스트 제목104, writer=테스터, content=테스트 내용104, createDate=2019-12-31, cnt=0)
--------> Board(seq=103, title=테스트 제목103, writer=테스터, content=테스트 내용103, createDate=2019-12-31, cnt=0)
--------> Board(seq=102, title=테스트 제목102, writer=테스터, content=테스트 내용102, createDate=2019-12-31, cnt=0)
--------> Board(seq=101, title=테스트 제목101, writer=테스터, content=테스트 내용101, createDate=2019-12-31, cnt=0)
--------> Board(seq=100, title=테스트 제목100, writer=테스터, content=테스트 내용100, createDate=2019-12-31, cnt=0)
--------> Board(seq=10, title=테스트 제목10, writer=테스터, content=테스트 내용10, createDate=2019-12-31, cnt=0)
특정변수만 조회하기
검색 결과로 엔티티 객체가 조회되는 것이 아니라 여러 변수 값들이 조회된다. 리턴 타입을 List<Obejct[]>로 해야한다. 리스트에 저장된 배열 객체를 꺼내서 출력하는 형식 사용
출력 결과를 보면 @query를 설정한대로 특정 컬럼 값만 조회된 것을 확인할 수 있다.
BoardRepository.interface
// 특정변수만 조회하기
@Query("SELECT b.seq, b.title, b.writer, b.createDate FROM Board b WHERE b.title LIKE %?1% ORDER BY b.seq DESC")
List<Object[]> queryAnnotationTest2(@Param("searchKeyword") String searchKeyword);
QueryAnnotationTest.class
@Test
public void testQueryAnnotationTest2() {
List<Object[]> boardList = boardRepo.queryAnnotationTest2("테스트 제목10");
System.out.println("검색 결과");
for(Object[] row : boardList){
System.out.println("--------> "+Arrays.toString(row));
}
}
Hibernate:
select
board0_.seq as col_0_0_,
board0_.title as col_1_0_,
board0_.writer as col_2_0_,
board0_.create_date as col_3_0_
from
board board0_
where
board0_.title like ?
order by
board0_.seq DESC
검색 결과
--------> [109, 테스트 제목109, 테스터, 2019-12-31]
--------> [108, 테스트 제목108, 테스터, 2019-12-31]
--------> [107, 테스트 제목107, 테스터, 2019-12-31]
--------> [106, 테스트 제목106, 테스터, 2019-12-31]
--------> [105, 테스트 제목105, 테스터, 2019-12-31]
--------> [104, 테스트 제목104, 테스터, 2019-12-31]
--------> [103, 테스트 제목103, 테스터, 2019-12-31]
--------> [102, 테스트 제목102, 테스터, 2019-12-31]
--------> [101, 테스트 제목101, 테스터, 2019-12-31]
--------> [100, 테스트 제목100, 테스터, 2019-12-31]
--------> [10, 테스트 제목10, 테스터, 2019-12-31]
@Query 주의사항
@Query로 등록한 SQL은 프로젝트가 로딩되는 시점에 파싱되어 처리된다. SQL에 오류가 있을 경우 무조건 예외가 발생되고 프로그램이 실행되지 않는다. 프로그램이 실행되기 전에 사용할 SQL들을 모두 메모리에 올려둠으로써 성능을 향상시킨다. 따라서 @Query를 사용할 때에는 사용할 쿼리를 한 번에 모두 등록하지 말고 JPQL에 오류가 없는지 하나씩 확인하면서 등록하는 것이 좋다.
네이티브 쿼리 사용하기
네이티브 쿼리를 쓸 경우 쿼리가 특정 데이터베이스에 종속되는 문제가 있지만, 성능상 특정 데이터베이스에 최적화된 쿼리를 사용해야 하는 경우에는 유용하게 쓸 수 있다.
nativeQuery=true 쿼리가 JPQL가 아닌 네이티브 쿼리임을 알려주는 프로퍼티 값을 설정
BoardRepository.interface
// 네이티브 쿼리 (엔티티 기준이 아니라 컬럼명 기준이므로 주의하자)
@Query(value="SELECT seq, title, writer, create_date FROM Board WHERE title LIKE '%'||?1||'%' ORDER BY seq DESC"
,nativeQuery = true)
List<Object[]> queryAnnotationTest3(String searchKeyword);
QueryAnnotationTest.class
@Test
public void testQueryAnnotationTest3() {
List<Object[]> boardList = boardRepo.queryAnnotationTest3("테스트 제목10");
System.out.println("검색 결과");
for(Object[] row : boardList){
System.out.println("--------> "+Arrays.toString(row));
}
}
Hibernate:
SELECT
seq, title, writer, create_date
FROM
Board
WHERE
title LIKE '%'||?||'%'
ORDER BY seq DESC
검색 결과
--------> [109, 테스트 제목109, 테스터, 2019-12-31]
--------> [108, 테스트 제목108, 테스터, 2019-12-31]
--------> [107, 테스트 제목107, 테스터, 2019-12-31]
--------> [106, 테스트 제목106, 테스터, 2019-12-31]
--------> [105, 테스트 제목105, 테스터, 2019-12-31]
--------> [104, 테스트 제목104, 테스터, 2019-12-31]
--------> [103, 테스트 제목103, 테스터, 2019-12-31]
--------> [102, 테스트 제목102, 테스터, 2019-12-31]
--------> [101, 테스트 제목101, 테스터, 2019-12-31]
--------> [100, 테스트 제목100, 테스터, 2019-12-31]
--------> [10, 테스트 제목10, 테스터, 2019-12-31]
페이징 및 정렬 처리하기
@Query도 페이징 처리를 위한 Pageable 인터페이스는 쿼리 메소드와 동일하게 사용할 수 있다.
BoardRepository.interface
// 페이징 및 정렬 사용
@Query("SELECT b FROM Board b ORDER BY b.seq DESC")
List<Board> queryAnnotationTest4(Pageable paging);
QueryAnnotationTest.class
@Test
public void testQueryAnnotationTest4() {
Pageable paging = PageRequest.of(0, 3, Sort.Direction.DESC, "seq");
//첫번째 페이지부터 세개의 데이터씩, 정렬은 seq기준으로 내림차순
List<Board> boardList = boardRepo.queryAnnotationTest4(paging);
System.out.println("검색 결과");
for(Board b : boardList){
System.out.println("--------> "+b.toString());
}
}
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
order by
board0_.seq DESC,
board0_.seq desc limit ?
검색 결과
--------> Board(seq=200, title=테스트 제목200, writer=테스터, content=테스트 내용200, createDate=2019-12-31, cnt=0)
--------> Board(seq=199, title=테스트 제목199, writer=테스터, content=테스트 내용199, createDate=2019-12-31, cnt=0)
--------> Board(seq=198, title=테스트 제목198, writer=테스터, content=테스트 내용198, createDate=2019-12-31, cnt=0)
4. QueryDSL을 이용한 동적 쿼리 적용하기
JPA에서는 @Query를 이용해서 애플리케이션에 사용할 쿼리를 관리. @Query는 프로젝트가 로딩되는 시점에 파싱되서 고정된 SQL만 쓸 수 있다. QueryDSL은 마이바티스처럼 동적으로 쿼리를 처리하게 해준다. 오픈소스 프로젝트로서 쿼리를 문자열이 아닌 자바 코드로 작성할 수 있도록 지원하는 일종의 JPQL 빌더.
의존성 추가
Duplicating managed version 4.2.2 for querydsl-jpa 가 뜨는 이유는 <parent>에서 이미 버전 정보에 대한 설정을 해놨기 때문이다.
pom.xml
<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>4.2.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-apt -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.2.2</version>
</dependency>
플러그인 추가
pom.xml
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</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>
You need to run build with JDK or have tools.jar on the classpath.If this occures during eclipse build make sure you run eclipse under JDK as well
자바 환경변수 경로를 설정
-vm %JAVA_HOME/bin/javaw.exe -vmargs
동적쿼리 사용하기
동적쿼리를 사용하기 위해서는 QuerydslPredicateExecutor를 상속 받아야 한다. DynamicBoardRepository를 추가로 생성했다.
package com.studyboot.persistence;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Board;
public interface DynamicBoardRepository extends CrudRepository<Board, Long>
, QuerydslPredicateExecutor<Board>{
}
count() |
검색할 데이터의 전체 개수 long |
exists() |
검색된 데이터의 존재 여부 boolean |
findAll() |
조건에 맞는 모든 데이터 목록 Iterable<T> |
findAll() |
조건에 맞는 모든 데이터 목록 Page<T> |
findAll() |
조건에 맞는 모든 데이터 목록 정렬 Iterable<T> |
findOne() |
조건에 맞는 하나의 데이터 T |
QuerydslPredicateExecutor 에서 제공하는 메소드들은 공통적으로 Predicate타입의 객체를 매개변수로 받고 있다. Predicate 인터페이스를 구현한 클래스가 BooleanBuilder 클래스
BooleanBuilder는 가변적인 파라미터 값에 따라 동적으로 and, or 조건을 추가할 수있다.
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicQueryTest {
@Autowired
private DynamicBoardRepository boardRepo;
@Test
public void testDynamicQuery() {
String searchCondition = "TITLE";
String searchKeyword = "테스트 제목10";
BooleanBuilder builder = new BooleanBuilder(); //가변적인 파라미터 값에 따라 동적으로 AND나 OR조건 반영 가능
QBoard qBaord = QBoard.board;
//검색 조건에 따른 분기처리
if (searchCondition.equals("TITLE")) {
builder.and(qBaord.title.like("%"+searchKeyword+"%"));
} else if (searchCondition.equals("CONTENT")) {
builder.and(qBaord.content.like("%"+searchKeyword+"%"));
}
// 페이징 처리를 위해 pagable 객체 생성
Pageable paging = PageRequest.of(0, 5);
Page<Board> boardList = boardRepo.findAll(builder, paging);
System.out.println("검색 결과");
for(Board board : boardList) {
System.out.println("------>" + board.toString());
}
}
}
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.title like ? escape '!' limit ?
Hibernate:
select
count(board0_.seq) as col_0_0_
from
board board0_
where
board0_.title like ? escape '!'
검색 결과
------>Board(seq=10, title=테스트 제목10, writer=테스터, content=테스트 내용10, createDate=2019-12-31, cnt=0)
------>Board(seq=100, title=테스트 제목100, writer=테스터, content=테스트 내용100, createDate=2019-12-31, cnt=0)
------>Board(seq=101, title=테스트 제목101, writer=테스터, content=테스트 내용101, createDate=2019-12-31, cnt=0)
------>Board(seq=102, title=테스트 제목102, writer=테스터, content=테스트 내용102, createDate=2019-12-31, cnt=0)
------>Board(seq=103, title=테스트 제목103, writer=테스터, content=테스트 내용103, createDate=2019-12-31, cnt=0)
조건을 변경하여 재실행하니 변경된 검색조건의 쿼리와 결과를 확인할 수 있다!
String searchCondition = "CONTENT"; 으로 변경 후 다시 테스트 실행
Hibernate:
select
board0_.seq as seq1_0_,
board0_.cnt as cnt2_0_,
board0_.content as content3_0_,
board0_.create_date as create_d4_0_,
board0_.title as title5_0_,
board0_.writer as writer6_0_
from
board board0_
where
board0_.content like ? escape '!' limit ?
검색 결과
[연관관계 매핑]
JPA는 테이블과 엔티티를 매핑하는 기술.
테이블이 관계를 맺듯이 엔티티 역시 다른 엔티티와 관계를 맺고 있고, 이 관계를 통해 연관 데이터를 관리 가능하다.
객체는 참조 변수를 통해 연관관계를 맺기 때문에 테이블의 연관과 엔티티의 연관이 정확하게 일치하지 않는다.
1. 단방향 연관관계 설정하기
객체지향 프로그램 : 객체는 참조변수를 통해 다른 객체와 관계를 맺음
관계형 데이터베이스 : 외래키를 이용하여 다른 테이블과 관계를 맺음
방향(direction) : 단방향과 양방향 존재. 방향은 객체에만 존재하고 테이블은 항상 양방향
다중성(Multiplicity) : 다대일, 일대다, 일대일, 다대다
연관관계 주인(Owner) : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야한다. 일반적으로 다대일이나 일대다관계에서 연관관계의 주인은 다쪽에 해당하는 객체로 생각.
다대일 단방향 매핑하기
다대일 관계는 데이터 모델링에서 가장 일반적. 모든 매핑에서 가장 기본이 됨.
객체 연관관계 : 게시판 객체는 참조변수를 통해 회원 객체와 관계를 맺는다.
테이블 연관관계 : 테이블은 외래키 하나로 처음부터 양방향 참조가 가능하다
@ManyToOne 다대일 관계 설정
속성 |
기능 |
기본값 |
Optional |
연관된 엔티티가 반드시 있어야 하는지 여부를 결정한다. False일 경우 항상 있어야 한다는 의미 |
True |
Fetch |
글로벌 패치 전략을 설정한다. EAGER : 연관 엔티티를 동시에 조회 LAZY : 연관 엔티티를 실제 사용할 때 조회 |
@ManyToOne : EAGER @OneToMany : LAZY |
cascade |
영속성 전이 기능을 설정한다. 연관 엔티티를 같이 저장하거나 삭제할 때 사용한다. |
|
@JoInColumn은 name속성을 통해 참조하는 테이블의 외래 키 칼럼을 매핑
@Getter
@Setter
@ToString
@Entity
public class Member { //회원 정보 클래스
@Id
@Column(name="MEMBER_ID")
private String id;
private String password;
private String name;
private String role;
}
package com.studyboot.persistence;
import org.springframework.data.repository.CrudRepository;
import com.studyboot.domain.Member;
public interface MemberRepository extends CrudRepository<Member, String>{
}
다대일 연관관계 테스트 - 게시글 등록
엔티티를 저장할 때 연관관계에 있는 엔티티가 있다면 해당 엔티티도 영속 상태에 있어야 한다.
Writer 컬럼을 더이상 사용하지 않으므로 기존 @query도 주석처리해야함
package com.studyboot;
import java.util.Date;
import org.junit.jupiter.api.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 com.studyboot.domain.Board;
import com.studyboot.domain.Member;
import com.studyboot.persistence.BoardRepository;
import com.studyboot.persistence.MemberRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RelationMappingTest {
@Autowired
private BoardRepository boardRepo;
@Autowired
private MemberRepository memberRepo;
@Test //다대일 연관관계 등록 테스트
public void testManyToOneInsert() {
Member mem1 = new Member();
mem1.setId("mem1");
mem1.setName("hello");
mem1.setPassword("123");
mem1.setRole("user");
memberRepo.save(mem1); //회원정보를 먼저 영속성 컨텍스트에 저장
Member mem2 = new Member();
mem2.setId("mem2");
mem2.setName("bye");
mem2.setPassword("456");
mem2.setRole("admin");
memberRepo.save(mem2);
for (int i=1; i<=3; i++) {
Board b = new Board();
b.setMember(mem1);
b.setTitle("hello가 작성한 글" + i);
b.setContent("내용"+i);
b.setCreateDate(new Date());
b.setCnt(0L);
boardRepo.save(b); //게시글 엔티티 저장
}
for (int i=1; i<=3; i++) {
Board b = new Board();
b.setMember(mem2);
b.setTitle("bye가 작성한 글" + i);
b.setContent("내용"+i);
b.setCreateDate(new Date());
b.setCnt(0L);
boardRepo.save(b);
}
}
}
application.properties
# JPA Setting - 컬럼이 변경되었으니 테이블 새로 생성되는 create으로 변경 후 다시 update로 변경
spring.jpa.hibernate.ddl-auto=create
다대일 연관관계 테스트 - 게시글 상세 조회
@ManyToOne 어노테이션의 Fetch속성의 기본값이 EAGER. 수정된 테스트 케이스를 실행하면 조인이 실행되서 연관관계에 있는 회원 정보까지 같이 조회된다.
RelationMappingTest.class
@Test //게시글 상세 조회 테스트
public void testManyToOneSelect() {
Board b = boardRepo.findById(5L).get();
System.out.println("["+b.getSeq()+"번 게시글 정보]");
System.out.println("제목: " + b.getTitle());
System.out.println("내용: " + b.getContent());
System.out.println("작성자: " + b.getMember().getName());
System.out.println("권한: " + b.getMember().getName());
}
외부 조인은 성능상 내부 조인보다 좋지 않다. 따라서 반드시 참조 키에 값이 설정된다는 전제가 성립된다면 외부 조인을 내부 조인으로 변경하는 것이 좋다.
Hibernate:
select
board0_.seq as seq1_0_0_,
board0_.cnt as cnt2_0_0_,
board0_.content as content3_0_0_,
board0_.create_date as create_d4_0_0_,
board0_.member_id as member_i6_0_0_,
board0_.title as title5_0_0_,
member1_.member_id as member_i1_1_1_,
member1_.name as name2_1_1_,
member1_.password as password3_1_1_,
member1_.role as role4_1_1_
from
board board0_
left outer join
member member1_
on
board0_.member_id=member1_.member_id
where
board0_.seq=?
[5번 게시글 정보]
제목: bye가 작성한 글2
내용: 내용2
작성자: bye
권한: bye
반드시 참조키에 값이 설정된다면 내부조인으로 설정하는 것이 좋다. 해당 칼럼이 항상 참조값을 가진다는 의미로 @JoinColumn에 nullable 속성을 추가하면 된다.
Board.class
@ManyToOne //다대일 관계설정
@JoinColumn(name="MEMBER_ID", nullable = false) //참조하는 외래키 칼럼과 매핑 관계를 설정하는 어노테이션
private Member member;
변경하여 실행하니 내부조인으로 실행되는 것을 볼 수 있다.
Hibernate:
select
board0_.seq as seq1_0_0_,
board0_.cnt as cnt2_0_0_,
board0_.content as content3_0_0_,
board0_.create_date as create_d4_0_0_,
board0_.member_id as member_i6_0_0_,
board0_.title as title5_0_0_,
member1_.member_id as member_i1_1_1_,
member1_.name as name2_1_1_,
member1_.password as password3_1_1_,
member1_.role as role4_1_1_
from
board board0_
inner join
member member1_
on
board0_.member_id=member1_.member_id
where
board0_.seq=?
[5번 게시글 정보]
제목: bye가 작성한 글2
내용: 내용2
작성자: bye
권한: bye
2. 양방향 연관관계 설정하기
회원은 게시판과 일대다 관계다.
객체 연관관계 : 하나의 객체가 여러 객체와 연관관계를 맺을 수 있으므로 List같은 컬렉션을 사용해야 한다.
테이블 연관관계 : 외래 키 하나로 조인을 통해 양방향 조회가 가능
@OneToMany는 일대다 관계를 매핑할 때 사용.
속성 |
기능 |
|
mappedBy |
양방향 연관관계에서 연관관계의 주인을 지정할 때 사용한다. 객체 둘 중 어떤 관계를 사용해서 외래키를 관리해야할지 결정해야하는데 이를 연관관계 주인이라 함. 외래키 관리자 연관관계 주인은 테이블에 외래키가 있는곳으로 정해야 한다. |
|
fetch |
연관 엔티티를 동시에 / 실제 사용할때 조회 |
@ManyToOne : EAGER @OneToMany : LAZY |
Member.class
//일대다 관계. 하나의 객체가 여러 객체와 연관관계를 맺을 수 있으므로 List와 같은 컬렉션을 사용해야 함
@OneToMany(mappedBy = "member", fetch=FetchType.EAGER)
private List<Board> boardList = new ArrayList<Board>();
양방향 연관관계를 테스트 하기 위한 테스트 케이스 메소드 생성
RelationMappingTest.class
@Test //양방향 연관관계 테스트
public void testTwoWayMapping() {
Member member = memberRepo.findById("mem1").get();
System.out.println("-------------------------------------------");
System.out.println(member.getName() +"의 작성 게시글");
System.out.println("-------------------------------------------");
List<Board> list = member.getBoardList();
for (Board b : list) {
System.out.println(b.toString());
}
}
롬복에서 @ToString이 양방향 참조에서 상호 호출을 하게 도면 StackOverFlowError가 발생한다. 상호 호출 고리를 끊기 위해 엔티티 클래스에 @ToString 어노테이션에 exclude 속성을 추가해야 한다.
//상호 호출 연결고리를 끊기 위해 exclude 속성 설정.
Board.class
@ToString(exclude = "member")
Member.class
@ToString(exclude = "boardList")
영속성 전이
JPA는 cascade 속성을 이용하여 부모 엔티티를 저장할 때 자식 엔티티도 같이 저장 가능. 부모 엔티티 삭제시 자식 엔티티도 삭제 가능
CascadeType.ALL : 객체가 영속화되거나 수정, 삭제될 때 관련된 객체 또한 같이 변경된다.
Member.class
//일대다 관계. 하나의 객체가 여러 객체와 연관관계를 맺을 수 있으므로 List와 같은 컬렉션을 사용해야 함
@OneToMany(mappedBy = "member", fetch=FetchType.EAGER, cascade = CascadeType.ALL)
private List<Board> boardList = new ArrayList<Board>();
Board.class
//영속상태가 아닌 단순한 일반 자바 객체 상태에서도 관련된 데이터를 사용하기 위함
public void setMember(Member member) {
this.member = member;
member.getBoardList().add(this);
}
Board 객체에 Member 객체를 설정할 때, 회원이 소유한 boardList에 자동으로 Board객체가 반영 되도록 Setter를 작성한다.
@Test //다대일 연관관계 등록 테스트
public void testManyToOneInsert() {
Member mem1 = new Member();
mem1.setId("mem1");
mem1.setName("hello");
mem1.setPassword("123");
mem1.setRole("user");
Member mem2 = new Member();
mem2.setId("mem2");
mem2.setName("bye");
mem2.setPassword("456");
mem2.setRole("admin");
for (int i=1; i<=3; i++) {
Board b = new Board();
b.setMember(mem1);
b.setTitle("hello가 작성한 글" + i);
b.setContent("내용"+i);
b.setCreateDate(new Date());
b.setCnt(0L);
}
memberRepo.save(mem1);// member 객체가 영속화되면서 자동으로 board 객체도 자동으로 영속화된다.
for (int i=1; i<=3; i++) {
Board b = new Board();
b.setMember(mem2);
b.setTitle("bye가 작성한 글" + i);
b.setContent("내용"+i);
b.setCreateDate(new Date());
b.setCnt(0L);
}
memberRepo.save(mem2);
}
영속성 전이를 이용하여 삭제하기
RelationMappingTest.class
@Test // 영속성 전이를 이용한 삭제 처리
public void testCascadeDelete() {
memberRepo.deleteById("mem2");
}
댓글 reply 엔티티 생성, 게시판 과 댓글 양방향 매핑처리
// TODO
참고 서적 : 누구나 끝까지 따라 할 수 있는 스프링 부트 퀵스타트
'Dev > SpringBoot' 카테고리의 다른 글
[SpringBoot] 스프링 부트 시큐리티 (0) | 2020.01.02 |
---|---|
[Springboot] 스프링부트 화면 개발 (0) | 2020.01.01 |
[SpringBoot] JPA 퀵 스타트 (0) | 2019.12.23 |
[SpringBoot] 테스트와 로깅 (0) | 2019.12.18 |
[SpringBoot] 스프링부트 자동설정 (0) | 2019.12.13 |