너굴 개발 일지

210707_Spring Boot_Intelli J 와 MariaDB를 이용한 Spring Data JPA 관련 작업 (MemoProject) 본문

SPRING BOOT

210707_Spring Boot_Intelli J 와 MariaDB를 이용한 Spring Data JPA 관련 작업 (MemoProject)

너굴냥 2021. 7. 13. 01:03

 

Spring Data JPA란?

spring framework에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트 (엔티티, Repository가 핵심)

- CRUD 처리를 위한 공통 인터페이스 제공

- repository 개발 시 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입

- 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있도록 지원

- 공통 메소드는 스프링 데이터 JPA가 제공하는 org.springframework.date.jpa.repository.JpaRepository 인터페이스에

  count, delete, deleteAll, deleteAll, deleteById, existsById, findById, save ..

 

<참고>

- 스프링 부트는 JPA 구현체중 Hibernate을 이용

- Hibernate : ORM을 지원하는 오픈소스 프레임워크로 단으로 프로젝트에 적용이 가능한 독립된 프레임워크

- Spring Data JPA는 Hibernate를 쉽게 사용할 수 있도록 추가적인 API를 제공

 

스프링 데이터 JPA 적용

- 공통 메소드가 아닐 경우에도 스프링 데이터 JPA가 메소드 이름을 분석해서 JPQL을 실행

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByUsername(String username);
    // select m from Member m where username = :username
}
 
public interface ItemRepository extends JpaRepository<Item, Long> { 
}

 

 

[사전 작업 ]

- Windows Maria DB 설치 (https://mariadb.org/)

- 신규 데이터베이스 및 사용자 계정 설정 (bootex에 권한을 부여해서 외부에서 사용가능하도록 설정)

 

[Spring Data JPA를 이용하는 프로젝트]

1. 아래와 같이 프로젝트 생성

추가 dependencies 

- Spring Data JPA

- Spring Boot DevTools

- Lombok

- Spring Web

 

2. build.gradle 파일에 MariaDB 관련 dependency 추가

https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client 에 접속하여 2.7.0 버전의 Gradle dependencies 코드 추가 

 

build.gradle 파일 일부

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    annotationProcessor 'org.projectlombok:lombok'
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
    implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.7.0'

}

 

3. application.properties에 DB(Datasource) 설정

server.port=9000
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/bootex
spring.datasource.username=bjy
spring.datasource.password=1234

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true;
spring.jpa.show-sql=true

spring.datasource.driver-class-name : 접속DB 드라이버명

spring.datasource.url = 데이터베이스 접속 주소

① MySQL, MaridDB 일 경우 : 데이터베이스 접속 주소/데이터베이스명

② 오라클일 경우 : 데이터베이스 접속 주소

 

[Spring Data JPA를 위한 설정]

- spirng.jpa.hibernate.ddl-auto : 플젝 실행시 자동으로 DDL 생성할지에 대한 여부 

 

create : 매번 테이블 새롭게 생성
update : 변경 필요한 경우에만 alter, 테이블 없을 경우 create
create-drop : 매번 테이블 생성후 작업 종료 직전 생성 테이블 삭제
validate : 테이블에 대한 유효성 검사

 

- spring.jpa.show-sql= true /false : 실제 JPA 구현체인 hibernate가 처리시 발생하는 sql문 보여줄것인지에 대한 여부

- spirng.jpa.properties.hibernate.format-sql=true / false : SQL을 포맷팅(들여쓰기 등) 출력 (실행되는 SQL에 대한 가독성 높일 경우 설정)

 

 

 

 

build.gradle, application.properties 파일 설정 후 PracticeApplication 실행시 정상적으로 구동되는 것을 볼 수 있습니다.

 

4. 엔티티 클래스(Memo) 생성

엔티티 클래스 : JPA를 통해 관리되는 객체(엔티티)를 위한 클래스

src/main/java의 기본 패키지에 entity 패키지 생성 후 클래스 생성

(JPA에서는 엔티티는 테이블에 대응하는 하나의 클래스라고 생각하면 됩니다.)

