Spring Data JPA

강상은·2023년 12월 4일

JPA 알아보기

처음 프로젝트 실행 시 데이터베이스와 관련된 설정이 없기때문에 오류 발생

사용하고자하는 데이터베이스 시스템(mariaDB사용)을 연동시켜주기 위해 application properties에

spring.datasource.driver-class name=org.mariadb.jdbc.Driver

spring.datasource.url=jdbc:mariadb://localhost:3307/webdbboot

spring.datasource.username=webuser

spring.datasource.password=webuser

으로 설정하고나면 다시 jpa open-in-view에서 오류났다고 함

spring.jpa.open-in-view=false 으로 설정을 추가해 줌

기본적으로 Spring Boot JPA에서는 "Open EntityManager in View" 패턴이 활성화되어 있습니다. 이 패턴은 웹 요청이 끝날 때까지 EntityManager를 유지하여 뷰 렌더링 단계에서도 지연로딩된 엔티티에 접근할 수 있게 해줍니다. 그러나 이 패턴은 장기 실행 트랜잭션과 지연로딩으로 인한 성능 문제를 일으킬 수 있음

또한 스프링 부트에서는 자동으로 의존성을 주입해주지만 Lombok의 경우 build.gradle에 Test 라이브러리를 넣어줘야한다.

Board 클래스의 설정(엔티티 객체를 생성하기 위한 엔티티 클래스 정의)

엔티티 클래스는 반드시 @Entity가 존재하고 해당 엔티티 객체의 구분을 위한 @Id가 필요함

@Entity
public class Board {

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

    private String title;
    private String content;
    private String writer;
}

spring.jpa.hibernate.ddl-auto=create 를 했을 경우 실행하면 아래와 같이 테이블이 생성된 것에 대해 콘솔에 출력됨(create는 기존에 테이블이 있는 경우에도 삭제하고 재생성하므로 주의할 것)

서버를 구동해서 ORM으로 매핑된 SQL 실행 → 테이블 생성

bno bigint not null auto_increment →

Board 클래스의 키생성전략(key generate strategy)@GeneratedValue(strategy = GenerationType.*IDENTITY*) 로 데이터베이스에서 알아서 결정하는 방식을 이용

키 생성 전략의 종류

  • IDENTITY : 데이터베이스에 위임(MYSQL,MariaDB)-auto_increment
  • SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용(ORACLE)-@SequenceGenerator 필요
  • TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용-@TableGenerator 필요
  • AUTO : 방언에 따라 자동 지정, 기본값

@MappedSuperClass를 이용한 공통 속성 처리

Board라는 Entity 클래스를 지정한 후 부모가 될 BaseEntity 클래스를 생성(persistance의 성격을 가짐) 부모가 자식을 감시한다라고 생각

@MappedSuperclass : 데이터베이스의 거의 모든 테이블에는 데이터가 추가된 시간이나 수정된 시간등이 칼럼으로 작성, 이를 쉽게 처리하고자 공통으로 사용되는 칼럼들을 지정하고 해당 클래스를 상속 Auditing(감사) 기능을 구현할 때 사용되며 해당 필드에 엔티티가 생성된 일시가 자동으로 저장

@EntityListeners``(value = { AuditingEntityListener.class}): 콜백 리스너 클래스를 지정, JPA 엔티티 리스너는 엔티티 유지 및 업데이트에 대한 감사 정보를 캡쳐, AuditingEntityListener를 적용하면 엔티티가 데이터베이스에 추가되거나 변경될 때 자동으로 시간 값을 지정

AuditingEntityListener를 활성화 시키기 위해서는 @SpringBootApplication 다음에 @EnableJpaAuditing을 추가해 주어야 함.

  • insert 기능 테스트

MemberDAO에서 객체를 저장하고 싶을 때 개발자는 JPA에 Member 객체를 넘긴다.

JPA는

