vue + jpa project (29) - 공통코드 join & fetch 심화적용 본문

프로그램/Vue.js

vue + jpa project (29) - 공통코드 join & fetch 심화적용

반응형

이번 장에서는 그룹코드 기준으로  Join 공통코드 리스트에 대한 백엔드를 좀 더 개선시켜보겠다.

 

select 절이라던지 where 조건에 실제 사용가능 할 만한 것들을 한가지씩 해보겠다.

 

첫번째 패턴인 상세코드를 기준으로 한 연관관계 조회는 사용할 수 없어 두번째 패턴으로 진행한다.

 

A. select 절에 subquery 적용

 

1.  공통코드 레파지토리 수정

   select 절과 Dto의 순서를 위해서 필드 조정과 subquery  절을 아래와 같이 추가한다.

... 중략
JPAQuery<CodeGroupEntity> query = queryFactory.selectFrom(a)
		.leftJoin(a.codeDetailEntity, b)
		.on(codeDetailDto.makeSearchCondition(a, "use_yn"))
		.where(
... 중략
			);

long total = query.stream().count();

List<CodeGroupDto> results = query
		.select(Projections.bean(CodeGroupDto.class, a.groupCode, a.groupName, a.useYn, 
				StringUtils.getEntityDateTimeFormat(a.regDate).as("regDate"), 
				StringUtils.getEntityDateTimeFormat(a.updDate).as("updDate"),
				b.codeValue, b.codeName, b.sortSeq, 
				StringUtils.getEntityDateTimeFormat(b.regDate).as("detailRegDate"), 
				StringUtils.getEntityDateTimeFormat(b.updDate).as("detailUpdDate"),
				Expressions.as(JPAExpressions.selectFrom(b).select(b.count()).where(a.groupCode.eq(b.groupCode)),"count")))
		.offset(pageable.getOffset())
		.limit(pageable.getPageSize())
		.fetch();
... 중략

많이 바뀌지는 않았고, 순서와 마지막 항목인 select 절이 보일것이다. 

잘 보면 하나의 subquery 인데 b테이블 즉 code_detail 의 개수를 count 하는데 그 조건은 a(code_group)의 group_code와

일치하는 건에 대해서 카운트 하는 것이다. 마지막에 count라는 것으로 alias를 부여하였다.

 

이것으로 끝나면 좋겠지만 현재 CodeGroupDto에는 count 변수가 없다. 

그래서 아래와 같이 수정하자.

 

2. 그룹코드 Dto 수정

   그냥 count 숫자 변수 하나만 더 추가했다.

... 중략
    private String codeValue;
    private String codeName;
    private int sortSeq;
    private String detailRegDate;
    private String detailUpdDate;
    
    private long count;  <-- 추가
... 중략

이제 실행시켜서 보면 화면에는 나오지 않지만 웹브라우저 게발자 콘솔(F12)에 네트웍크 탭에 리스트를 한번 살펴보면

count 값이 잘 찍혀서 나온다. 그룹코드 A01 에 해당하는 상세코드 개수는 23개, A02에 해당하는 상세개수는 1개로 잘 나온다.

쿼리도 역시 잘 나온다.

 

 

B. where 절에 subquery 적용

 

1.  공통코드 레파지토리 추가 수정

    아래 코드를 보면 두 줄이 추가되었는데 첫번째 줄은 exists 구분이며 두번째 줄은 특정 값에 대해서 

    subquery로 비교하는 부분이다.

...중략
JPAQuery<CodeGroupEntity> query = queryFactory.selectFrom(a)
        .leftJoin(a.codeDetailEntity, b)
        .on(codeDetailDto.makeSearchCondition(a, "use_yn"))
        .where(
                // searchKey를 위해서 각 항목별로 호출이 필요하다.
                codeDetailDto.makeSearchCondition(a, "group_code"), 
                codeDetailDto.makeSearchCondition(a, "group_name"), 
                codeDetailDto.makeSearchCondition(b, "code_value"), 
                codeDetailDto.makeSearchCondition(b, "code_name"),  
                codeDetailDto.makeSearchCondition(a, "use_yn"),     
                codeDetailDto.makeSearchCondition(a, "reg_date"),   
                // 두줄 추가
                JPAExpressions.select(Expressions.constant(1)).from(a).where(a.groupCode.eq(b.groupCode)).exists(),
                a.groupCode.eq(JPAExpressions.select(b.groupCode.max()).from(b).where(a.groupCode.eq(b.groupCode)))
            );
...중략

수정하고 한번 실행시켜 보자. 

select 절이 바뀐게 아니라서 그냥 쿼리만 봐보겠다. 

첫번째 줄에 대한 exists와 상세테이블의 max(group_code)에 대한 건을 가져오도록 되어있다. 

생각보다 자동으로 잘 만들어진다.

 

 

