728x90

토이 프로젝트 관련

JPA 스터디를 하면서 배운 내용을 토대로 하여 Spring Data JPA를 더 유연하게 사용하기 위해 토이프로젝트를 진행했다.

이슈사항이 될 수도 있겠다 싶어서 삽질 아닌 삽질을 하다가 알게된 사실이다.

사실, 계속 궁금했다. 🤔

코드로 보면 아래와 같은 궁금증이었다.

@Test
void findById() {
    memberRepository.save(member);
    Optional<Member> result = memberRepository.findById(1L);

    result.ifPresent(m -> {
        assertThat(m.getName()).isEqualTo("홍길동");
    });
}
@Test
void findAll() {
    memberRepository.save(member);
    List<Member> list = memberRepository.findAll();
    assertThat(list.size()).isEqualTo(0);
}

이렇게 두 메서드 이다.

결과부터 보자면

findById

image

findAll

findAll

결과가 다르다. 아무 sql을 출력해주지 않는다.

이유는 findAll을 하면 해당 테이블에 있는 모든걸 가져와야 하니까 update를 해야한다.

이 트랜잭션 밖에서 무슨일이 벌어져서 해당 테이블에 무슨 변화가 있었는지 모른다.

그래서 실행 전에 update도 발생한다❗

현재 트랜잭션 안에서 벌어진 일도 반영하고 select를 해야 현재 가장 최신 상태의 데이터를 가져오는거니까 동기화를 하는 느낌? 이라고 생각하면 될것같다.

근데 findById는 한건에 대한 데이터고, 그 한 건이 현재 트랜잭션 안에서 변경중인 데이터이다. 그래서 해당 변경 사항은 이미 영속성 컨텍스트가 관리하는 중이었기 때문에 findAll처럼 updateselect가 발생할 필요없이, 영속성 컨텍스트 안에서 캐싱하고 있던 객체를 찾아주기 때문에 updateselect 모두 발생하지 않은것 처럼 보인다. 근데 해당 코드가 테스트가 아니라 일반 애플리케이션 코드였으면 트랜잭션 끝나고 update가 발생한다.

이게 왜 발생한다고 말을 할 수 있냐면 실제로 한 메서드 내의 트랜잭션에서 save를 빼고 다른 트랜잭션에서 save를 해주고 findById만 조회하면 또 잘나오게 된다.

JPA를 사용함에 있어서 트랜잭션 정말 중요하다는걸 또 한번 깨닫는다❗

데이터를 원하는 조건에 맞게 추출하게 로직을 구현하는건 중요하지만 그 전에 트랜잭션 관리가 최우선이다. 앞으로 더 참고해서 개발해보도록 하자 😁

728x90
728x90

네이티브 SQL

JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL함수를 지원한다.
근데 특정 DB의 방언과 같은 종속적 기능은 지원하지 않는다.

  • 특정 DB만 지원하는 함수, 문법, SQL 쿼리 힌트
  • 인라인 뷰(from절 서브쿼리), UNION, INTERSECT
  • 스토어드 프로시저

종속적인 기능을 지원하는 방법은

  • 특정 DB만 사용하는 함수
    • 특정 JPQL에서 네이티브 SQL 함수를 호출할 수 있다.
    • Hibernate는 DB 방언에 각 DB에 종속적 함수를 정의했다. 그리고 직접 호출할 함수를 정의하기도 가능하다.
  • 특정 DB만 지원하는 힌트
    • Hibernate를 포함한 몇몇 JPA구현체가 지원한다.
  • 인라인 뷰(from절 서브쿼리), UNION, INTERSECT
    • JPA구현체들이 지원한다. Hibernate는 지원 ❌
  • 스토어드 프로시저
    • JPQL에서 호출 가능
  • 특정 DB만 지원하는 문법
    • 너무 유니크한 쿼리 문법은 지원하지 않는데, 이때 네이티브 SQL사용. 근데 이걸 사용할까...?

네이티브 SQL을 사용하면 엔티티를 조회할 수 있고, JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.

네이티브 SQL 사용

네이티브 쿼리 API는 3가지가 있다.

  • 결과 타입을 정의
  • 결과 타입을 정의할수 없을 때
  • 결과 매핑 사용

엔티티 조회

    @Test
    @DisplayName("네이티브 SQL 엔티티 조회")
    void nativeQueryTest() {
        String sql = "select id, name, price from item where price > ?";
        Query nativeQuery = em.createNativeQuery(sql, Item.class).setParameter(1, 100);

        List<Item> items = nativeQuery.getResultList();
    }

jdbc 사용할때와 똑같은 느낌이 든다.
근데 가장 중요한점은
SQL만 직접 사용할 뿐, JPQL을 사용할 때와 같다. 조회한 엔티티도 영속성 컨텍스트에서 관리 된다.

값 조회

값으로 조회하려면 엔티티 조회처럼 class를 같이 넣어주는게 아니라
em.createNativeQuery(sql) 를 사용하면 된다.
대신 이때 nativeQuery.getResultList() 는 Object 배열을 반환하므로
List<Object[]> 로 반환을 받아야한다.
더욱 더 JDBC같이 생겼다.