1.Member 엔티티를 분석한다.
2. insert sql을 생성한다.
3. JDBC API를 사용하여 SQL을 DB에 날린다.

  • insert를 실행하는 기능은 JpaRepository의 save()를 통해 이루어짐
    • JpaRepository는 Spring Data JPA에서 제공하는 기능 중 하나로서 JPQL을 활용하여 데이터베이스와 상호 작용합니다. 개발자는 간편한 CRUD 작업을 위해 JpaRepository를 사용하면서, 필요에 따라 JPQL을 직접 작성하여 더 복잡한 쿼리를 수행할 수 있습니다.
  • save()는 현재의 영속 컨텍스트 내에 데이터가 존재하는지 찾아보고 해당 엔티티 객체가 없을때는 insert, 존재할 때는 update를 자동으로 실행
@SpringBootTest
@Log4j2
public class BoardRepositoryTests {

    @Autowired
    private BoardRepository boardRepository;

    @Test
    public void testInsert() {
        IntStream.rangeClosed(1,100).forEach(i -> {
            Board board = Board.builder()
                    .title("title..." +i)
                    .content("content..." + i)
                    .writer("user"+ (i % 10))
                    .build();

            Board result = boardRepository.save(board);
            log.info("BNO: " + result.getBno());
        });
    }

  • select 기능 테스트

개발자는 member의 pk값을 JPA에 넘긴다.

JPA는

  1. 데이터를 객체지향적으로 관리할 수 있기 때문에 개발자는 비즈니스 로직에 집중할 수 있고 객체지향 개발이 가능하다.
  2. JDBC API를 사용하여 SQL을 DB에 날리낟.
  3. DB로부터 결과를 받아온다.
  4. 결과(ResultSet)를 객체에 모두 매핑한다.
    쿼리를 JPA가 만들어 주기 때문에 Object와 RDB 간의 패러다임 불일치를 해결할 수 있다.
  • 특정한 번호의 게시물을 조회하는 기능은 findById()를 이용해서 처리한다. findById()의 리턴 타입Optional<T>이다.
@Test
    public void testSelect() {
        Long bno = 100L;
				//findById의 테스트에러가 뜰 경우 엔티티 클래스의 기본 생성자를 추가했는지 확인할 것
				//(@AllArgsConstructor
				//@NoArgsConstructor
				//@ToString)
        Optional<Board> result = boardRepository.findById(bno); //findById의 리턴타입이 Optional

        Board board = result.orElseThrow();

        log.info(board);

    }

Untitled

  • update 기능 테스트
  • insert와 동일하게 save()를 통해서 처리된다. 동일한 @Id값을 가지는 객체를 생성해서 처리
  • update는 등록 시간이 필요하므로 가능하면 findById()로 가져온 객체를 이용해서 약간의 수정을 통해 처리해보기로 함
  • 가능하면 엔티티 객체는 최소한의 변경이나 변경이 없는 불변(immutable)하게 설계하는 것이 좋음
    • orElseThrow()는 Optional 인터페이스의 메서드(해당 Optional이 비어있을 때 예외를 발생시킴)
    • 일반적으로 Optional은 값이 존재하는지 여부를 확인하고, 값이 존재할 경우 해당 값을 반환하고, 값이 비어있을 경우에는 대체값을 제공하거나 예외를 발생시키는 등의 처리를 할 때 사용

findById() 실행

save() 실행

update()실행

moddate가 수정된 것을 확인

  • delete 기능 테스트
  • delete는 @Id값에 해당하는 값으로 deleteById()를 통해서 실행할 수 있다
  • 먼저 데이터베이스 내부에 같은@Id가 존재하는지 먼저 확인하고 delete문이 실행
@Test
    public void testDelete() {
        Long bno = 101L;

        boardRepository.deleteById(bno);
    }

수정이나 삭제 시에 굳이 select 문이 먼저 실행되는 이유?

JPA를 이용하는 것은 엄밀하게 얘기하면 영속 컨텍스트와 데이터베이스를 동기화해서 관리한다는 의미

