[Spring] 쿼스텀 쿼리 사용

WOOK JONG KIM·2022년 11월 23일
0

패캠_java&Spring

목록 보기
63/103
post-thumbnail

JPA Repository 조건 지정 문제점

JPA Repository에 메소드로 조건을 여러개 설정하는 경우 이름 길어지는 단점이 발생

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
		...

		List<Book> findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(String name, LocalDateTime createdAt, LocalDateTime updatedAt);
}
@SpringBootTest
class BookRepositoryTest {
		...
		
		@Test
    void queryTest(){
        System.out.println("findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual : "
        + bookRepository.findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(
            "JPA 초격자 패키지",
            LocalDateTime.now().minusDays(1L),
            LocalDateTime.now().minusDays(1L)
        ));
    }
}

시간 관련 정보 및 default 설정 (data.sql)

BaseEntity.java에 data.sql을 통해 created_at, updated_at 컬럼 값 넣는 방법

insert into book(`id`, `name`, `publisher_id`, `deleted`) values(1, 'JPA 초격자 패키지', 1, false);
insert into book(`id`, `name`, `publisher_id`, `deleted`) values(2, 'Spring', 1, false);
insert into book(`id`, `name`, `publisher_id`, `deleted`) values(3, 'Spring Security', 1, true);

위의 경우 data.sql에 created_at, updated_at 값이 null로 저장

입력값 넣는 방법

BaseEntity에 @Column(nullable = false)를 통해 필수 입력 값으로 체크하도록 수정 후
-> data.sql 쿼리 수정 (created_at, updated_at)

매번 created_at, updated_at을 추가해줘야하는 불편함 발생

insert into book(`id`, `name`, `publisher_id`, `created_at`, `updated_at`) values(1, 'JPA 초격자 패키지', 1, false, now(), now());
insert into book(`id`, `name`, `publisher_id`, `created_at`, `updated_at`) values(2, 'Spring', 1, false, now(), now());
insert into book(`id`, `name`, `publisher_id`, `created_at`, `updated_at`) values(3, 'Spring Security', 1, true, now(), now());

다른방법

@Column 속성에 columnDefinition 이용 (현업은 AutoDDL을 안쓰므로 잘 안쓰는 속성) -> DDL 시 함께 반영 하도록 하는 옵션

@Data
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public class BaseEntity implements Auditable {
    @CreatedDate
    @Column(columnDefinition = "datetime(6) default now(6)", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(columnDefinition = "datetime(6) default now(6)", nullable = false)
    private LocalDateTime updatedAt;
}

columnDefinition 은 지정 값으로 표시 이후 설정 값은 이어서 표시 (타입 삭제 됌)

//columnDefinition = "default now(6)" -> 옵션에 datetime(6)를 뺀 경우
created_at default now(6) not null,
updated_at default now(6) not null,

//@Column(columnDefinition = "datetime(6) default now(6) null", nullable = false, updatable = false)
// 이 경우 에러, null not null...?
// 즉 기존 속성에 이어붙이는 형태로 작동함
created_at datetime(6) default now(6) null not null,

//@Column(columnDefinition = "datetime(6) default now(6) comment '수정시간'", nullable = false)
updated_at datetime(6) default now(6) comment '수정시간' not null,

// 그냥 now() 해도 에러
//@column 구현체 중 일부
if ( col.columnDefinition().isEmpty() ) {
	sqlType = null;
}
else {
	sqlType = normalizer.applyGlobalQuoting( col.columnDefinition() );
}

문자열 그대로를 치환하여 replace 하는 것을 볼 수 있다

MySQL에 일시를 표시하는 함수 now(), current_timestamp()

now(3), now(6)은 밀리세컨즈 자릿수를 의미 (최대: 6)

mysql> select now(), now(3), now(6), current_timestamp();
+---------------------+-------------------------+----------------------------+---------------------+
| now()               | now(3)                  | now(6)                     | current_timestamp() |
+---------------------+-------------------------+----------------------------+---------------------+
| 2022-11-22 22:15:38 | 2022-11-22 22:15:38.062 | 2022-11-22 22:15:38.062801 | 2022-11-22 22:15:38 |
+---------------------+-------------------------+----------------------------+---------------------+

JPQL을 사용해 긴 메소드 문제 해결

BookRepository

@Query(value = "select b from Book b "
            + "where name = ?1 and createdAt >= ?2 and updatedAt >= ?3 and category is null")
List<Book> findByNameRecently(String name, LocalDateTime createdAt, LocalDateTime updatedAt);

BookRepositoryTest.java

void queryTest(){
        bookRepository.findAll().forEach(System.out::println);

        System.out.println("findByCategoryIsNullNameEqualsAndCreatedAtGreaterThanEqualsAndUpdateAtGraterThanEqual"
        + bookRepository.findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(
                "JPA 초격차 패키지",
                LocalDateTime.now().minusDays(1L),
                LocalDateTime.now().minusDays(1L)
        ));

        System.out.println("findByNameRecently : " + bookRepository.findByNameRecently("JPA 초격차 패키지", LocalDateTime.now().minusDays(1L),
                LocalDateTime.now().minusDays(1L)));
    }
findByCategoryIsNullNameEqualsAndCreatedAtGreaterThanEqualsAndUpdateAtGraterThanEqual[Book(super=BaseEntity(createdAt=2022-11-23T11:44:23, updatedAt=2022-11-23T11:44:23), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false)]

findByNameRecently : [Book(super=BaseEntity(createdAt=2022-11-23T11:44:23, updatedAt=2022-11-23T11:44:23), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false)]

@Query에 사용된 쿼리 문법을 JPQL이라고 부름 (데이터베이스 쿼리 X)
-> JPA 엔티티 기반의 쿼리

Book은 테이블이 아닌 엔티티로서 쿼리에 사용(name, createdAt, updatedAt도 마찬가지)
-> 동작 쿼리에는 from book 이라고 들어감

JPQL에 쿼리는 Dialect(방언)을 통해 데이터베이스 종류별 서로 다른 쿼리가 생성


JPQL에 동적으로 파라미터를 설정하는 방법 2가지

  1. 물음표와 숫자 기반에 파라미터 매핑 (ORDINAL)

몇번째 파라미터에 있는 값인지 확인하고 치환

꼭 순차적일 필요는 없지만, Java에서는 순서의 의존성을 가진 파라미터는 지양 (파라미터 순서가 바뀌면 결과 변경)
-> 예) ?1, ?2, ?3와 같이 파라미터 입력 순서로 지정

  1. 네임 기반 파라미터 매핑