결과 매핑 사용

결과 매핑을 사용하면 엔티티 자체에 너무 많은 어노테이션 설정을 해야되므로 보편적으로 사용하지 않을 것 같다는 나의 생각이 들어있다.
그래도 정리를 해보도록 하겠다.

String sql = "select M.ID, AGE, NAME, TEAM_ID, I.ORDER_COUNT FROM MEMBER M " +
"LEFT JOIN (SELECT IM.ID, COUNT(*) AS ORDER_COUNT FROM ORDERS O, MEMBER IM " +
"WHERE O.MEMBER_ID = IM.ID) I ON M.ID = I.ID";

Query nativeQuery = em.createNativeQuery(sql, "memberWithOrderCount");
List<Object[]> members = nativeQuery.getResultList();

아래는 매핑 정의 코드이다.

@Entity
@SqlResultSetMapping(name = "memberWithOrderCount",
    entities = {@EntityResult(entityClass = Member.class) },
    columns = {@ColumnResult(name = "ORDER_COUNT")}
}
public class Member {...}

id, age, name, team_idMember 엔티티로 매핑을 시키고 order_count 는 단순 칼럼으로 매핑했다.
이렇게 여러 컬럼들을 매핑해서 추출할 수 있다.

@NamedNativeQuery

Named 네이티브 SQL을 사용하여 정적 SQL도 작성이 가능하다.
엔티티 클래스에

@NamedNativeQuery(
    name = "Member.memberSQL",
    query = "select 조회 쿼리문",
    resultClass = Member.class
)

로 등록해주고 사용하고자 하는 곳에서

TypedQuery<Member> nativeQuery = em.createNamedQuery("@NamedNativeQuery의 name", Member.class)
//파라미터가 있을 때
.setParameter();

네이티브 SQL은 휴먼에러를 발생할 확률이 QueryDSL보다 굉장히 높을 것으로 예상한다.
그래서 웬만하면 QueryDSL로 하지만 한방쿼리가 적절하게 필요할때만 사용하도록 해야할듯? 싶다. 😅
아직 실무에서 제대로 사용하지 않아서 이런 실무에서의 타협점은 점차 늘려가야 될것으로 보인다.

728x90

'JPA' 카테고리의 다른 글

JPA metamodel must not be empty!  (0) 2022.08.06
[JPA] findAll, findById 차이  (0) 2022.08.06
[JPA] 객체 지향 쿼리 언어 - Querydsl 2  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - Querydsl  (0) 2022.08.05
728x90

이번 포스팅에서는 조인에 대해 알아볼 것이다.

조인

조인은 innerJoin(join), leftJoin, rightJoin, fullJoin을 사용할 수 있고 추가로 JPQL의 on과 성능 최적화를 위한 fetchJoin을 사용할 수 있다.

jpaQueryFactory.selectFrom(item).join(조인할 쿼리클래스).fetch();

연관관계가 있으면 그냥 join만 사용해도 되지만 지금은 on이 추가되어 on() 절로 연관관계 없이 조인도 가능하다.

jpaQueryFactory.selectFrom(item).join(chair).on(chair.name.eq(item.name)).fetch();


from절에 여러 조건을 사용해서 세타조인도 가능하다.

서브 쿼리

서브 쿼리는 예전 버전에서는 JPASubQuery를 사용했지만 업데이트가 되어 현재 버전에서는 JPAExpressions 를 사용하여 서브쿼리를 작성한다.

@Test
@DisplayName("subQuery 테스트")
void subQueryTest() {
    jpaQueryFactory.selectFrom(item)
                   .where(item.name.eq(String.valueOf(JPAExpressions.selectFrom(chair).where(chair.name.eq("item3")))))
                   .fetch();
}

전 직장에서 근무하였을때 서브쿼리로 너무 많은것들을 처리했던 레거시 쿼리가 상당히 많아서
서브쿼리를 사용할때는 항상 생각해보고 쿼리를 짜야 좋을것 같다.
이게 다른 조회를 여러번 하는것이나 Join을 사용할때 보다 느린것 같다. 자세하게 속도를 비교해보면서 포스팅을 한번 했었어야 했는데 이게 안되서 개인적으로는 아쉬운 부분이다.😅

프로젝션

프로젝션 대상이 하나라면 해당하는 타입으로 결과를 받아야 한다.

List<String> list = jpaQueryFactory.select(item.name).from(item).orderBy(item.name.desc()).fetch();

그렇지만 대상이 여러개라면?
반환값이 Tuple인 객체를 반환하기 때문에 이렇게 사용해야 한다.

List<Tuple> list = jpaQueryFactory.select(item.name, item.price).from(item).orderBy(item.name.desc()).fetch();

가만보니까 이렇게 특정 칼럼만 받아서 쓰는 서비스 로직이 따로 있을거란 생각이 든다.
그때 그냥 DTO를 써주면 어떨까?

빈 생성

  • 프로퍼티 접근(Setter)
  • 필드 접근
  • 생성자 사용
    객체를 생성하는 방법 3가지 이다. 이것을 통해 그리고 Projections를 사용하여 dto객체를 생성해주면 될듯 하다.