package com.bjy.pracitce.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Table(name="tbl_memo")
@ToString
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Memo {

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

    @Column(length = 200,nullable = false)
    private String memoText;

    @Transient
    private int t;
}
JPA 관련 어노테이션 설명
@Entity Spring Data JPA 사용시 필수로해당 클래스는 엔티티 클래스이고, 해당 클래스의 인스턴스들이 JPA로 괸리되는 엔티티 객체라는 것을 의미
옵션에 따라서 지동으로 테이블을 생성할 수도 있으며 이 경우 해당 클래스의 멤버변수 설정에 따라서 자동으로 컬럼까지 생성됨
@Id  @Entity 가 붙은 클래스 내에는 반드시 PK에 해당하는 특정 멤버변수를 
@Id로 설정해야 함,
만약 해당 멤버변수가 사용자 입력값이 아니라면 @GeneratedValue를 사용하여 자동으로 번호를 생성하여 사용 
@Table @Entity 어노테이션과 함께 사용 가능
관계형 데이터베이스에서 엔티티 클래스를 어떤 테이블로 생성할 것인지에 대한 정보를 담기위한 어노테이션
ex ) @Table(name = "tbl_memo")의 경우
테이블 이름이 "tbl_memo"인 테이블을 생성 
@GeneratedValue
(strategy = GeneratoionType.IDENTITY)
연결 DB가 오라클일 경우 : 별도의 번호 저장을 위한 테이블 자동 생성
연결 DB가 MySQL, MariaDB일 경우 : auto increment를 이용
@Column  추가적인 필드(컬럼) 가 필요할 경우에 사용
(name, nullable 등 다양한 설정을 위한 옵션 제공

[참고 - strategy 설정 값]

GeneratoionType.AUTO : 기본값으로 JPA 구현체(Hibernate)가 생성 방식을 결정

GeneratoionType.IDENTITY : 사용하는 데이터베이스가 키 생성을 결정

GeneratoionType.SEQUENCE : 데이터베이스의 시퀀스를 이용하여 키를 생성. @SequenceGenerator 와 함께 사용

GeneratoionType.TABLE : 키 생성 전용 테이블을 생성하여 키 생성. @TableGenerator 와 함께 사용

 

Lombok 관련 어노테이션  설명
@Builder 해당 클래스에 대한 객체 생성 처리 (실제 사용시엔 클래스명.builder() 를 이용)
단, 이 어노테이션을 사용할 경우에는 반드시 @AllArgsConstrutor / @NoArgsConstrutor 를 함께 설정

그렇지 않을 경우 컴파일 시 오류 발생

 

5. JpaRepository 인터페이스 생성 (MemoRepository)

(src/main/java 기본 패키지에서 repository 패키지 추가 후 인터페이스 생성)

Spring Data JPA는 JPA를 쉽게 사용할 수 있는 API의 일부로 Repository라는 타입의 기능을 제공합니다.

검색/정렬 가능하며 인터페이스만 작성하면 동적으로 객체가 생성되는 방식입니다. (동적 프록시)

=> Spring Data JPA는 인터페이스 선언만으로 자동으로 인터페이스에 맞는 객체 생성하여 Bean 등록 

 

위 인터페이스를 사용할 경우 엔티티 타입 정보와 @Id 타입 정보를 Map 형태로 지정합니다.

스프링부트에서는 Entity의 기본적인 CRUD가 가능하도록 JpaRepository 인터페이스제공합니다.

 

import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.ex2.entity.Memo;
public interface MemoRepository extends JpaRepository<Memo, Long> {
}

JpaRepository를 상속받을 때는 사용될 Entity 클래스와 ID 값이 들어가게 됩니다. 즉, JpaRepository<T, ID> 가 됩니다.

 

그렇게 JpaRepository를  단순하게 상속하는 것만으로 위의 인터페이스는 Entity 하나에 대해서 아래와 같은 기능을 제공합니다.

 

메소드 기능
save()(엔티티 객체) 레코드 저장 (insert, update)
 findOne() primary key로 레코드 한건 찾기
findAll() 전체 레코드 불러오기. 정렬(sort), 페이징(pageable) 가능
count() 레코드 갯수
delete()(엔티티 객체) 레코드 삭제

기본 기능을 제외한 조회 기능을 추가하고 싶다면 규첵이 맞는 메소드를 추가하면 됩니다.

참고로 save()는 JPA의 구현체가 메모리상에서 객체를 비교하고 없으면 insert / 존재하면 update를 동작합니다.

메소드 설명
findBy로 시작 쿼리를 요청하는 메서드 임을 알림
ex ) findByName
countBy로 시작 쿼리 결과 레코드 수를 요청하는 메서드 임을 알림

 

6. 테스트 코드를 통한 CRUD (MemoRepositoryTests)

