Java에서 @Query는 데이터베이스에서 실행할 사용자 지정 SQL 쿼리를 지정하기 위해 Spring Data JPA에서 사용되는 어노테이션 입니다.
@Query를 사용할 때 개발자는 SQL 쿼리를 문자열 값으로 지정할 수 있습니다. 또한 파라미터, 인덱스에 적용이 가능합니다. 명명된 매개변수는 ":parameterName"구문을 사용하여 지정되며 인덱싱된 매개변수는 "?:index"구문을 사용하여 지정됩니다. 또한 @Param어노테이션을 사용하여 매개변수의 이름을 지정할 수도 있습니다.
SQL쿼리가 여러 행을 반환하는 경우 List, Set 또는 Obejct와 같은 당야한 옵션을 사용하여 메서드의 반환 유형을 지정할 수 있습니다. 또한 @Modifying 어노테이션을 같이 사용하는 경우 해당 쿼리가 데이터베이스를 수정할 것을 나타냅니다.
Repository
findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEuqualAndUpdatedAtGreaterThanEqual(String name, LocalDateTime created_at, LocalDateTime updated_at);
해당 코드를 보면 아주 가독성이 떨어지는 것을 확인할 수 있습니다. 메서드 이름이 쿼리문이기 때문 입니다. @Query를 사용하면 다음과 같습니다.
@Query(value = "select b from Book b where name = ?1 and createdAt >=?2 and updatedAt >= ?3 and category is null")
List<Book> findByNameRently(String name, LocalDateTime createdAt, LocalDateTime updatedAt)
위의 예시 모두 좋아보이진 않지만 메서드 이름이 엄청 긴 것보다는 쿼리문으로 나타내는 것이 더 좋아보입니다.
하지만 해당 코드에는 문제점이 있습니다. 파타미터의 위치가 바뀌는 경우 잘못된 값을 집어넣고 에러를 불러일으킵니다. 이러한 실수를 방지하기 위하여 @Param을 사용합니다.
@Query(value = "select b from Book b where name = :name and createdAt >= :createdAt and updatedAt >= :updatedAt and category is null")
List<Book> findByRecently(
@Param("name") String name,
@Param("createdAt") LocalDateTime createdAt,
@Param("updatedAt") LocalDateTime updatedAt
)
앞선 코드들 보다 가독성이 좋고 위치 상관없이 알맞게 파라미터를 넣을 수 있습니다.
앞선 예시 처럼 가독성이 떨어지는 경우에 사용하는 것도 있지만 데이터의 규모가 커지는 경우에는 다음과 같은 형식도 사용합니다.
Repository
@Query(value = "select b.name as name, b.category as category from Book b")
List<Tuple> findBookNameAndCategory();
//또는
@Query(value = "select new com.example.bookreview.repository.dto.BookNameAndCategory(b.name, b.category) from Book b")
List<Tuple> findBookNameAndCategory();
//페이징 사용
@Query(value = "select new com.example.bookreview.repository.dto.BookNameAndCategory(b.name, b.category) from Book b")
Page<BookNameAndCategory> findBookNameAndCategory();
dto
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookNameAndCategory {
private String name;
private String category;
}
규모 점점 커지는 경우 일일히 @Param을 사용하기에는 무리가 있습니다. 위의 예시에서는 2개의 정보만 매핑을 하였지만 실제 서비스에 사용되는 데이터는 더 많을 것입니다.
Spring Data JPA에서 데이터베이스에 대해 직접 실행되는 SQL 쿼리입니다. JPA엔티티에 대한 자동 번역 및 매핑을 하며 JPQL(Java Persistance Query Language)에 대한 기능을 사용하기에 충분하지 않은 경우에 사용됩니다.
Spring에서 사용할 경우 @Query 어노테이션에 nativeQuery 속성을 true로 설정할 수 있습니다.
NativeQuery는 SQL 쿼리 실행에 대한 더 많은 제어를 제공하며 데이터베이스별 기능을 사용하거나 JPA에서 생성한 것보다 더 효율적인 성능을 위해 작성합니다. 즉, JPQL에서 지원하지 않는 영역이 필요한 경우 또는 성능이 더 나은 경우에 사용합니다.
NativeQuery는 여러 테이블, 복잡한 조인, 하위 쿼리가 포함된 복잡한 쿼리를 실행할 때 사용합니다. 예를 들어 엔티티 내용을 업데이트 해야 하는 경우 JPQL에서 제공하는 메서드를 사용하면 다음과 같습니다.
NativeQuery 미사용
@Test
void queryTest(){
List<Book> books = repository.findAll();
for(Book b : books){
book.setName("Need Native");
}
repository.saveAll(books);
}
쿼리문
...
where
b1_0.id=?
엔티티의 id값을 가져와서 update를 진행하는 것을 확인할 수 있습니다. 즉, 쿼리문이 데이터의 개수 만큼 생성되는 것을 확인할 수 있습니다. 데이터가 적은 경우에는 문제가 되지 않지만 데이터가 클 수록 속도가 많이 줄어듭니다. nativeQuery를 사용하면 쿼리문에 적힌 내용만 실행하기 때문에 쿼리문이 한번만 작성됩니다.
NativeQuery 사용
@Transactional
@Modifying
@Query(value = "update book set name = :name", nativeQuery = true)
int updateNativeQuery(String name);
@Test
void queryTest() {
System.out.println("count : " + bookRepository.updateNativeQuery("Need Native"));
}
쿼리문
Hibernate:
update
book
set
name = ?
count : 5
5개의 데이터를 업데이트 하였지만 한번만 실행하는 것을 확인할 수 있습니다.
NativeQuery를 사용하면 주의해야 할 점이 있습니다. 말 그대로 Native이기 때문에 JPA에서 설정한 제약사항은 네이티브에 적용되지 않습니다. Book 이라는 엔티티에는 deleted
값이 false이면 표시 되지 않게 하는 어노테이션을 적용하였지만 NativeQuery를 사용하므로써 적용되지 않았습니다. 또한 @Transactional 어노테이션을 사용하지 않으면 변경 사항이 데이터베이스와 자동으로 동기화 되지 않습니다. 즉, 데이터에 대한 변경 사항이 일치 하지 않아 에러가 나게 됩니다. 해당 어노테이션을 사용하여 NativeQuery 사용전에 트랜잭션을 시작하고 업데이트 이후 트랜잭션을 커밋하기 위해 사용합니다.
NativeQuery를 사용하면 코드가 데이터베이스에 더 종속될 수 있으며 SQL 주입 공격의 위험이 증가할 수 있습니다. 필요한 경우에만 사용해야 하며 쿼리에 사용된 데이터의 유효성을 검사해야 합니다.