주의❗️ 빈 생성자를 무조건적으로 넣어줘야 한다.

기본값이 빈 생성자라서 명시적으로 넣어주지 않았는데
다음과 같은 에러가 발생한다. 😱


protected 생성자 넣었을때 또 protected 에러를 발생시킨다.

그래서 lombok의 @NoArgsConstructor를 넣어주었더니 정상 실행이 된다.
public 접근제어자여야 한다.

@Test
@DisplayName("projection dto")
void dtoTest() {
        List<ItemDto> result = jpaQueryFactory.select(
        Projections.bean(ItemDto.class,item.name.as("name"), item.price))
        .from(item).fetch();
}

프로퍼티 접근 방식인데 여기선 Projections.bean을 사용한다. 이것이 setter 메소드를 사용해서 값을 채우는 방식이다.

필드 직접 접근 방식은 Projections.fields()를 사용하면 된다.

생성자 접근 방식은 Projections.constructor()를 사용하는데 지정한 프로젝션과 파라미터 순서가 같은 생성자여야 잘 동작한다.

수정, 삭제 배치 쿼리

이부분은 그냥 영속성 컨텍스트에서 삭제, 수정 해주는것이 더 편리할것 같아서 따로 정리하지는 않고 읽는 단계로 넘어가겠다.

동적 쿼리

여기가 내가 많이 생각했고 어떻게 해야 조건문으로 쿼리를 처리할지에 대한 고민을 했던게 이 부분인것 같다.

@Test
@DisplayName("동적 쿼리")
void 동적쿼리_Test() {
    SearchParam param = new SearchParam();
    param.setName(null);
    param.setPrice(200);

    BooleanBuilder booleanBuilder = new BooleanBuilder();
    if (!ObjectUtils.isEmpty(param.getName())) {
        System.out.println(param.getName());
        booleanBuilder.and(item.name.eq(param.getName()));
    }

    if (!ObjectUtils.isEmpty(param.getPrice())) {
        System.out.println(param.getPrice());
        booleanBuilder.and(item.price.eq(param.getPrice()));
    }

    List<Item> result = jpaQueryFactory.selectFrom(item).where(booleanBuilder).fetch();
}

BooleanBuilder를 사용하여 조건에따른 조건을 할당해서 조회해줄수가 있다.
이것을 할줄 몰라서 Mybatis를 고집했던 이유도 없지않아 있는것 같다.

JPQL이 기본인 CRUD보다 훨씬 중요하고 이것을 알아야 원하는대로 select 쿼리를 날려줄 수가 있다.
그러면서 동시에 이 QueryDSL을 쿼리로 만드는게 복잡하고 어렵다고 생각해서 mybatis에서 또는 그냥 @Query에서 못벗어난? 것 같다. 😭

여기에 리팩토링을 추가한다면 조건문 하나당 로직을 메서드로 분리하여 수행해주면 편할것이다.

728x90

'JPA' 카테고리의 다른 글

[JPA] findAll, findById 차이  (0) 2022.08.06
[JPA] 객체 지향 쿼리 언어 - Native SQL  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - Querydsl  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - 3  (0) 2022.08.05
728x90

QueryDSL

Criteria의 단점 너무 복잡하고 어렵다는 것 그래서 JPQL이 어떻게 생성되는지 파악이 어렵다.
그래서 나온게 이 QueryDSL이다.. 코드로 작성하는데 간결하고 알아보기 쉽다.

QueryDSL은 오픈소스 프로젝트이다. 단순 CRUD보다는 이름에 걸맞게 데이터를 조회 그러니까 통계형 쿼리를 짤때 적합하지 않을까 생각한다.

QueryDSL Setting

build.gradle

buildscript {
    ext {
        ...
        querydslVersion = '1.0.10'
    }

    dependencies {
        ....
        classpath "gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:$querydslVersion"
    }
}

subprojects {
    apply plugin: 'com.ewerk.gradle.plugins.querydsl'

    ext {
        querydslDir = "$buildDir/generated/querydsl"
    }

    dependencies {
        ...
        implementation 'com.querydsl:querydsl-jpa'
    }

    querydsl {
        jpa = true
        querydslSourcesDir = querydslDir
    }

    sourceSets {
        main.java.srcDir querydslDir
    }

    configurations {
        querydsl.extendsFrom compileClasspath
    }

    compileQuerydsl {
        options.annotationProcessorPath = configurations.querydsl
    }
}

이렇게 설정을 해주었는데 중요한 부분은 subproject.ext.querydslDir 부분이다.
$buildDir이 뜻하는 것은 우리의 스터디는 일단 모듈을 나누어서 한사람당 모듈을 사용하고 있다.
그래서 의존성을 구분해놓았는데 여기서의 $buildDir은 모듈의 빌드된 폴더
build/를 의미하며 build/generated/querydsl 폴더에 Entity클래스 앞에 Q가 붙은 클래스가 빌드되어 있다.
이것으로 QueryDSL을 세팅해주는 것이다.

QuerydslConfig.java

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

전에 말했듯 JavaEE환경에서는 @PersistenceContext를 활성화하면 알아서 주입받는다.
JPAQueryFactory 가 QueryDSL을 사용하기 위해서 구현해야 하는 것이다.

