본문 바로가기

Spring/JPA

3) Spring Data JPA 3부 - @Query , QueryDSL

반응형

3부에서는 실제 데이터를 조회할때 Query를 어떤 식으로 사용하는지에 대해 공부해 보자.

데이터를 조회 할때 아래 소스와 같이 JpaRepository를 상속 받게 되면 기본적으로 find 함수가 제공된다.

이것은 조회하고자 하는 Entity의 @Id 컬럼의 값으로 조회가 되게 된다. 

그러나 실제 Query에서는 여러 컬럼을 값으로 like 조회나 날짜 조회를 사용하게 되는데 이를 하기 위해

3가지 정도의 방법이 사용되는것 같다.

 

첫째 JpaRepository를 이용한 함수명으로 쿼리하기

둘째 @Query 를 사용하기

셋째 QueryDSL 이용하기

 

1. JpaRepository를 이용한 함수명으로 쿼리하기

@Repository
public interface ProductRepository extends JpaRepository<Product, String> {
	Product findByProductName(String productName);
}

위와 같이 Product Entity의 컬럼명으로 findBy컬럼명 으로 함수를 만들면 해당 컬럼명이 조건절로 들어가면서 쿼리가 생성된다.

 

해당함수를 실행한 결과이다.

2021-03-18 09:55:44.331 DEBUG 3404 --- [nio-8080-exec-1] o.h.q.c.internal.CriteriaQueryImpl       : Rendered criteria query -> select generatedAlias0 from Product as generatedAlias0 where generatedAlias0.productName=:param0
2021-03-18 09:55:44.332 DEBUG 3404 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        product0_.product_id as product_1_1_,
        product0_.product_name as product_2_1_ 
    from
        product product0_ 
    where
        product0_.product_name=?
Hibernate: 
    select
        product0_.product_id as product_1_1_,
        product0_.product_name as product_2_1_ 
    from
        product product0_ 
    where
        product0_.product_name=?
2021-03-18 09:55:44.333 TRACE 3404 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [ProductTest4]

2. @Query를 사용하기

@Query("from Product p left outer join fetch p.items where p.productName= :productName")
List<Product> findProducts(@Param("productName") String productName);

위 쿼리 사용하면 아래오 ㅏ같은 쿼리가 생성된다. fetch keyword는 한번의 쿼리로 연관된 items 까지 조회 하겠다는 의미이다. 

아래 로그를 보자.

2021-03-18 10:49:53.571 DEBUG 22372 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        product0_.product_id as product_1_1_0_,
        items1_.item_id as item_id1_0_1_,
        product0_.product_name as product_2_1_0_,
        items1_.item_name as item_nam2_0_1_,
        items1_.item_number as item_num3_0_1_,
        items1_.product_id as product_4_0_1_,
        items1_.product_id as product_4_0_0__,
        items1_.item_id as item_id1_0_0__ 
    from
        product product0_ 
    left outer join
        item items1_ 
            on product0_.product_id=items1_.product_id 
    where
        product0_.product_name=?
Hibernate: 
    select
        product0_.product_id as product_1_1_0_,
        items1_.item_id as item_id1_0_1_,
        product0_.product_name as product_2_1_0_,
        items1_.item_name as item_nam2_0_1_,
        items1_.item_number as item_num3_0_1_,
        items1_.product_id as product_4_0_1_,
        items1_.product_id as product_4_0_0__,
        items1_.item_id as item_id1_0_0__ 
    from
        product product0_ 
    left outer join
        item items1_ 
            on product0_.product_id=items1_.product_id 
    where
        product0_.product_name=?
2021-03-18 10:49:53.578 TRACE 22372 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [ProductTest4]

로그에서 보면 item정보를 다 조회 한다. 

 

이상태로 아래 소스와 같이 Response를 Json으로 parsing해서보내면 StackOverflow가 발생한다.

 

이유는 Product -> Items를 조회하고 각 Items -> Product 다시 조회된 Product ->Items 와 같이 무한루프에 빠지게 된다. 

이를 해결하기 위해서는 여러 방법들이 제시 되고 있다

DTO를 사용한다든지  Json parsing시에 처리를 한다든지 여기서는 간단하게 

@JsonIdentityInfo 를 사용해서 이문제를 해결해 보자.  Product와 Item Entity 들다에 적용을 해야 한다.

package com.devracoon.jpa.entity;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import org.hibernate.annotations.GenericGenerator;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@Entity
@RequiredArgsConstructor
@NoArgsConstructor
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "jsonId")
public class Product {

	@Id
	@GeneratedValue(generator = "uuid2")
	@GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
	@Column(name = "product_id", columnDefinition = "VARCHAR(255)")
	private String productId;

	@NonNull
	@Column(name = "product_name", columnDefinition = "VARCHAR(255)")
	private String productName;

