Dev/SpringBoot

[SpringBoot] 스프링 데이터 JPA

창문닦이 2019. 12. 28. 02:14

[스프링 데이터 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 실행된다.

DB 확인

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

DB 확인

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

테이블과 시퀀스 생성 > 테스트 멤버 데이터 입력 > 테스트 게시판 데이터 입력
DB에 정상적으로 입력되었다

다대 연관관계 테스트 - 게시 상세

@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);
	}

자동으로 영속화가 처리되어 입력 진행이 된다.
누적되어 입력된 데이터 DB 확인

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

RelationMappingTest.class

	@Test // 영속성 전이를 이용한 삭제 처리
	public void testCascadeDelete() {
		memberRepo.deleteById("mem2");
	}

cascade 삭제 진행
성공적으로 삭제된 내용 db 확인

댓글 reply 엔티티 생성, 게시판 과 댓글 양방향 매핑처리

// TODO

 

 

참고 서적 : 누구나 끝까지 따라 할 수 있는 스프링 부트 퀵스타트