이렇게 설정해주면 QueryDSL을 사용할 수가 있다.

이제 테스트 코드를 작성해 보자.

@DataJpaTest
public class QuerydslTest {
    @PersistenceUnit
    EntityManagerFactory emf;
    EntityManager em;
    EntityTransaction tx;
    JPAQueryFactory jpaQueryFactory;

    private Member member;

    @BeforeEach
    void setUp() {
        em = emf.createEntityManager();
        tx = em.getTransaction();
        jpaQueryFactory = new JPAQueryFactory(em);
        tx.begin();
        Address address = Address.builder()
                .city("city")
                .street("street")
                .zipcode("zipcode")
                .build();

        Address comAddress = Address.builder()
                .city("cocity")
                .street("costreet")
                .zipcode("cozipcode")
                .build();

        Period period = Period.of("20210714", "20210714");

        member = Member.builder()
                .name("kim")
                .age(26)
                .period(period)
                .homeAddress(address)
                .companyAddress(comAddress)
                .build();

        em.persist(member);
        em.flush();
    }

    @Test
    @DisplayName("QueryDSL 시작")
    void querydslTest() {
        QMember qMember = new QMember("m"); //생성된 별칭
        List<Member> members = jpaQueryFactory.selectFrom(qMember)
                .where(qMember.name.eq("kim")).orderBy(qMember.id.desc()).fetch();

        assertThat(members.get(0).getName()).isEqualTo("kim");
        assertThat(members.get(0).getAge()).isEqualTo(26);
    }
}

책에서는 orderBy(qMember.id.desc()).list(qMember); 로 사용했는데
버전이 바뀌면서 list 메소드는 없어졌다고 한다.
그래서 공식문서를 찾아보니까

QueryDSL로 select 조회 쿼리를 만들었을때 로그이다.

이상한점 이라고 느낄수 있다면 어? 하고 코드상에는 jpaQueryFactory.from으로 시작하기 때문에 이것이 어떤 CRUD인지 모를 수 있다.fetch()를 해주면 반환이 List인데 그래도 타입은 캐스팅을 해주어야 한다.
자동으로 뭔가 Q클래스를 맞춰줄줄 알았다.

🤣

ㅋㅋㅋㅋㅋㅋㅋ 아니었다..... 그냥 기본값이 select가 아니라 list(qMember) 를 해주던 버전에서는 이 list 메소드가 엔티티 타입을 맞춰서 List에 넣어주었는데 지금은 selectFrom(qMember)를 해주면 이것이 List 타입을 맞춰준다.
이렇게 또 하나 깨달음을 얻는다 👍

페이징 정렬

@Test
void 페이징_테스트() {
    QItem item = QItem.item;
    List<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                                           .orderBy(item.price.desc(), item.name.desc())
                                           .offset(1).limit(3).fetch();

    result.forEach(r -> System.out.println(r.toString()));
}

여기서 where 조건에 lt는 부등호로 < 이고, gt는 >이다. orderBy절에서는 쿼리 타입인(Q)에서 asc(), desc()를 지원해준다. 페이징은 offsetlimit 을 조합해서 사용하면 된다.
이렇게해서 얻은 결과이다.

전체 데이터 수를 알고 싶을때는

listResults()를 사용한다.

이것 역시 바뀌었다.
SearchResults<T> 가 아니라 버전이 바뀌면서 QueryResults<T>로 변경되었다.

listResults() 가 아니라 fetchResults()로 바뀌게 되었다.

@Test
@DisplayName("조회 결과 테스트")
void listResultsTest() {
    QueryResults<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                .orderBy(item.price.desc(), item.name.desc())
                .offset(1).limit(3).fetchResults();

    long total = result.getTotal(); //500보다 작은수 총 count
    long limit = result.getLimit(); //limit 3
    long offset = result.getOffset(); // offset 1
    List<Item> results = result.getResults();

    assertThat(total).isEqualTo(4L);
    assertThat(limit).isEqualTo(3L);
    assertThat(offset).isEqualTo(1L);

    results.forEach(r -> System.out.println(r.toString()));
}

getTotal은 오류의 여지가 있어보인다. 왜냐면 저 QueryDSL 전체를 시켰을때의 count가 아니라 count가 먼저 실행되기 때문에 where() 조건까지 수행한 count가 나온다.

결과의 조회는 QueryResultsgetResults()를 사용하여 페이징 정렬할때처럼의 결과를 얻을수가 있다.

그룹

그룹은 groupBy를 사용하고 그 다음에 조건을 해주려면 having을 사용하면 된다.

List<Item> results = jpaQueryFactory.selectFrom(item)
                                    .groupBy(item.price)
                                    .having(item.price.lt(500))
                                    .fetch();

728x90
728x90

깃허브 바로가기
책의 내용이 너무많아 계속 분리해서 작성하게 된다.
이번 포스팅에서는 서브 쿼리부터 내용을 다뤄보도록 하겠다.

서브 쿼리