(src/test/java 의 기본 패키지에 repository 패키지 생성 후 해당 클래스 생성)

insert 작업: save(엔티티 객체)

select 작업: findById(키 타입), getOne(키 타입)

update 작업: save(엔티티 객체)

delete 작업: deleteById(키 타입), delete(엔티티 객체)

 

 

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class MemoRepositoryTests {
 @Autowired
 MemoRepository memoRepository;
 @Test
 public void testClass(){
 System.out.println(memoRepository.getClass().getName());
 }
}

[등록 작업 테스트]

@Autowired
 MemoRepository memoRepository;
...
 @Test
 public void testInsertDummies(){
 IntStream.rangeClosed(1,100).forEach(i -> {
 Memo memo = Memo.builder().memoText("Sample..."+i).build();
 memoRepository.save(memo);
 });
 }

콘솔창 결과
mariaDB 추가된 데이터

[조회 작업 테스트]

(1) findById()  를 이용 : 해당 메서드가 호출되어 실행되는 순간 SQL이 바로 처리

- findById() 는 반환 타입이 Optional 타입으로 반환

MemoRepositoryTests 클래스의 일부
@Test
public void testSelect(){
 //데이터베이스에 존재하는 mno
 Long mno = 99L;
 Optional<Memo> result = memoRepository.findById(mno);
 System.out.println("==================================");
 if(result.isPresent()){
 Memo memo = result.get();
 System.out.println(memo);
 }
}

(1)getOne()  를 이용 : getOne()는 해당객체를 반환하는데, 메서드 실행 시 SQL 이 실행되지 않고, 실제 객체가 필요한 시점에서 SQL 이 실행

- getOne()는 엔티티 객체로 반환

    @Transactional
    @Test
    public void  testSelect2(){
        Long mno = 99L;
        Memo memo = memoRepository.getOne(mno);
        System.out.println("===========");

        System.out.println(memo);
        // Memo는 @ToString을 이용하여 toString()가 재정의 되었기 때문
        // memo 객체를 출력하면 자동으로 toString() 메서드가 호출되어

        System.out.println("===========");
    }

 

[수정 작업 테스트]

save()를 이용 : id 값이 있으면(존재하면) update , 존재하지 않으면 insert

따라서 save()는 select를 이용하여 해당 값이 존재하는지 확인 후, update, insert를 결정하는 메서드

@Test
public void testUpdate() {
 Memo memo = Memo.builder().mno(100L).memoText("Update Text").build();
 System.out.println(memoRepository.save(memo));
}

테스트하면서 mno=100의 데이터도 생성했기에 update 구문으로 실행됨

[삭제 작업 테스트] 

deleteById() : 리턴값이 없으며 해당 데이터가 존재하지 않는다면 예외 발생

@Test
public void testDelete() {
 Long mno = 100L;
 memoRepository.deleteById(mno);
}

콘솔창 결과

[페이징 / 정렬 처리]

Spring Data JPA는 페이지 처리와 정렬을 API에서 지원합니다. 별도의 추가적인 호출 없이 자동으로 페이지 처리와 필요한 count관련 쿼리 실행합니다. 

Pageable : 페이지 처리에 필요한 정보를 전달하는 용도로 설계된 인터페이스

(org.springframework.data.domain.Pageable)