  1. 특정한 엔티티 객체가 추가되면
  2. 영속 컨텍스트에 추가하고
  3. 데이터베이스와 동기화가 이루어져야함

따라서 영속 컨텍스트에 대해 해당 엔티티 객체가 존재해야만 하므로 select로 엔티티 객체를 영속 컨텍스트에 저장해서 이를 수정/삭제한 후에 이후 메서드가 실행되는 것

Spring-Data-JPA

JPA는 ORM을 위한 자바 EE 표준이며 Spring-Data-JPA는 JPA를 쉽게 사용하기 위해 스프링에서 제공하고 있는 프레임워크이다.

추상화 정도는 Spring-Data-JPA -> Hibernate -> JPA이다.

Hibernate를 쓰는 것과 Spring Data JPA를 쓰는 것 사이에는 큰 차이가 없지만

  • 구현체 교체의 용이성
  • 저장소 교체의 용이성이라는 이유에서 Spring Data JPA를 사용하는것이 더 좋다.

Paging 처리하기

  • Pageable과 Page 타입
  • Pageable이라는 타입의 객체를 구성해서 파라미터로 전달한다
  • Pageable은 인터페이스로 설계되어있고, 일반적으로 PageRequest.of()라는 기능을 이용
    • PageRequest.of(페이지번호,사이즈) : 페이지번호는 0부터

    • PageRequest.of(페이지번호,사이즈,sort) : 정렬조건추가

    • PageRequest.of(페이지번호,사이즈,sort,Direction,속성…) : 정렬 방향과 여러 속성 지정

      @Test
          public void testSearch1() {
      
              //2 page order by bno desc
              Pageable pageable = PageRequest.of(1,10, Sort.by("bno").descending());
      
              boardRepository.search1(pageable);
      
          }
  • 파라미터로 Pageable을 이용하면 리턴 타입을 Page 타입을 이용할 수 있는데, 단순 목록뿐 아니라 페이징 처리에 데이터가 많은 경우에는 count 처리를 자동으로 실행
  • 일반적으로 Spring Data JPA 리포지토리에서 메소드 시그니처를 설계할 때, Pageable을 사용하는 경우 메소드의 리턴 타입으로 Page<T>를 선택하는 것이 관례
@Test
    public void testSearch1() {

        //2 page order by bno desc
        Pageable pageable = PageRequest.of(1,10, Sort.by("bno").descending());

        boardRepository.search1(pageable);

    }
  • findAll()의 리턴 타입으로 나오는 Page타입은 내부적으로 페이징 처리에 필요한 여러 정보를 처리
@Test
    public void testPagingEX(){

       Pageable pageable =  PageRequest.of(0,10); //페이지번호는 0부터 10까지
        //return Type pageagble
        log.info("페이지관련 객체 " + pageable);
        log.info("첫번째 페이지 " + pageable.first() );
        log.info("다음 페이지 " + pageable.next() );
        log.info("페이지 번호 가져오기 " + pageable.getPageNumber() );
        log.info("10페이지 가져오기 " + pageable.withPage(10) );
    }

쿼리메소드와 @Query

  • 다양한 조건의 쿼리와 목록 기능 구현시 JPQL 이용

    • @Query 어노테이션의 value로 작성하는 문자열을 JPQL이라고 함

    • SQL과 유사하게 JPA에서 사용하는 쿼리 언어(query language)

  • JPA는 데이터베이스에 독립적으로 개발이 가능하므로 특정한 데이터베이스에서만 동작하는 SQL 대신 JPA에 맞게 사용하는 JPQL을 이용하는 것

  • 스프링 JPQ에서는 복잡한 JPQL을 메소드로 대신 처리 -> 쿼리 메소드(메소드의 이름으로 필요한 쿼리를 만들어주는 기능)

  • find + 엔티티 이름(생략가능)  + By + 변수명

    • 엔티티 이름이 생략되면 현재  사용하는 Repository 인터페이스에 선언된 타입 정보를 기준으로 자동으로 엔티티 이름이 적용

Querydsl을 이용한 동적 쿼리 처리

Querydsl(domain specific language)

특정 도메인(산업,분야)에 맞춘 쿼리

개요

데이터베이스를 이용할 때 JPA나 JPQL을 이용하는 경우

JPQL (Java Persistence Query Language):

