대부분의 JPQL의 한계는 대부분 해소 가능하지만 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다.
위 같은 문제를 해결하기 위해서 사용하는 것이 QeryDSL 이다.
QueryDSL은 하이버네이트 쿼리 언어(HQL: Hibernate Query Language)의 쿼리를 타입에 안전하게 생성 및 관리해주는 프레임워크이다.
정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원.
문자열이나 XML 파일을 통해 쿼리를 작성하는 대신에 QueryDSL에서 제공하는 플루언트(fluent) API를 통해 쿼리 생성이 가능하다.
/*pom.xml*/
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<scope>provided</scope>
</dependency>
<plugins> 태그에 QueryDSL을 사용하기 위한 APT 플러그인 추가 <plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
<options>
<querydsl.entityAccessors>true</querydsl.entityAccessors>
</options>
</configuration>
</execution>
</executions>
</plugin>
APT란?
APT(Annotation Processing Tool)는 어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능이다. JDK 1.6부터 도입된 기능이며, 클래스를 컴파일 하는 기능도 제공한다.
위처럼 의존을 추가한 뒤 메이븐의 compile 단계를 클릭하여 빌드 작업을 수행한다.
완료 시, target 디렉토리에 generated-resource 경로에 Q도메인 클래스가 생성된다.
QueryDSL에서는 Qdomain이라는 쿼리 타입의 클래스를 자체적으로 생성하여 메타데이터로 사용하는데, 이를 통해 SQL 쿼리를 생성하여 제공함.
- JPAQuery를 활용한 QueryDSL 테스트 코드
@PersistenceContext
EntityManager entityManager;
@Test
void queryDslTest(){
JPAQuery<Product> query = new JPAQuery<>(entityManager);// 쿼리 작성을 위한 객체
QProduct qProduct = QProduct.product; // QueryDSL에서 사용할 도메인 객체
List<Product> productList = query
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for(Product product : productList){
System.out.println("---------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("---------------");
}
}
JPAQuery 객체는 엔티티 매니저를 활용해 생성하며, 빌더 형식으로 쿼리를 작성한다.
fetch() 메서드
List<T> fetch() : 조회 결과를 리스트로 반환.T fetchOne() : 단 건의 조회 결과를 반환.T fetchFirst() : 여러 건의 조회 결과 중 1건을 반환.Long fetchCount() : 조회 결과 갯수 반환.QueryResult<T> fetchResults() : 조회 결과 리스트와 갯수를 포함한 QueryResults를 반환. @Test
void queryDslTest2(){
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for(Product product : productList){
System.out.println("---------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("---------------");
}
}
@Test
void queryDslTest3(){
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for(String product : productList){
System.out.println("--------------");
System.out.println("Product Name : " + product);
System.out.println("--------------");
}
// 튜플 2가지 이상 조회
List<Tuple> tupleList = jpaQueryFactory
.select(qProduct.name, qProduct.price)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for(Tuple product : tupleList){
System.out.println("--------------");
System.out.println("Product Name : " + product.get(qProduct.name));
System.out.println("Product price : " + product.get(qProduct.price));
System.out.println("--------------");
}
}
List<Tuple> 타입으로 지정한다.- QueryDSL configuration 파일 생성
@Configuration
public class QueryDSLConfiguration{
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
@PersistenceContext 란?
EntityManager를 빈으로 주입할 때 사용하는 어노테이션.
@PersistenceContext로 지정된 프로퍼티에 아래 두 가지중 한가지로 EntitiyManager를 주입해준다.
@Test
@DisplayName("쿼리 생성 관련 설정을 Config 파일에 별도로 정의한 뒤 의존 주입만 하여 해당 객체를 사용한다.")
void queryDslTest4(){
QProduct qProduct = QProduct.product;
List<String> productList = jpaQueryFactory // 의존 주입한 빈 객체
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy((qProduct.price.asc()))
.fetch();
for(String product : productList){
System.out.println("--------------");
System.out.println("Product Name : " + product);
System.out.println("--------------");
}
}
public interface QProductRepository extends JpaRepository<Product, Long>
, QuerydslPredicateExecutor<Product>{
}
@Autowired
QProductRepository qProductRepository;
@Test
public void queryDSLTest1(){
Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
.and(QProduct.product.price.between(1000,2500));
Optional<Product> foundProduct = qProductRepository.findOne(predicate);
if(foundProduct.isPresent()){
Product product = foundProduct.get();
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
@Test
public void queryDSLTest2(){
QProduct qProduct = QProduct.product;
Iterable<Product> productList = qProductRepository.findAll(
qProduct.name.contains("펜")
.and(qProduct.price.between(1000,2500))
);
for(Product product : productList){
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
가장 보편적인 방법은 CustomRepository를 활용해 레포지토리를 구현하는 방식이 있다.
QuerydslRepositorySupport를 사용하기 위한 상속 구조.

- ProductRepositoryCustom 인터페이스
public interface ProductRepository{
List<Product> findByName(String name);
}
@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport
implements ProductRepositoryCustom{
public ProductRepositoryCustomImpl(){
super(Product.class);
}
@Override
public List<Product> findByName(String name) {
QProduct qProduct = QProduct.product;
List<Product> productList = from(qProduct)
.where(qProduct.name.eq(name))
.select(qProduct)
.fetch();
return productList;
}
}
QuerydslRepositorySupport를 상속받기 위해 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 한다.
Q도메인 클래스를 사용하여 QuerydslRepositorySupport에서 제공하는 기능인 from() 메서드를 사용한다. 이는 어떤 도메인에 접근할 것인지 지정하는 기능을 한다.
from() 메서드 수행 후 JPAQuery 객체를 반환함.
기존에 Product 엔티티 클래스와 매핑하여 사용하던 ProductRepository가 존재하는 경우, ProductRepositoryCustom을 상속받아 사용 가능하다.
public interface ProductRepository extends JpaRepository<Prouct,Long>, ProductRepositoryCustom{
}
@SpringBootTest
public class ProductRepositoryTest {
@Autowired
ProductRepository productRepository;
@Test
void findByNameTest(){
List<Product> productList = productRepository.findByName("펜");
for(Product product : productList){
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
}
JPA에서 Audit 이란 '감시하다'라는 뜻으로 누가 언제 데이터를 생성했고 변경했는지 감시한다는 의미로 사용된다. 클래스에는 공통적으로 들어가는 필드가 있다. 일반적으로 '생성일자'와 '수정 일자'가 있다.
이러한 필드들은 매번 엔티티를 생성하거나 변경할 대마다 값을 주입해야 한다는 번거로움이 있다. 이를 해결하기 위해 Spring Data JPA에서는 이러한 값을 자동으로 넣어주는 기능을 제공한다.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration{
}
@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(insertable = false)
private LocalDateTime updatedAt;
}
@MappedSuperclass : JPA의 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달.
@Entitylisteners : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 하는 어노테이션.
AuditingEntityListener : 엔티티의 Auditing 정보를 주입하는 JPA 엔티티 리스너 클래스.
@CreatedDate : 데이터 생성 날짜를 자동으로 주입하는 어노테이션.
@LastModifiedDate : 데이터 수정 날짜를 자동으로 주입하는 어노테이션.
@Entity
@Getter
@Setter
@ToString(callSuper = true)
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Table(name="product")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable=false)
private String name;
@Column(nullable=false)
private Integer price;
@Column(nullable=false)
private Integer stock;
}
BaseEntity를 상속받은 Product 엔티티 클래스
@ToString, @EqualsAndHashCode 어노테이션에 적용한 callSuper 속성은 부모 클래스의 필드를 포함하는 역할을 수행하는 것을 의미한다.
Auditing 적용한 테스트 코드
@Test
public void auditingTest(){
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(100);
Product savedProduct = productRepository.save(product);
System.out.println("productName : " + savedProduct.getName());
System.out.println("createdAt : " + savedProduct.getCreatedAt());
}
엔티티 종류
One To One
One To Many
Many To One
Many To Many
관계는 어떤 엔티티를 중심으로 연관 엔티티를 보느냐에 따라 연관관계의 상태가 달라진다.
JPA를 사용하는 객체지향 모델링에서는 엔티티 간 참조 방향을 설정할 수 있다.
일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행할 수 있다.