	@OneToMany(mappedBy = "product", targetEntity = Item.class, fetch = FetchType.LAZY, cascade = { CascadeType.DETACH,
			CascadeType.REFRESH, CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE }, orphanRemoval = true)
	private List<Item> items = new ArrayList<Item>();

}
package com.devracoon.jpa.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import org.hibernate.annotations.GenericGenerator;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@Entity
@RequiredArgsConstructor
@NoArgsConstructor
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "jsonId")
public class Item {

    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
    @Column(name = "item_id", columnDefinition = "VARCHAR(255)")
    private String itemId;
    
    @ManyToOne(targetEntity = Product.class ,fetch = FetchType.LAZY )
    @JoinColumn(name = "product_id")
    private Product product;
    
    @NonNull
    @Column(name = "item_name", columnDefinition = "VARCHAR(255)")
    private String itemName;
    
    @Column(name = "item_number", columnDefinition = "VARCHAR(255)")
    private String itemNumber;
}

이상태로 Request를 해보면 그림과 같이 Json String에 jsonId라는 필드가 추가되고 무한루프가 해결되는것을 확인할수 있다.

무한루프의 이문제는 양방향 참조가 되어 있는 모든 Entity에 공통으로 적용되는 문제이다. 

이를 위해 @JsonIdentityInfo 를 이용하는 간단한 방법도 있지만 좀 더 체계화해서 DTO를 만들어

Controller - > Service 간에는 DTO를 Service <-> Repository 간에는 Entity를 이용하는 방법이 더 나은것 같다.

 

3. QueryDSL 이용하기

이제 QueryDSL에 대해 알아보자. QueryDSL를 사용하기위해  gradle.bulild 파일을 수정하자.

plugins {
  id 'org.springframework.boot' version '2.4.3'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
  id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}

group = 'com.devracoon.jpa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation 'org.springframework.boot:spring-boot-starter-jdbc'
  implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
  implementation 'com.querydsl:querydsl-jpa'
  implementation 'com.querydsl:querydsl-apt'
    
  implementation 'com.h2database:h2'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  
  compileOnly 'org.projectlombok:lombok:1.18.6'
  annotationProcessor 'org.projectlombok:lombok:1.18.6'
}


def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

test {
  useJUnitPlatform()
}

그리고 gradle build를 실행하면 그림과 같이 build path 밑에 generated/querydsl 폴더가 생기고 그밑에 

기존 Entity들 이름에 Q가 붙은 class들이 생긴것이 확인된다.

이제 QueryDSL을 이용해서 다이나믹한 쿼리를 만들어보자 . 

public List<Product> findProductByQueryDSL(String productId , String productName) throws Exception{
    	JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
    	QProduct product = QProduct.product; 
    	
    	BooleanBuilder builder = new BooleanBuilder();
    	

        if (!ObjectUtils.isEmpty(productId)) {
            builder.and(product.productId.eq(productId));
        }
        
        if (!ObjectUtils.isEmpty(productName)) {
            builder.and(product.productName.like("%"+productName+"%"));
            
        }
        
    	List<Product> products = queryFactory.selectFrom(product).where(builder).fetch();
    	
    	return products;
    }

쿼리를 java 소스로 코딩한다고 생각하면 될것 같다. text 형태의 Query보다는 코딩양이 많아지기는 하지만 

text 형태 보다는 쿼리를 만들때 실수를 많이 줄일수 있을거 같다.

 

이제 잘 작동하는지 실행을 해보자.

[
    {
        "jsonId": 1,
        "productId": "fc00ddb5-9f05-40a0-beb9-4ea5431379c0",
        "productName": "ProductTest1",
        "items": [
            {
                "jsonId": 2,
                "itemId": "39aacda8-0186-46e7-8c98-1a3637a37ebd",
                "product": 1,
                "itemName": "Product Item 1",
                "itemNumber": "Product Item 1 Number"
            }
        ]
    },
    {
        "jsonId": 3,
        "productId": "02b80cb2-0ffa-4925-be95-a1ec0c0972d9",
        "productName": "ProductTest2",
        "items": [
            {
                "jsonId": 4,
                "itemId": "9556fbf8-2582-4e7c-930f-c29590a8e794",
                "product": 3,
                "itemName": "Product Item 1",
                "itemNumber": "Product Item 1 Number"
            }
        ]
    },
    {
        "jsonId": 5,
        "productId": "e2bb7b24-70f6-4ba8-ab9e-55c840a69674",
        "productName": "ProductTest3",
        "items": [
            {
                "jsonId": 6,
                "itemId": "04f1a742-e5bf-4579-8847-2b257516920d",
                "product": 5,
                "itemName": "Product Item 0",
                "itemNumber": "Product Item 0 Number"
            },
            {
                "jsonId": 7,
                "itemId": "405228ed-6217-44d1-a533-99c4d1104ae6",
                "product": 5,
                "itemName": "Product Item 1",
                "itemNumber": "Product Item 1 Number"
            }
        ]
        .........
        .........

위 결과처럼 like 검색이 잘 된것을 확인 할 수 있다. 

여러가지 쿼리에 대해 다양하게 사용 할 수 있고 익숙해 지면 쿼리를 만들때 실수를 많이 줄 일수 있을거 같아 

앞으로 많이 사용하게 될것 같다.