JPQL도 SQL처럼 서브 쿼리를 지원하는데, 여기서는 몇 가지 제약사항이 있다.
서브 쿼리를 WHERE, Having 절에서만 사용할 수 있고 select, from 절에서는 사용할 수 없다.

서브쿼리 함수

  • [NOT] EXISTS 서브쿼리
    • 서브쿼리에 결과가 존재하면 참. NOT은 반대
  • {ALL | ANY | SOME} 서브쿼리
    • 비교 연산자와 같이 사용한다.
      • ALL: 조건을 모두 만족하면 참
      • ANY or SOME : 둘은 같은 의미이다. 조건을 하나라도 만족하면 참
  • [NOT] IN 서브쿼리
    • 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참. 참고로 IN은 서브쿼리가 아닌 곳에서도 사용한다.

조건식

종류 설명 예제
문자 작은 따옴표 사이에 표현, 작은 따옴표를 표현하고 싶다면 작은따옴표 2개('') 사용 'Hello', 'He''s'
숫자 L(Long), D(Double), F(Float) 10L, 10D, 10F
날짜 Date {d 'yyyy-mm-dd'}
Time {t 'hh-mm-ss'}
DateTime {ts 'yyyy-mm-dd hh:mm:ss.f'}
{d '2021-07-25'}
{t '15:00:12'}
{ts '2021-07-25 15:48:22.123}
m.createDate = {d '2021-07-25'}
Boolean TRUE, FALSE -
Enum 패키지명을 포함한 전체 이름을 사용해야 한다. pack.MemberType.Customer
엔티티 타입 엔티티의 타입을 표현한다. 주로 상속과 관련해서 사용한다. TYPE(m) = Member

연산자 우선 순위

  1. 경로 탐색 연산(.)
  2. 수학 연산 : 단항 연산자: +, -, 사칙연산 : *, /, +, -
  3. 비교 연산 : =, >=, >, 등, <>(다름), Between, Like, In, Is Null, Is Empty, Exists
  4. 논리 연산 : NOT, AND, OR

Between

where m.age between 10 and 20
where 뒤에 식으로 작성할 수 있다. Member의 나이가 10~20인 사람 찾기

In

In에는 서브쿼리를 사용할 수 있다. in절의 조건이 하나라도 있으면 참이다.

Like

문자표현식과 패턴을 비교한다.

  • % : 아무 값들이 입력되어도 된다. (값이 없어도 됨)
  • _ : 한 글자는 아무 값이 입력되어도 되지만 값이 있어야 한다.

NULL

Null 인지 비교한다. Null은 =으로 비교하면 안되고 is null을 사용해야 한다.

컬렉션 식

컬렉션에만 사용하는 특별한 기능이다. 컬렉션은 컬렉션 식 이외에 다른 식은 사용할 수가 없다.

Is Empty

컬렉션에 값이 비었으면 참이다.

스칼라 식

위의 수학 연산에 더해 아래와 같은 문자함수도 있다.

함수 설명 예제
CONCAT(1, 2) 문자를 합한다. CONCAT('A','B') = AB
SubString(문자, 위치, [길이]) 길이 값이 없으면 나머지 전체 길이를 뜻한다. SUBSTRING('ABCDEF', 2, 3) = BCD
TRIM([[LEADING] , TRAILING , BOTH] [트림문자] FROM] 문자) LEADING: 왼쪽만, TRAILING: 오른쪽만, BOTH: 양쪽 다 트림 문자를 제거한다. 기본값은 Both, 트림문자 기본값은 공백이다. TRIM(' ABC ') = 'ABC'
LOWER(문자) 소문자로 변경
UPPER(문자) 대문자로 변경
LENGTH(문자) 문자 길이
LOCATE(찾을 문자, 원본 문자, [검색시작위치]) 검색 위치부터 문자를 검색한다. 1부터 시작, 못 찾으면 0을 반환 LOCATE('AB', 'ABCDE') = 1
함수 설명
ABS() 절대값
SQRT() 제곱근
MOD(수학식, 나눌수) 나머지
SIZE(컬렉션 값 연관 경로식) 컬렉션 크기 구함
INDEX(별칭) LIST 타입 컬렉션의 위치값 구함, 컬렉션이 @OrderColumn을 사용해야 할 수 있다.

날짜 함수는 아래와 같다.

  • Current_Date: 현재 날짜
  • Current_Time: 현재 시간
  • Current_TimeStamp: 현재 날짜 시간

이렇게 보면 DB에서 사용하는 함수가 거의 다 문법이 비슷하게 사용되는 것을 볼 수 있다.

CASE식은 생략하고 추후에 내가 사용할 때 다시 정리해야 겠다.

다형성 쿼리

JPQL로 부모 엔티티를 조회하면 자식 엔티티도 조회된다. Item의 자식으로 Book, Album, Movie 가 있다고 한다면 조회를 했을때 Item을 상속받는 Book, Album, Movie도 조회한다.

이걸 단일 테이블 전략을 사용하면 SQL이 select * from Item 이 되는데
조인 전략을 가져가면 left outer join이 세번 걸리게 된다.

TYPE

TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.

TREAT

