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)
));
}
}
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 |
+---------------------+-------------------------+----------------------------+---------------------+
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(방언)을 통해 데이터베이스 종류별 서로 다른 쿼리가 생성
물음표와 숫자 기반에 파라미터 매핑 (ORDINAL)
몇번째 파라미터에 있는 값인지 확인하고 치환
꼭 순차적일 필요는 없지만, Java에서는 순서의 의존성을 가진 파라미터는 지양 (파라미터 순서가 바뀌면 결과 변경)
-> 예) ?1, ?2, ?3와 같이 파라미터 입력 순서로 지정
네임 기반 파라미터 매핑
@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);
가독성을 위한 요소도 있지만, Entity에 연결되지 않은 쿼리가 가능하다는 점
-> 현업에서 Book Entity가 있다면 많은 컬럼 가짐
-> 필요한 컬럼만 선택해서 조회가능
Interface
, DTO
, 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));
});
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 형태로도 처리 가능
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());
});
@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