@Param과 :을 사용하여 값을 매핑

순서와 상관이 없어 파라미터의 변경여부와 상관없이 결과값 동일

-> 예) @Param("name")로 선언 된 파라미터와 :name이 연결 상태

@Query(value = "select b from Book b "
        + "where name = :name and createdAt >= :createdAt and updatedAt >= :updatedAt and category is null")
List<Book> findByNameRecently(
        @Param("name") String name,
        @Param("createdAt") LocalDateTime createdAt,
        @Param("updatedAt") LocalDateTime updatedAt);

@Query 사용 이유

가독성을 위한 요소도 있지만, Entity에 연결되지 않은 쿼리가 가능하다는 점
-> 현업에서 Book Entity가 있다면 많은 컬럼 가짐
-> 필요한 컬럼만 선택해서 조회가능

Interface, DTO, Tuple 사용

1. Tuple 사용

@Query(value = "select b.name as name, b.category as category from Book b")
List<Tuple> findBookNameAndCategory();
bookRepository.findBookNameAndCategory().forEach(tuple -> {
    System.out.println(tuple.get(0) + " : " + tuple.get(1));
});

2. interface 사용

public interface BookNameAndCategory {
    String getName();
    String getCategory();
}
@Query(value = "select b.name as name, b.category as category from Book b")
List<BookNameAndCategory> findBookNameAndCategory();
bookRepository.findBookNameAndCategory().forEach(b -> {
    System.out.println(b.getName() + " : " + b.getCategory());
});

Tuple을 interface 형태로도 처리 가능

3.DTO 사용

JPQL 내에서는 자바 객체 활용 가능

@Query(value = "select new com.example.bookmanager.repository.dto.BookNameAndCategory(b.name, b.category) from Book b")
List<BookNameAndCategory> findBookNameAndCategory();
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BookNameAndCategory {
    private String name;
    private String category;
}
bookRepository.findBookNameAndCategory().forEach(b -> {
    System.out.println(b.getName() + " : " + b.getCategory());
});

JPQL을 사용해서 Paging처리

@Query(value = "select new com.example.bookmanager2.repository.BookNameAndCategory(b.name, b.category) from Book b")
Page<BookNameAndCategory> findBookNameAndCategory(Pageable pageable);
/*
	select 조회 결과
	JPA 초격자 패키지 : null
	Spring : null
*/

bookRepository.findBookNameAndCategory(PageRequest.of(1, 1)).forEach(
      bookNameAndCategory -> System.out.println(bookNameAndCategory.getName() + " : " + bookNameAndCategory.getCategory()));
//결과: Spring : null


bookRepository.findBookNameAndCategory(PageRequest.of(0, 1)).forEach(
        bookNameAndCategory -> System.out.println(bookNameAndCategory.getName() + " : " + bookNameAndCategory.getCategory()));
//결과: JPA 초격자 패키지 : null
profile
Journey for Backend Developer

0개의 댓글