  • JPQL은 주로 EntityManager를 통해 사용되며, JPA에서 제공하는 createQuery 메서드를 사용하여 작성된 JPQL 쿼리를 실행
  1. 어노테이션을 이용해서 지정하기 때문에 고정된 형태
  2. Board의 경우 ‘검색’이라는 기능이 필요한데 ‘제목/내용/작성자’와 같은 단일 조건이 있기도 하지만 ‘제목과 내용’,’제목과 작성자’와 같이 복합적인 검색 조건을 해결하기 어려움

근본 원인은 JPQL이 정적으로 고정되기 때문

이를 해결해주는 방식으로 Querydsl을 사용

JPQL은 Java Persistence Query Language의 약자로, Java에서 JPA(Java Persistence API)를 사용하여 데이터베이스와 상호 작용하기 위해 사용되는 쿼리 언어입니다. 
JPQL은 엔터티 객체를 대상으로 쿼리를 작성하며, SQL과는 다르게 데이터베이스의 특정 구현에 의존하지 않습니다. 대신, 엔터티 객체를 기반으로 쿼리를 작성하고 실행할 수 있어 객체 지향적인 접근이 가능합니다.

반면에 JPQLQuery는 Querydsl에서 제공하는 인터페이스로, Querydsl을 사용하여 동적이고 타입 안전한 쿼리를 작성하기 위한 도구입니다. 
Querydsl은 JPQL의 확장된 형태로 생각할 수 있습니다. Querydsl을 사용하면 자바 코드로 쿼리를 작성할 수 있으며, 타입 안전성을 제공하면서도 동적인 쿼리 작성이 가능합니다.

간단히 말해, JPQLJPA에서 사용하는 쿼리 언어이고, JPQLQuery는 Querydsl에서 제공하는 도구로서 타입 안전하고 동적인 쿼리 작성을 위한 인터페이스입니다. 
Querydsl은 JPQL을 기반으로 하되, 좀 더 강력하고 유연한 쿼리 작성을 지원합니다.

정의

Querydsl은 JPA의 구현체인 Hibernate 프레임워크가 사용하는 HQL(Hibernate Query Language)을 동적으로 생성할 수 있는 프레임워크지만 JPA를 지원

자바코드를 이용하기 때문에 타입의 안정성을 유지한 상태에서 원하는 쿼리 작성 가능

사용

Querydsl을 이용하기 위해서는 Q도메인이라는 존재가 필요함

Q도메인은 Querydsl의 설정을 통해서 기존의 엔티티 클래스를 Querydsl에서 사용하기 위해 별도의 코드로 생성하는 클래스

Querydsl을 사용하기 위한 프로젝트 설정 변경

build.gradle의 설정 변경

buildscript {
    ext {
        queryDslVersion = "5.0.0"
    }
}
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"

    annotationProcessor(

            "javax.persistence:javax.persistence-api",

            "javax.annotation:javax.annotation-api",

            "com.querydsl:querydsl-apt:${queryDslVersion}:jpa")
}
sourceSets {
    main {
        java {
            srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
        }
    }

기존의 Repository와 Querydsl 연동하기

Querydsl을 기존 코드에 연동하기 위해서는 다음과 같은 과정을 따른다

  1. Querydsl을 이용한 인터페이스 선언
  2. 인터페이스 이름 + Impl'이라는 이름으로 클래스 선언 - 이 때 QuerydslRepositorySupport라는 부모 클래스를 지정하고 인터페이스를 구현
  3. 기존의 Repository에는 부모 인터페이스로 Querydsl을 위한 인터페이스를 지정

Querydsl의 목적은 타입 기반으로 코드를 이용해서 JPQL 쿼리를 생성하고 실행하는 것 이때 코드를 만드는 대신 클래스가 Q도메인 클래스이다

public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {

//어떤 타입이든 올 수 있다
    public BoardSearchImpl(Class<?> domainClass) {
        super(domainClass);
    }

    @Override
    public Page<Board> search1(Pageable pageable) {
				//QBoard객체를 생성해서 Querydsl 작성
        QBoard board = QBoard.board;
				//쿼리를 생성하는 부분
        JPQLQuery<Board> query = from(board);
				//쿼리 검색 조건
        query.where(board.title.contains("1"));
				
				//JPQLQuery 실행
				List<Board> list = query.fetch();
				//JPQLQuery count쿼리 실행
        long count = query.fetchCount();

				
        return null;
				//테스트용이기때문에 리턴값은 null
    }
}

테스트코드

void testSearch1() {
        Pageable pageable = PageRequest.of(1,10,Sort.by("bno").descending());

        boardRepository.search1(pageable);

    }
}
//에러나는 경우 QBoard나 QBaseEntity쪽에서 객체가 불러와지지 않았을 수 있으므로
//새로고침이나 build clean 적용해보기