C. where 절에  DB function 을 호출하여 subquery 적용

 

     이 부분을 찾는데 상당한 시간이 걸렸다. 대부분 간략히만 서술이 되어 있거나 아니면 이미 deprecated 되어서 

     추가 버전 갱신이 필요한 경우가 많고 어떻게 적용해야하는지도 각양각색이어서 쉽지 않았다. 

     일단 지금 내가 가지고 있는 버전에 맞춰서 진행하겠다. 그리고 Mariadb를 기준으로 하였다.

 

     우선 DB에서 함수를 두가지를 만들겠다. 

 

1.  DB 함수 생성 (with DBeaver)

CREATE FUNCTION test.fn_get_groupname(`p_group_code` VARCHAR(50))
RETURNS varchar(100) CHARSET utf8
BEGIN
	
 DECLARE _result VARCHAR(100) DEFAULT '';

	   SELECT group_name 
	   INTO _result
	   FROM code_group
	   WHERE group_code = p_group_code;

	RETURN _result;
	
END

와 

CREATE FUNCTION test.fn_get_sortmatch(`p_group_code` VARCHAR(50))
RETURNS INT
BEGIN

	DECLARE _result int DEFAULT 0;

	   SELECT max(b.sort_seq)
	   INTO _result
	   FROM code_group a,
	        code_detail b
	   WHERE a.group_code = b.group_code
	   and   a.group_code = p_group_code;

	RETURN _result;

END

간단한 함수 2개를 만들었다. 하나는 그룹코드를 던지면 그룹코드명을 리턴하는 것이고, 

또 하나는 그룹코드를 던지면 해당 그룹코드에 대한 상세코드의 max(sort_seq)값을 가져오는 부분이다.

왜 이렇게 만들었냐는 의미가 없다. 테스트로 해보는 것이니깐..

 

 

2.  함수 Contributor 클래스 생성

 

이젠 JPA에서 제공해주는 Contributor 를 사용한 함수 등록 클래스 파일을 만들어 보겠다.

참고로 이전 버전에서는 Dialect를 이용하는 소스도 있지만 registerFunction()을 불러올 수 없다.

(아래는 참고 소스이다.)

public class MySQLDialectConfig extends MySQL8Dialect {

    public MySQLDialectConfig() {
        super();
        this.registerFunction("function_test", new StandardSQLFunction("function_test", new StringType()));
    }
}

그리고 MetaDataBuilder를 이용하는 소스도 있지만 이것 역시 deprecated되었다.

 

그래서 새로운 버전에서는 Contributor 를 사용하는 클래스를 아래와 같이 만든다.  

만들때에 import 하는 것도 어려워서 전체 소스를 넣었다.

 

그리고 이전 버전보다 좋아진 것은 이전 버전에서는 DB의 종류에 따라서 상속받아야하는 인터페이스가 달랐지만

이젠 그런것을 생각할 필요도 없다는게 좋아진 점인 것 같다.

서론이 좀 길었다. 아래가 실제 생성할 클래스 파일이다.

package com.example.vueJpaProject.util;

import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.FunctionContributor;
import org.hibernate.type.BasicType;
import org.hibernate.type.StandardBasicTypes;

public class CustomFunctionContributor implements FunctionContributor {

    @Override
    public void contributeFunctions(FunctionContributions functionContributions) {

        BasicType<String> StringType = functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.STRING);
        BasicType<Integer> IntegerType = functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.INTEGER);
        functionContributions.getFunctionRegistry().registerPattern("fn_get_groupname","?1 @@ fn_get_groupname(?2)", StringType);
        functionContributions.getFunctionRegistry().registerPattern("fn_get_sortmatch","?1 @@ fn_get_sortmatch(?2)", IntegerType);
    }
}

registerPattern 메소드의 파라미터는 (String name, String pattern, BasicType returnType) 순이다. 

말 그대로 본다면 첫번째는 함수의 대표이름이고, 두번째는 호출시의 패턴인 것이고, 세번째는 리턴되는 타입을 말한다.

위의 리턴 타입을 두가지로 만든 것도 그 이유에서다. 첫번째는 이름인 Strng이 두번째는 순서인 Integer 값이기 때문이다.

 

아직 복잡한 패턴을 해보질 못해서 간단한 것으로만 진행하겠다. 점점 찾아보면 늘어날 것으로 생각된다.

 

그리고 기존 버전에서는 application.yml 에 등록을 해야했지만 이번 버전부터는 등록을 하지 않아도 자동? 등록이 된다.

 

 

3. 공통코드 레파지토리  재수정

   위의 테스트를 위해서 두줄 추가한 부분을 주석으로 막고 이번에 테스트하기 위한 함수 호출을 위해서 다른 두줄을

   아래와 같이 추가한다.

