본문 바로가기

FastCampus - 한번에 끝내는 Java|Spring 웹 개발/04 JPA

JPA Ch 9 @Query (1, 2) - 패스트캠퍼스 챌린지 38일차

앞서 종종 썼던 @Query 에 대해 다룹니다.

아쉬운 점은 이 지점까지 왔는데도 현업에서 쓰인다는 Query DSL 을 다룬 챕터는 보이지 않는다는 건데... 뭐 이거 C#의 LINQ / Entity Framework 에서 쓰이는 fluent API랑 비슷하게 생겼으니 (저는 그걸 공부해본 적이 있으니) 나중에 필요하면 배우면 될 거 같긴 해요.

극단적인 예시

먼저, @Query(value = "JPQL 쿼리문") 처럼 쓰는 것의 유용성 중 한 가지를 보여주기 위해 Spring Data JPA 쿼리메소드로 이름 길이가 미쳐돌아가는 메소드를 만듭니다.

// 테스트는 안 해봤으니, 맞는 이름인지는 직접 IDE에서 확인해주세요.
List<Book> findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualsAndUpdatedAtGreaterThanEquals(String name, LocalDateTime createdAt, LocalDateTime updatedAt);

단계별로 쪼개보면 이해가 가는 쿼리메소드이긴 합니다.

List<Book> findBy \
    CategoryIsNull And \
    NameEquals And \ // (1)
    CreatedAtGreaterThanEquals And \ // (2)
    UpdatedAtGreaterThanEquals( // (3)
        String name,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
    );

물론 위에처럼 메소드 이름을 별모양으로 예쁘게 잘라서 쓰면 컴파일이 안 되겠죠. 호출하는 쪽에서 길어지는 문제도 있고 해서 이거로는 곤란하겠죠.

@Query(value = "JPQL 쿼리문") 으로 쿼리 쓰기

대안은 아시다시피... 위에서 언급했던 JPQL 쿼리문을 쓰는 거겠죠?

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

특이한 점은 이게 앞서도 말했듯이 @Entity 의 필드를 기반으로 한 "JPQL"이라는 겁니다. 실제 필드명은 created_at 같은 걸테니까 필드 이름을 적거나 클래스 이름을 Book 처럼 쓰거나 하면 안 되겠지만, 여기서는 됩니다.

어째서인지 저는 빨간 밑줄이 뜨기는 하는데... 이 S/O로도 해결이 안 되네요. https://stackoverflow.com/q/12420996

value 필드의 문서는 아래와 같습니다.

Defines the JPA query to be executed when the annotated method is called.

@Query(value=) 의 패러미터를 채워넣는 두 가지 방법

위에서 ?1 같이 ?n번째_패러미터_숫자 같은 방법으로 패러미터를 채워넣는 방법이 나왔습니다. 이것 외에도 방법이 있습니다. @Param("이름") 으로 패러미터에 이름을 매기고, :이름 으로 지정하는 방법입니다.

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

@Query 의 또 다른 장점
: @Entity 에 무관하게 DTO를 만들 수 있다

JPQL 로 직접 쿼리를 날리는 거다보니 이 방법을 사용하면 @Entity 로 마킹된 객체가 아닌 생판 다른 객체에 쿼리 결과를 매핑시킬 수 있습니다. 이건 DTO (data transfer object) 같은 스코프를 한정시키는 작업에 굉장히 유용하겠죠.

오늘은 (1), (2) 번 강의를 봤는데 (2) 번 강의 끄트머리에서 나올려다 쏙 들어가고 다음 영상으로 잘리더라구요. 아무튼. 다음 시간에 이 내용이 이어집니다.

트러블슈팅

강의 (1) 에서 대부분의 시간을 소요한 건 길다란 메서드 만들기와 트러블슈팅이었습니다.

트러블슈팅
: @Column( columnDefinition = "칼럼 정의" ) 에 타입을 꼭 쓰자

data.sql 에서 삽입된 Book 객체는 이전에 만들어둔 EntityListener 를 거치지 않으므로 createdAt, updatedAt 필드가 비어있습니다. 이 점을 해결하기 위해 Auto DDL 이 될 부분에 디폴트 값을 정의하는 에 대목이었는데요. (현업에서는 Auto DDL을 잘 안 쓴다고는 하지만...) 겸사겸사 nullable 을 false 로 설정해서 이런 일도 애초에 막아버리구요.

이걸 위해서 다음과 같이 썼습니다.

@Data
@MappedSuperclass // <<<<<<< 이 클래스를 상속받는 클래스들도 부모클래스의 필드를 칼럼으로 사용
@EntityListeners(value = AuditingEntityListener.class)
public abstract class BaseAuditingEntity {
    @CreatedDate
    @Column( columnDefinition = "datetime(6) default now(6)", nullable = false )
    private LocalDateTime createdAt;
    @LastModifiedDate
    @Column( columnDefinition = "datetime(6) default now(6)", nullable = false )
    private LocalDateTime updatedAt;
}

저거 직접 돌려보니 mysql 용이라서 안 돌아가는 건 둘째치고서라도
(그러니까, JPQL이 아니라 특정 DB용 칼럼 정의라는 겁니다!)

Hibernate: 
    
    create table account (
       id int8 generated by default as identity,
        created_at datetime(6) default now(6) not null,
        updated_at datetime(6) default now(6) not null,
        email varchar(255),
        name varchar(255),
        primary key (id)
    )
2022-03-02 22:49:49.042  WARN 9268 --- [    Test worker] o.h.t.s.i.ExceptionHandlerLoggedImpl
  : GenerationTarget encountered exception accepting command
  : Error executing DDL "위에랑 동일한 쿼리" via JDBC Statement

이 부분에 주목해봅시다.

updated_at datetime(6) default now(6) not null,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^

columnDefinition 에 넣으면 타입 부분까지 잠식해버립니다. datetime(6) 부분이 필요하다는거죠. 이걸 강사님은 소스를 보고 파악하셨다는데, 확실히 문서가 양이 적긴 하더라구요. (이어지는 소스읽기 찬양 - 제 생각에는 트레이드오프라는 게 있으니 문서가 좋으면...)

(Optional) The SQL fragment that is used when generating the DDL for the column. Defaults to the generated SQL to create a column of the inferred type.

PGSQL AutoDDL 실행 실패 때의 예외는 좀 많이 불친절하던... 거 어디가 틀렸는지 정도는 좀 알려줍시다 진짜...

PGSQL 용으로 맞추는 가장 빠른 해결법은 역시 JPA가 만들어내는 DDL 쿼리를 보는 것... 아니 DDQ라고 불러야 할까요? 아무튼 대충 실행해보고 채워넣었습니다.

https://www.postgresql.org/docs/13/functions-datetime.html

@Data
@MappedSuperclass // <<<<<<< 이 클래스를 상속받는 클래스들도 부모클래스의 필드를 칼럼으로 사용
@EntityListeners(value = AuditingEntityListener.class)
public abstract class BaseAuditingEntity {
    @CreatedDate
    @Column( columnDefinition = "timestamp default localtimestamp", nullable = false)
    private LocalDateTime createdAt;
    @LastModifiedDate
    @Column( columnDefinition = "timestamp default localtimestamp", nullable = false)
    private LocalDateTime updatedAt;
}

강의에서는 now() 에 숫자가 들어가야하는 추가 오류가 있었습니다. 제 환경은 MySQL variant 가 아니므로 해당이 없습니다.

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online

협약!!!! 협야아아아악!!!! 18점!!!!!게임 하러 갈거야!!!!!!