Querydsl로 검색 조건과 목록 처리

다양한 검색 조건을 Querydsl을 이용해서 원하는 JPQL을 생성하고 실행

검색의 경우 ‘제목(t),내용(c),작성자(w)’의 조합을 통해 이루어진다고 가정

SQL문으로 작성할 경우(제목이나 내용에 키워드가 존재하며 bno가 0보다 큰 데이터→검색조건)

select * from board where 
(title like concat('%','1','%') or
content like concat('%','1','%'))
order by bno desc;

//title 또는 content에 1을 포함한 문자열이 있는지 찾는 쿼리문
//concat-> 문자열을 결합하는 명령어
//'%1%' -> 와일드카드를 사용해서 1이라는 숫자가 어느 곳에 위치하던 1이 포함된 문자열을 찾는다

BooleanBuilder

where 조건에 and와 or의 우선 순위는 기본적으로 and > or라서 or 조건은 ‘()’로 묶어준다

Querydsl을 이용할때 ‘()’가 필요한 상황에서 BooleanBuilder를 이용해서 작성

QBoard board = QBoard.board;

        JPQLQuery<Board> query = from(board);

        BooleanBuilder booleanBuilder = new BooleanBuilder(); // 

        booleanBuilder.or(board.title.contains("11")); // title like ...

        booleanBuilder.or(board.content.contains("11")); // content like ....

        query.where(booleanBuilder);
        query.where(board.bno.gt(0L)); //gt-> bno 필드의 값이 0보다 큰지를 확인

검색을 위한 메소드 선언과 테스트

types → 여러 조건의 조합이 가능하도록 처리하는 메소드를 BoardSearch에 정의

Page<Board> searchAll(String[] types, String keyword, Pageable pageable);

BoardSearchImpl 에서 반복문과 제어문을 이용한 처리

@Override
    public Page<Board> searchAll(String[] types, String keyword, Pageable pageable) {

        QBoard board = QBoard.board;
        JPQLQuery<Board> query = from(board); //Querydsl을 사용하기 위해 Qboard라는 동적 쿼리용 엔티티 생성

        if( (types != null && types.length > 0) && keyword != null ){ //검색 조건과 키워드가 있다면

            BooleanBuilder booleanBuilder = new BooleanBuilder(); // (

            for(String type: types){

                switch (type){      //어느 필드에서 검색할지 결정
                    case "t":
                        booleanBuilder.or(board.title.contains(keyword));
                        break;
                    case "c":
                        booleanBuilder.or(board.content.contains(keyword));
                        break;
                    case "w":
                        booleanBuilder.or(board.writer.contains(keyword));
                        break;
                }
            }//end for
            query.where(booleanBuilder);
        }//end if

        //bno > 0
        query.where(board.bno.gt(0L));

        //paging 현재 Querydsl을 가져와 페이징 처리
        this.getQuerydsl().applyPagination(pageable, query);

        List<Board> list = query.fetch();

        long count = query.fetchCount();

        return null;

    }

PageImpl을 이용한 Page<T>반환

페이징 처리의 최종 결과는 Page<T> 타입을 반환하는 것이므로 Querydsl에서는 이를 직접 처리해야하는 불편함이 있다. 따라서 Spring Data JPA에서는 이를 처리하기위해 PageImpl이라는 클래스를 제공해서 3개의 파라미터로 Page<T>를 생성

  • List : 실제 목록 데이터
  • Pageable : 페이지 관련 정보를 가진 객체
  • long : 전체 개수
return new PageImpl<>(list, pageable, count);

0개의 댓글