자바의 타입 캐스팅과 빗슷함. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용.
JPA 표준은 FROM, Where 절에서 사용할 수 있지만, Hibernate는 Select 절에서도 Treat를 사용할 수 있다.

사용자 정의 함수 호출

JPA2.1 버전부터 사용자 정의 함수를 지원한다.
문법은 다음과 같다.
function_invocation::= FUNCTION(function_name {, function_arg}*)
예) select function('group_concat', i.name) from Item i

Hibernate 구현체를 사용하면 방언클래스를 상속해서 구현하고 사용할 DB함수를 미리 등록해야 한다.

spring.jpa.hibernate.dialect: org.hibernate.dialect.H2Dialect

방언에따라 dialect 뒤를 해당 SQL로 바꿔서 등록한다.
그렇게 이 구현체를 사용하면 해당하는 함수를 바로 사용할 수 있다.

기타 정리

  • enum은 =비교 연산만 지원한다.
  • 임베디드 타입은 비교를 지원하지 않는다.

Empty String

JPA표준은 ''을 길이가 0인 Empty String으로 정했지만 DB에 따라 ''를 Null로 사용하는 DB가 있으므로 확인하고 사용해야 한다.

Null 정의

  • 조건을 만족하는 데이터가 하나도 없으면 Null
  • Null은 알수 없는 값이다. Null과의 모든 수학적 계산 결과도 Null이 된다.
  • Null == Null 은 알수 없는 값이다.
  • Null is Null은 참이다.

엔티티 직접 사용

기본 키 값

객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다.
JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키값을 사용한다.

Named 쿼리 : 정적 쿼리

  • 동적 쿼리: em.createQuery("") 처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라고 한다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.
  • 정적 쿼리: 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 Named 쿼리라고 한다. Named쿼리는 한번 정의하면 변경할 수 없는 정적인 쿼리다.

Named쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 그래서 오류를 빨리 확인할 수 있고, 사용하는 시점에 결과를 재사용하므로 성능상의 이점도 있다.
Named 쿼리는 정적 SQL이 생성되기 때문에 DB의 성능 최적화에 도움이 된다.
Named 쿼리는 @NamedQuery 를 사용하여 자바 코드에 작성하거나 XML문서에 작성할 수 있다.

  • lockMode: 쿼리 실행 시 락을 건다.
  • hints: 여기서의 힌트는 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트이다. 2차 캐시를 다룰때 사용한다.

하나 이상의 NamedQuery를 사용하려면 @NamedQueries 어노테이션 사용할 것.

Named 쿼리를 XML에 정의하는 부분은 XML 대신에 Java에서 많이 하려고 노력하자.

728x90
728x90

JPQL

다시한번 JPQL의 특징을 정리해보자

  • JPQL은 객체지향 쿼리 언어이다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화해서 특정 DB SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.

기본 문법과 쿼리 API

JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다. em.persist()로 엔티티를 저장하므로 INSERT 쿼리는 없다.

JPQL 문법

select_문 :: =
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]

update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]

SELECT문

SELECT m FROM Member AS m where m.name = 'hello'
  • 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분한다.
    • Member, name 은 대소문자를 구분함.
    • SELECT, FROM, AS 등.. JPQL 키워드는 대소문자를 구분하지 않음.
  • 엔티티 이름
    • JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다.
    • @Entity(name="") 로 지정할 수 있지만, 기본값은 클래스명을 기본값으로 한다.
  • 별칭 필수
    • 여기서는 m 이라는 별칭을 주었는데 JPQL은 별칭을 필수로 줘야한다. 아니면 에러 발생.

TypeQuery, Query

작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery, Query가 있는데 반환할 타입을 명확하게 지정할 수 있다면 TypeQuery를, 그렇지 않으면 Query 객체를 사용하면 된다.

@Test
@DisplayName("TypeQuery 테스트")
void typeQueryTest() {
    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

    List<Member> resultList = query.getResultList();

    assertThat(resultList.get(0).getName()).isEqualTo(member.getName());
}

em.createQuery()부분의 두번째 매개변수에 반환 타입을 위와같이 지정하면 TypeQuery를 반환, 아니면 Query를 반환한다.

결과조회

  • query.getResultList() : 결과를 리스트로 반환, 결과가 없으면 빈 컬렉션을 반환한다.
  • query.getSingleResult() : 결과가 하나일때 사용
    • 결과가 없으면 NoResultException 예외 발생
    • 결과가 1개보다 많으면 NonUniqueResultException 발생

파라미터 바인딩

JDBC와의 차이점

JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩 지원

  • 이름 기준 파라미터
    • 이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다.
    • 앞에 : 를 사용한다.

예제

@Test
@DisplayName("이름기준 파라미터")
void nameOfParameterTest() {
    String name = "kim";

    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m where m.name = :name", Member.class);

    query.setParameter("name", name);
    List<Member> resultList = query.getResultList();
    assertThat(resultList.get(0).getName()).isEqualTo(member.getName());
}

이렇게도 할 수 있고
메소드 체이닝 방식으로도 할 수 있다.

