JPA ,Query 어노테이션, JPQL, DTO 매핑, function 사용
JPA 장점
- SQL문이 아닌 Method를 통해 DB를 조작할 수 있어 개발자가 비즈니스 로직 구성에 좀 더 집중할 수 있음
- 쿼리문 등의 부수적인 코드가 줄어들어 가독성이 높아짐
- 오직 객체지향적 접근만 고려되므로 생산정 증가
- 매핑 정보가 Class로 명시되어 있어서 ERD를 보는 의존도를 낮출 수 있고 유지보수 및 리팩토링에 유리
JPA 단점
- 프로젝트의 규모가 크고 복잡하여 설계가 잘못된 경우, 속도 저하 및 일관성을 무너뜨리는 문제점이 생길 수 있음
- 복잡하고 무거운 Query는 속도를 위해 별도의 튜닝이 필요하기 때문에 결국 SQL문을 써야할 수도 있음
JPA가 쿼리를 자동으로 생성해주지만 상황에 따라 직접 쿼리를 작성할 필요가 생긴다.
JPA에서 직접 쿼리를 작성할 수 있는 방법
- JPQL 작성
- 네이티브 쿼리(일반 SQL) 작성
JPQL은 JPA의 일부분으로 정의된 플랫폼 독립적인 객체지향 쿼리 언어이다.
네이티브 쿼리는 데이터베이스를 바라보고 작성한다면 JPQL은 엔티티 클래스를 바라보고 작성해야 한다.
JPQL에서는 대 소문자 구분을 하고 select, from과 같은 키워드는 구분하지 않는다.
@Query 어노테이션
JpaRepository에서 쿼리(JPQL, native query 둘 다)를 직접 작성해줄 때 사용한다.
메서드 명은 기존 자동생성 방식과 달리 자유롭게 작성할 수 있다.
그리고 nativeQuery라는 속성을 이용하여 JPQL로 작성한 것인지 SQL로 작성한 것인지 구분할 수 있다.
- nativeQuery = false(default) → JPQL
- nativeQuery = true → SQL
Entity : Broadcasting (방송 정보)
@Entity
@Table
@Data
public class Broadcasting {
@Id
@Column(name = "bc_seq")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long bcSeq;
@Column(name = "bc_title", nullable = false, length = 400)
private String bcTitle;
Entity : ViewerReaction (시청자 반응)
@Entity
@Table(name = "viewer_reaction")
@Data
public class ViewerReaction {
@Id
@Column(name ="vr_seq")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long vrSeq;
@Column(name ="vr_viewers", nullable = false)
private int vrViewers;
@Column(name ="vr_sales", nullable = false)
private int vrSales;
@Column(name ="vr_comments", nullable = false)
private int vrComments;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bc_seq")
private Broadcasting broadcastingVO;
}
1. @Query 사용 방법 (일반 쿼리)
JPQL은 엔티티의 이름과 속성명을 사용하고, 일반 SQL은 설계된 테이블명과 칼럼명 사용
@Repository
public interface ViewerReactionRepository extends JpaRepository<ViewerReaction, Long> {
// JPQL 사용
@Query(value = "select vr.vrSeq from ViewerReaction vr")
List<Long> selectAllVrSeq();
// 일반 SQL 사용
@Query(value = "select vr.vr_seq from viewer_reaction vr", nativeQuery = true)
List<Long> selectAllVr_seq();
}
2. @Query 사용 방법 (파라미터 사용)
@Repository
public interface ViewerReactionRepository extends JpaRepository<ViewerReaction, Long> {
// JPQL 일반 파라미터 쿼리, @Param 사용 X, 엔티티 변수명 "vrSeq" 사용
@Query(value = "select vr from ViewerReaction " +
"vr where vr.vrSeq = ?1")
List<ViewerReaction> selectAllByVrSeq(long seq);
// 일반 SQL문 파라미터 쿼리 ,@Param 사용 X, 테이블 컬럼명 "vr_seq" 사용
@Query(value = "select * from viewer_reaction " +
"where vr_seq = ?1", nativeQuery = true)
List<ViewerReaction> selectAllByVr_seq(long seq);
// JPQL 일반 파라미터 쿼리, @Param 사용 O
@Query(value = "select vr from ViewerReaction vr where vr.vrSeq = :seq")
List<ViewerReaction> selectAllByVrSeq2(@Param(value = "seq") long seq);
// 일반 SQL문 파라미터 쿼리 ,@Param 사용 O
@Query(value = "select * from viewer_reaction " +
"where vr_seq = :seq", nativeQuery = true)
List<ViewerReaction> selectAllByVr_seq2(@Param(value = "seq") long seq);
// JPQL 객체 파라미터 쿼리, :#{#객체명}으로 사용
@Query(value = "select vr.vrTitle from ViewerReaction vr " +
"where vr.vrSeq < :#{#bc.bcSeq}")
List<String> selectVrTitleByBc(@Param(value = "bc") Broadcasting bc)
// 일반 SQL문 객체 파라미터 쿼리, :#{#객체명}으로 사용
@Query(value = "select vr_title from viewer_reaction "
+"where vr_seq < :#{#bc.bcSeq}", nativeQuery = true)
List<String> selectVrTitleByBc(@Param(value = "bc") Broadcasting bc)
3. @Query 사용 방법(집계 함수 단독)
객체 파라미터를 사용할 때 여기서는 ManyToOne 매핑이 되어 ViewerReaction 엔티티가 Broadcasting의 매핑 주인이 된다.
그러므로, JPQL 사용 할 때에는 외래키 속성 변수가 객체 형태가 되므로 주의해서 참고
@Repository
public interface ViewerReactionRepository extends JpaRepository<ViewerReaction, Long> {
// 집계함수 SUM 사용
// JPQL 사용, broadcastingVO 형태
@Query(value = "select SUM(vr.vrSales) from ViewerReaction vr "
+ "where vr.broadcastingVO = :#{#bc}")
Integer vrSalesSum(@Param("bc") Broadcasting bc);
// 일반 SQL 사용, 테이블에선 컬럼이 객체 형태가 아님
@Query(value = "select SUM(vr.vr_sales) from viewer_reaction vr "
+ "where vr.bc_seq = :bcSeq", nativeQuery = true)
Integer vrSalesSum(@Param("bcSeq") long bcSeq);
DTO Mapping
JPQL을 작성하면서 여러 function
과 join
을 사용하다보면 결과가 Entity 형태로 나오지 않을 경우도 있다. 이를 위해 DTO 반환이 필요하다.
@Query 어노테이션을 사용하여 DTO 반환을 하기 위해서는 select 구분에서 생성자를 통해 객체를 반환해야 한다.
Entity : User
@Entity
@Table(name = "user")
@Data
public class User {
@Id
private String id;
private String name;
private String phone;
private String deptId;
}
JpaRepository
public interface UserRepository extends JpaRepository<User, String> {
@Query(value = "select u from User u where u.name = :name")
List<User> findByName(@Param("name") String name);
}
DTO
@Data
public class UserDTO {
private String id;
private String name;
private String deptId;
private String deptName;
}
@Query로 DTO 반환하기
public interface UserRepository extends JpaRepository<User, String> {
@Query(value = "select" +
"new com.yoonkie.dto.UserDto(u.id, u.name, u.deptId, d.deptName " +
"from User u left outer join Dept d on u.deptId = d.deptId")
List<UserDTO> findUserDept();
}
SQL Function
JPQL에서는 기본적으로 select 구문의 max, min, count, sum, avg를 제공하며 기본 function
으로는 COALESCE, LOWER, UPPER 등을 지원하며 자세한 Function은 문서를 참고하면 된다.
public interface UserRepository extends JpaRepository<User, String> {
@Query(value = "select max(u.id) " +
"from User user " +
"where u.deptId is not null")
String findMaxUserId();
}
이러한 JPQL에서 기본적으로 지원하는 ANSI Query Function
만으로는 비지니스 조회를 해결하기에 한계가 존재한다.
DataBase Function
을 사용하는 방식은 JPQL에서 function()
을 활용하여 hibernate에 등록된 각 DataBase의 Dialect에 정의된 function
을 사용하는 방법이 있다.
public interface UserRepository extends JpaRepository<User, String> {
@Query(value = "select function('date_format', :date, '%Y/%m/%d') " +
"from User u"
String findNow(@Param("date") LocalDateTime date);
}
하지만, hibernate에서 기본적으로 등록되는 function에서도 누락되는 function이 존재한다. 이러한 경우 MetadataBuilderContributor
의 구현체를 구현하는 방식으로 사용할 수 있다.
(이전에는 Dialect를 상속받아 구현하는 방식 사용)
public class MyMetadataBuilderContributor implements MetadataBuilderContributor{
@Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction("JSON_EXTRACT", new StandardSQLFunction("JSON_EXTRACT", StringType.INSTANCE))
.applySqlFunction("JSON_UNQUOTE", new StandardSQLFunction("JSON_UNQUOTE", StringType.INSTANCE))
.applySqlFunction("STR_TO_DATE", new StandardSQLFunction("STR_TO_DATE", LocalDateType.INSTANCE))
.applySqlFunction("MATCH_AGAINST", new SQLFunctionTemplate(DoubleType.INSTANCE, "MATCH (?1) AGAINST (?2 IN BOOLEAN MODE)"))
}
}
applySqlFunction
의 첫번째 파라미터는 JPQL에서 function(”함수명”)
에서 함수명에 해당하는 등록명이다.
StandardSQLFunction
은 기본적인 함수를 등록하기 위한 Class로 생성자의 첫번째 파라미터는 실제 DataVase Function명
이며, 두번째 파라미터는 function의 리턴 타입
이 되야 한다.
StandardSQLFunction
의 경우 파라미터는 함수에 순서에 맞게 JPQL function(’등록 함수명’, 파라미터1, 파라미터2, …) 같이 정의하여 사용하면 된다.
SQLFunctionTemplete
은 문법이 존재하는 function을 등록할 때 사용가능하며, 첫번째 파라미터가 function의 리턴타입
이고, 두번째 파라미터가 function
이다.
?1, ?2와 같이 명시하여 JPQL function()에서 전달되는 파라미터의 순서대로 파싱된다.
public interface UserRepository extends JpaRepository<User, String> {
@Query(value = "select u from User u " +
"where function('JSON_UNQUOTE', function('JSON_EXTRACT', u.phone, '$.id')) = 'admin' ")
List<User> findAllByPhoneAdmin();
}
public interface UserRepository extends JpaRepository<User, String> {
@Query(value = "select user from User u" +
"where function('MATCH_AGAINST', u.name, :name) > 0")
List<User> findAllByName(@Param("name") String name);
}
댓글남기기