따라서 객체 생성시 (org.springframework.data.domain.PageRequest 클래스를 이용합니다.

 

PageRequest는 생성자가 protected이기에 new 연산자 사용이 불가하고

=> static of() 이용하여 Pageable 객체를 반환

 

PageRequest.of(int page, int size) : 0번부터 시작하는 페이지 번호와 갯수
page : 페이지 번호
size : 페이지당 데이터 갯수

PageRequest.of(int page, int size, Sort.Direction direction, String ...props)
Sort.Direction direction : 정렬 방향(방법)

tring ...props              : 정렬 기준 필드들...

PageRequest.of(int page, int size, Sort sort)

Sort sort : 정렬 관련 정보 (즉, 정렬 방향과 정렬 기준 필드들을 하나로 묶은 객체)

 

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
…생략

 @Test
    public void testPageDefault(){
        // 만약 한 페이지에 10개씩 출력할 경우
        Pageable pageable = PageRequest.of(0,10);
        Page<Memo> result=memoRepository.findAll(pageable);

        System.out.println(result);
        System.out.println("===============");
        System.out.println("Total Pages : "+result.getTotalPages());
        System.out.println("Total Count : "+result.getTotalElements());
        System.out.println("Current Page Number : "+result.getNumber());
        System.out.println("Page Size : "+result.getSize());
        System.out.println("has next page ? :"+result.hasNext());
        System.out.println("first page ? "+result.isFirst());
        }

- findAll(Pageable pageable)

 PagingAndSortRepository 에서 정의된 메서드로 반환 타입 : Page<엔티티 객체 타입>

(스프링 부트 2.0부터 지원)

 

findAll() 메서드는 두 가지의 SQL을 실행

1. limit 를 사용하는 select SQL               : 지정한 시작 페이지 번호부터 지정한 갯수를 select 하기 위한 SQL

2. count() 함수를 사용하는 select SQL     : 전체 레코드(엔티티) 갯수를 확인하기 위한 select SQL

 

- Page<엔티티 객체 타입>

해당 객체는 결과를 확인하기 위한 다양한 메서드 제공 (getTotalPages(), isFrist() 등..)

 

실행된 sql

실행된 sql문을 보면 tbl_memo memo0_ limit ?   <==== MySQL, MariaDB(limit) 와 Oracle(inline view)이 다르게 실행됩니다.

 

 

[ 정렬 조건 추가하여 데이터 출력 ]

findAll() 메소드를 이용

    @Test
    public void testSort(){
        Sort sort = Sort.by("mno").descending();
        Pageable pageable = PageRequest.of(0, 10, sort);
        Page<Memo>result = memoRepository.findAll(pageable);

        result.get().forEach(memo ->{
                System.out.println(memo);
        });
    }

mno을 내림차순으로 정렬한 후 페이지당 10개의 데이터를 가지면서 첫번째 페이지의 데이터를 출력한 결과입니다.

 

7. 쿼리 메서드 기능

쿼리 메서드는 메소드 이름 자체가 쿼리문이 되는 기능으로  findBy, getBy 등으로 메소드명을 시작하며 칼럼, 키워드 조합으로 메서드를 작성합니다.

메소드의 파라미터(매개변수)는 키워드에 따라서 갯수가 결정됩니다.

만약 Between의 경우 범위를 지정하는 2가지 값이 필요하기에 2개의 매개변수가 요구되며 실제 SQL은

'select ~~~~ where startDate between ? and ?' 와 같은 형식으로 실행됩니다.

(참고로 IntellJ 일 경우 쿼리 메서드 작성 기능을 제공합니다.)

ex ) findByMnoBetweenOrderByMnoDesc

[ 쿼리 메서드의 리턴타입 ] 

select 작업 => List 타입이나 배열 선택

파라미터에 Pageable 타입을 넣는 경우 무조건 Page<E> 타입

 

만약 mno값이 70에서 80에 속하는 Memo객체를 구하고 싶을 경우 test 메소드 예시

  @Test
    public void testQueryMethods(){
        List<Memo> list = memoRepository.findByMnoBetweenOrderByMnoDesc(70L, 80L);
        // List<Memo> findByMnoBetweenOrderByMnoDesc(Long from, Long to);
        for(Memo memo : list) {
            System.out.println(memo);
        }
    }

 

[쿼리 메소드와 Pageable의 결합]

페이지 처리와 order by처리 가능합니다.

대부분의 경우 쿼리 메서드는 정렬조건은 만들지 않고, Pageable 파라미터를 이용하는 경우가 많습니다.

 

public interface MemoRepository extends JpaRepository<Memo, Long> {
 List<Memo> findByMnoBetweenOrderByMnoDesc(Long from, Long to);
 Page<Memo> findByMnoBetween(Long from, Long to, Pageable pageable);
}
//    // 쿼리 메서드와 Pageable 결합
//    // Memo의 mno값이 10~50 사이의 객체를 조회
//    // 그중에 첫 페이지에 해당하는 10개의 객체를 mno 기준으로 역순 정렬
    @Test
    public void testQuertMethodWithPageable(){
        Pageable pageable = PageRequest.of(0,10, Sort.by("mno").descending());
        Page<Memo> result = memoRepository.findByMnoBetween(10L, 40L, pageable);

        result.get().forEach(memo -> System.out.println(memo));
    }

 

[deleteBy 삭제]

삭제 처리 기능

@Commit을 사용하지 않았을 경우에는 Rollback 기능인 @Transactional을 사용해야 합니다.