List<Member> resultList = em.createQuery("SELECT m FROM Member m where m.name = :name", Member.class)
                            .setParameter("name", name);
                            .getResultList();
  • 위치 기준 파라미터
    • 위치 기준은 사용하려면 ? 다음에 위치 값을 주면 된다.
    • 위치 값은 1부터 시작이다.
em.createQuery("SELECT m FROM Member m where m.name= ?1", Member.class);

이런식으로 사용하면 되겠다. 나머지는 이름 기준과 같다.

위치 기준 보다는 이름 기준 파라미터 바인딩 방식이 더 명확하다.

프로젝션

SELECT절에 조회할 대상을 지정하는 것을 프로젝션이라고 하고 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다.

엔티티 프로젝션

SELECT m FROM Member m      //회원
SELECT m.team From Member m //팀

처음은 회원을 조회했고 두번째는 회원과 연관된 팀을 조회했는데 엔티티를 프로젝션 대상으로 한 예시이다.
이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다❗❗

임베디드 타입 프로젝션

JPQL에서 임베디드 타입은 엔티티와 비슷하게 사용 되는데 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.

주가 되는 엔티티에서부터 시작해서 나가야 쿼리가 수행된다.
임베디드 타입은 엔티티 타입이 아니라 값 타입이다.
따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

스칼라 타입 프로젝션

숫자, 문자, 날짜와 같은 기본 데이터 타입이 스칼라 타입.
반환되는 값의 Wrapper클래스 매칭하여 추출

중복 데이터를 제거하려면 DISTINCT를 사용한다.
통계형 쿼리는 지금 다루지 않지만, 통계용 쿼리도 스칼라 타입이다.

여러 값 조회

엔티티 대상이 조회하기 편리하지만, 특정 데이터만 선택해서 추출해야 하는 경우에는 Query클래스 객체를 사용해야한다.

페이징 API

페이징 처리용 SQL을 작성하는 일은 지루하고 반복적이다. 더 큰 문제는 DB마다 페이징을 처리하는 SQL 문법이 다르다.
JPA는 페이징을 두개의 API로 추상화했다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터의 수

DB마다 다른 페이징 처리를 같은 API로 처리할 수 있는 이유는 DB 방언 덕분이다. JPQL이 방언에 따라 설정에 맞는 DB SQL로 변환된다.

나는 PostgreSQL을 사용해서 결과는 아래와 같이 나왔다.

@Test
@DisplayName("페이징 API")
void pagingApiTest() {
    TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m order by m.name desc", Member.class);

    query.setFirstResult(10);
    query.setMaxResults(20);
    query.getResultList();
}

집합과 정렬

집합은 집합 함수와 함께 통계 정보를 구할때 사용함.

집합 함수

함수 설명
COUNT 결과 수를 구한다. 반환 타입: Long
MAX, MIN 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다.
AVG 평균 값을 구한다. 숫자타입만 사용이 가능하다. 반환 타입: Double
SUM 합을 구한다. 숫자타입만 사용할 수 있다. 반환 타입: 정수합: Long, 소수합: Double, BigInteger합: BigInteger, BigDecimal합: BigDecimal

고려사항

  • Null 값은 무시하므로 통계에 잡히지 않는다.
  • 값이 없는데 AVG, MIN, MAX, SUM 을 사용하면 Null 값이 된다. COUNT는 0
  • DISTINCT를 집합함수 안에 사용하여 중복된 값 제거 후 집합을 구할 수 있다.
  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입 지원 ❌

Group By, Having

Group By는 통계 데이터를 구할 때 특정 그룹을 묶어준다.
Having은 Group By와 같이 사용하는데 Group By가 끝나고 난 뒤에 필터링해준다.

Order By

order by는 정렬할 때 사용한다.

  • ASC : 기본값, 오름차순
  • DESC : 내림차순

JPQL 조인

JPQL도 조인을 지원한다. SQL 조인과 기능은 같은데 문법이 조금 다르다.

JPQL조인을 SQL 조인처럼 사용하면 문법 오류가 발생한다.
반드시 JOIN 명령어 다음에 조인할 객체의 연관 필드를 사용해야 한다.

SELECT m.username, t.name
FROM Member m JOIN m.team t
WHERE t.name = 'team1'

이렇게가 아니라 WHERE m.팀과관련된 것 이 나와야 한다.

내부조인

Inner Join을 사용한다. Inner는 생략이 가능하다.

외부조인

외부 조인은 기능상 SQL의 외부 조인과 같다. OUTER는 생략이 가능하기 때문에 LEFT JOIN 으로 사용한다.

컬렉션 조인

1:N, N:1 처럼 컬렉션을 사용하는 곳에 조인하는 것이 컬렉션 조인이다.

세타 조인

WHERE절을 이용해 세타 조인을 할 수 있다.
세타 조인은 내부 조인만 지원
select count(m) from Member m, Team t where m.name = t.name
관계없는 엔티티를 조인할 수 있다.

JOIN ON절

ON 절을 사용하면 조인 대상을 필터링 후 조인이 가능하다. Inner Join에서의 ON 절은 WHERE절을 사용할 때와 같아서 외부 조인에서만 사용한다.

페치 조인

JPQL에서 성능 최적화를 위해 제공하는 기능이다.
join fetch로 사용할 수 있다.