...중략
JPAQuery<CodeGroupEntity> query = queryFactory.selectFrom(a)
        .leftJoin(a.codeDetailEntity, b)
        .on(codeDetailDto.makeSearchCondition(a, "use_yn"))
        .where(
                codeDetailDto.makeSearchCondition(a, "group_code"),
                codeDetailDto.makeSearchCondition(a, "group_name"),
                codeDetailDto.makeSearchCondition(b, "code_value"),
                codeDetailDto.makeSearchCondition(b, "code_name"),
                codeDetailDto.makeSearchCondition(a, "use_yn"),
                codeDetailDto.makeSearchCondition(a, "reg_date"),
                // 두줄추가
                codeDetailDto.makeSearchCondition(a, "name_match"),  <-- 추가
                codeDetailDto.makeSearchCondition(b, "sort_match")   <-- 추가
                // 두줄추가 
                //JPAExpressions.select(Expressions.constant(1)).from(a).where(a.groupCode.eq(b.groupCode)).exists(),
                //a.groupCode.eq(JPAExpressions.select(b.groupCode.max()).from(b).where(a.groupCode.eq(b.groupCode)))
            );
...중략

주의할 점은 첫번째 name_match는 a 엔티티를 사용하고 두번째 sort_match는 b 엔티티를 사용하는 것이다.

 

 

4. 상세코드 Dto 수정

   가변쿼리를 위해서 QCodeDetailEntity 에 sort_match를 추가하고 QCodeGroupEntity 에 name_match를 추가했다.

... 중략
        if(qEntity instanceof QCodeDetailEntity) {
            
            ... 중략
            if("sort_match".equals(gubun)) {
                if(StringUtils.hasLength(groupCode)) {
                    BooleanExpression exp = Expressions
                            .numberTemplate(Integer.class, "fn_get_sortmatch({0})", groupCode)
                            .gt(((QCodeDetailEntity)qEntity).sortSeq);
                    
                    return exp;
                }
            }
        }
        
        if(qEntity instanceof QCodeGroupEntity) {
            
            ... 중략
            if("name_match".equals(gubun)) {
                if(StringUtils.hasLength(groupCode)) {
                    BooleanExpression exp = Expressions
                            .stringTemplate("fn_get_groupname({0})", groupCode)
                            .eq(((QCodeGroupEntity)qEntity).groupName);
                    
                    return exp;
                }
            }
        }
        return null;
    }
... 중략

차이가 나는 부분은 첫번째는 numberTemplate 메소드를 사용했고, 두번째는 stringTemplate 메소드를 사용하였다.

처음엔 이름만 바꾸면 되는 줄 알았는데 메소드 파라미터 부터 차이가 난다.

numberTemplate는 리턴되는 값의 정의가 맨앞에 class 형태로 필요하다. 

나머지 두개는 stringTemplate와 동일하게 호출되는 함수 및 인수번호, 그리고 인수 값으로 입력한다.

이때 인수 값은 인수 번호의 개수 만큼 가변적으로 늘어날 수 있다. java가 많이 발전을 했다. ^^;

그 뒤는 비교할 대상에 대한 조건으로 마무리 하였다. 이것 이외에도 Template 메소드가 여러개가 있다. 

 

이렇게 하고 강제로 groupCode 값을 프론트에서 전달해보자. 테스트를 위해서 어쩔 수 없다. 

 

5. 공통코드 리스트 프론트 수정

   그룹코드 기준 리스트 조회 파라미터에 강제로 groupCode를 부여했다. 

... 중략
fnGroupJoinList() {

      this.requestBody = { // 데이터 전송
        searchKey: this.search_key,
        searchValue: this.search_value,
        useYn : 'Y',
        groupCode: 'A01',
        page: this.page,
        size: this.size
      }

      this.$axios.get(this.$serverUrl + "/codegroups/group_join_list", {
        params: this.requestBody,
        headers: {}
      }).then((res) => {
... 중략
    }

수정이 완료되었다면 한번 실행시켜 보자. 데이터 추출보다는 쿼리에 어떻게 만들어지는지가 중요하다.

 

아래 and 조건을 살펴보면 함수가 잘 보여지고 비교도 잘 되었다. 그래고 해당 ? 에 대한 값도 잘 셋팅이 되었다.

 

화면도 아래와 같이 잘 조회가 된다.

 

이전보다 심도있는 개선 부분으로 여겨지지만 아직 가야할 길은 멀다.

항상 실전에서는 별의별 상황이 많이 발생을 해서 지금 이정도의 쿼리는 쿼리도 아닐 정도이니 말이다.

다음 장에서는 Querydsl 에 기본 방식을 다시 정리해보겠다. 

기억하는 건 한계가 있으니 많은 샘플이 있으면 향후에 도움이 될 것이니 말이다.

반응형

프로그램/Vue.js Related Articles

MORE

Comments