@Transactional가 없을 경우엔 예외가 발생합니다. (javax.persistence.TransactionRequiredException)

 

이유는?

1. select 문으로 해당 엔티티객체 조회

2. 각 엔티티 객체들을 한번에 삭제하지 않고 하나씩 삭제 하기 때문입니다.

 

//    // Memo의 mno가 10보다 작은 엔티티 객체를 삭제하는 메서드
   @Commit
    @Transactional
    @Test
    public void testDeleteQueryMethods(){
        memoRepository.deleteMemoByMnoLessThan(10l);
    }

 

8. @Query

기존 SQL을 이용하여 메서드 생성이 가능한 어노테이션입니다. 만약 조인 같은 복잡한 조건을 처리할 때 해당 어노테이션을 사용합니다.

 

@Query("SQL 구문")

메서드 이름과 무관하게 메서드에 추가한 어노테이션을 통하여 원하는 작업을 처리합니다. 또한 SQL 구문은 JPQL(객체 지향 쿼리)로 작성합니다.

(즉, 메서드명은 개발자가 임의의 명명할 수 있다)

 

@Query 를 이용하여 할 수 있는 작업

1. 필요한 데이터만 선별적으로 추출 가능

2. 데이터베이스에 맞는 순수한 SQL을 사용 가능

3. insert, update, delete 와 같이 select가 아닌 DML 등을 처리 가능합니다. 단, @Modifying 과 함께 사용합니다.

 

JPQL(객체 지향 쿼리) 

테이블 대신 엔티티 클래스를 이용합니다. 테이블의 컬럼 대신, 엔티티 클래스의 멤버변수(필드)를 이용하여 작성합니다.

실제 SQL 에서 사용되는 함수들은 동일하게 사용 가능합니다.

( avg() / sum() / max() / count() . group by / order by 등...)

 

예) mno 역순으로 정렬하는 메서드 선언 시.

     @Query("select m from Memo m order by m.mno desc")

     List<Memo> getListDesc();

 

 

비교) JPA에서 

      SQL : "select * from tbl_memo m order by m.mno desc"

     JPQL : "select m from Memo m order by m.mno desc

 

 

[역순으로 정렬하여 모든 Memo 객체 호출하는 메서드]

public interface MemoRepository extends JpaRepository<Memo, Long> {
// mno 역순으로 정렬하는 @Query를 이용한 쿼리 메서드
    @Query("select m from Memo m order by m.mno desc")
    List<Memo> getListDesc();
}
    @Test
    public  void testSelectAll(){
        List<Memo> list = memoRepository.getListDesc();

        for(Memo memo : list){
            System.out.println(memo);
        }
    }

데이터가 많아서 나머지는 생략합니다.

 

 

 

@Query 의 파라미터 바인딩 방법

1. ~~~ 와 같이 :파라미터명   을 활용하는 방식

2. 여러 개의 파라미터를 전달 =>  객체를 이용  :#{ } 

 

* 참조 : update / delete 등 데이터가 수정이 되는 경우에는 @Transactional 을 사용합니다.

  특히 update를 사용할 경우에는 @Modifying 까지 사용해야 합니다.

[특정 mno인 객체의 memoText를 변경하는 메서드]

    // mno를 이용하여 해당 memoText를 변경하는 @Query를 이용한 쿼리 메서드
    @Transactional
    @Modifying
    @Query("update Memo m set m.memoText = :memoText where m.mno = :mno")
    int updateMemoText(@Param("mno") Long mno, @Param("memoText") String memoText);
    // mno가 20인 엔티티 객체의 memoText를 "Update Text"로 변경
    @Test
    public void testUpdateMemoText(){
        int n = memoRepository.updateMemoText(20L, "Update Text");

        System.out.println("==>" + n);
    }

[mno가 아닌 no를 이용하여 해당 memoText를 변경하는 @Query를 이용한 쿼리 메서드 (단, 객체로 전달)]

   // no를 이용하여 해당 emmoText를 변경하는 @Query를 이용한 쿼리 메서드(단, 객체로 전달)
    @Transactional
    @Modifying
    @Query("update Memo m set m.memoText = :#{#param.memoText} where m.mno = :#{#param.mno}")
    int updateMemoText(@Param("param") Memo memo);
   @Test
    public void testUpdateMemoText2(){
        Memo memo = Memo.builder().memoText("Update Memo").mno(22L).build();
        int n = memoRepository.updateMemoText(memo);

        System.out.println("==>" + n);
    }