엔티티 페치 조인

select m from Member m join fetch m.team

join뒤에 fetch를 붙이면 연관된 엔티티나 컬렉션을 함께 조회한다.
m.team 다음에 별칭을 붙이는데 페치 조인은 별칭을 사용할 수 없다.

Member와 Team을 지연로딩으로 설정했다고 한다면 회원을 조회할 때 페치 조인을 사용해서 팀도 같이 조회를해서 팀 엔티티는 프록시가 아닌 실제 엔티티이다. 그래서 연관된 팀을 사용해도 지연로딩이 일어나지 않는다.
프록시가 아닌 실제 엔티티기 때문에 멤버 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태여도 연관된 팀을 조회할 수가 있다.

컬렉션 페치 조인

컬렉션으로 페치 조인한 JPQL은 조회하는 엔티티가 어떤 엔티티안에 연관되어있다면, 연관된 다른 엔티티도 같이 조회한다. 그래서 조인하면서 결과가 증가하여 조회를 두번하게 된다.

페치조인과 DISTINCT

SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다. JPQL에서의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것과 더불어 애플리케이션에서 한번 더 중복을 제거한다.

JPQL에서 select distinct의 의미는 엔티티의 중복을 제거하라는 뜻이므로 위에서 조회를 두번했던 결과를 하나만 조회하게 된다.

페치 조인과 일반 조인의 차이

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 그래서 연관된 엔티티는 조회하지 않는다.
컬렉션을 지연 로딩으로 설정한다면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다.
즉시 로딩 이라면 컬렉션을 즉시 로딩하기 위해 바로 쿼리를 한번 더 실행한다.

페치 조인의 특징과 한계

페치 조인을 사용하면 SQL한번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라고 한다.
페치 조인은 글로벌 로딩 전략보다 우선한다. 지연 로딩을 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인이 적용되어 같이 조회하게 된다.
최적화를 위해서 즉시 로딩을 글로벌 로딩 전략으로 가져간다면 항상 즉시 로딩이 일어나게 된다.
일부는 빠를 수 있지만 사용하지 않는 엔티티들도 자주 로딩하여 속도 저하가 우려된다.
그래서

글로벌 로딩 전략은 지연 로딩으로 채택하고 최적화가 필요할 때는 JPQL에서 페치 조인을 적용하는 것이 효과적이다.

페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다. 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다

페치조인의 한계점

  • 페치 조인 대상에는 별칭을 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
    • 컬렉션이 아닌 단일 값 연관 필드들은 페치 조인을 사용해도 페이징 API 사용 가능

페치 조인은 SQL한번으로 연관된 여러 엔티티를 조회할 수가 있어서 성능 최적화에 상당히 유용하지만 모든 것을 해결할 수는 없다. 그래서 여러 테이블을 조인해서 원하는 값들만 추출해야 할 경우에는 페치 조인보다는 여러 테이블에서 필요한 값들만 조회 한 후에 DTO로 반환 해주는 것이 더 효과적일 수가 있다.

경로 표현식

경로 표현식은 .(점)을 통해 객체 그래프를 탐색하는 것이다.

  • 상태 필드 : 단순히 값을 저장하기 위한 필드
  • 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함
    • 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
    • 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션

경로 표현식과 특징

경로 표현식을 사용하여 경로 탐색을 하려면 3가지 경로에 따라 어떤 특징이 있는지 보자

  • 상태 필드 경로 : 경로 탐색의 끝 더는 탐색 할 수 없음.
  • 단일 값 연관 경로 : 묵시적으로 내부 조인이 일어남, 단일 값 연관 경로는 계속 탐색할 수 있다.
  • 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어남, 더는 탐색할 수 없다.
    • 단, FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색이 가능함.

단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인 이라고 한다.
묵시적 조인은 모두 내부 조인이다.

  • 명시적 조인 : JOIN을 직접 적어주는 것
  • 묵시적 조인 : 경로 표현식에 의해서 묵시적으로 조인이 일어 나는 것, INNER JOIN만 할 수 있음.

경로 탐색을 사용한 묵시적 조인 시 주의 사항

경로 탐색을 사용하면 묵시적 조인이 발생해서 SQL에서 내부 조인이 일어날 수 있다.

  • 항상 내부조인이다.
  • 컬렉션은 경로탐색의 끝, 컬렉션에서 경로 탐색을 하려면 명시적으로 조인을 해서 별칭을 얻어야만 한다.
    • ex) select m.name from Team t join t.members m
  • 경로 탐색은 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향을 준다.

조인이 성능으로 차지하는 부분이 아주 크다. 묵시적 조인의 단점은 조인이 일어나는 상황을 한눈에 파악하기 어렵다는 단점이 있다. 성능이 중요하다면 분석하기 쉽도록 명시적 조인을 사용하는것이 좋다.

728x90

'JPA' 카테고리의 다른 글

[JPA] 객체 지향 쿼리 언어 - Querydsl  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - 3  (0) 2022.08.05
[JPA] 객체지향 쿼리 언어 - 1  (0) 2022.08.05
[JPA] 값 타입  (0) 2022.08.05

+ Recent posts