자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

객체지향 쿼리

EntityManager.find() 메서드로 식별자를 통한 엔티티 검색은 한계가 있다.

복잡한 조회조건으로 검색을 있어야 한다.

 

JPQL 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.

벤더별 조금씩 다른 SQL 추상화하여 SQL 의존적이지 않다.

 

SQL DB Table 대상 쿼리

JPQL 엔티티 객체 대상 쿼리

JPQL 받은 JPA 적절한 SQL 변환한다.

 

JPA 지원하는 검색 방법

  • JPQL (Java Persistence Query Language)
  • Criteria 쿼리
    JPQL 쉽게 다루도록 도와주는 빌더 클래스 모음
  • 네이티브 SQL
  • QueryDSL
    JPQL
    쉽게 다루도록 도와주는 빌더 클래스 모음
  • JDBC 직접 사용, Mybatis 같은 SQL 매퍼 프래임워크 사용

 

핵심은 JPQL 이다. 이를 이해해야 이를 이용한 빌더 클래스도 이해할 있다.

 

 

JPQL 소개

엔티티 객체를 조회하는 객체지향 쿼리

SQL 문법이 매우 비슷

SQL 한번 추상화해 벤더에 종속적이지 않다.

JPQL 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 간결하다.

 



@Entity
@Data
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "member_id")
        private Long id;
        @Column(name = "username")
        private String name;
}

 

 

 

 

Criteria 쿼리 소개

JPQL 생성하는 빌더 클래스

 

가장 장점은 문자열로된 JPQL 대신 프로그래밍 코드로 JPQL 작성할 있다.

 

프로그래밍 코드 장점

  • 컴파일 시점에 오류 발견
  • IDE 도움으로 코스 추천 받을 있음
  • 동적 쿼리 작성 편리

 

JPA 기본 내장되어 있다.

 

 

보는 바와 같이 Criteria 코드로 JPQL 생성할 있지만, 장점을 무시할 정도로 지나치게 복잡하다.

 

QueryDSL 소개

JPQL 빌더

단순하고 사용하기 쉽다.

다만, JPA 내장되어 있지는 않다. 따라서 의존성 설정이 필요하다.

 



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>jpa-basic</groupId>
    <artifactId>jpa-01</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <hiberate.version>6.0.0.Final</hiberate.version>
        <querydsl.version>5.0.0</querydsl.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hiberate.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-hikaricp</artifactId>
            <version>${hiberate.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>23.3.0.23.09</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.21.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
            <classifier>jakarta</classifier>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-apt -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>${querydsl.version}</version>
            <classifier>jakarta</classifier>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <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>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

 

 

 

 

바로 Maven generate-sources 눌러도 되나 Maven clean 돌리는 것을 권장

 

 

 

모르는 사람이 봐도 흐름이 이해가 만큼 쉽다.

 

네이티브 SQL 소개

 

 

직접 SQL 다루기 때문에 벤더 의존성이 생긴다. 바꿔 말하면, JPQL에서 표준화되어 지원하지 않고, 벤더에 의존하는 기능을 사용할 때는 유용할 있다.

 

JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용

JPA JDBC 커넥션을 획득하는 API 제공하지 않는다. JPA구현체가 제공하는 방법을 사용해야 한다.

위는 직접 JDBC 커넥션 얻는 , 아래는 하이버네이트가 JDBC 커넥션을 얻는

 

JDBC 마이바티스 같은 SQL 매퍼 프레임워크는 JPA 우회해서 DB 접근한다.

따라서 JPA 함께 사용하는 경우 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.

최악의 경우 영속성 컨텍스트와 DB 데이터 불일치로 무결성이 깨질 수가 있다.

 

이런 이슈를 해결하기 위해 JPA 우회하는 경우 SQL 실행 직전 영속성 컨텍스트를 수동으로 플러시해서 DB 영속성 컨텍스트를 동기화시켜야 한다.

 

스프링 프레임워크를 사용하면 JPA 마이바티스를 쉽게 통합할 있다.

AOP 활용하면 JPA 우회하는 모든 메서드에 강제로 플러시하는 코드를 넣을 수도 있다.

 

JPQL

Java Persistence Query Language

 

예제



@Entity
@Data
@ToString(exclude = {"orders"})
public class Member {
        @Id
        @GeneratedValue
        private Long id;
        
        @Column(name = "name")
        private String username;
        private int age;
        
        @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<Order> orders = new ArrayList<>();
        
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "team_id")
        private Team team;
        
        public void setTeam(Team team) {
                if (this.team != null) {
                        this.team.getMembers().remove(this);
                }
                this.team = team;
                team.getMembers().add(this);
        }
        
        public void addOrder(Order order) {
                orders.add(order);
                order.setMember(this);
        }
}
@Entity
@Data
public class Team {
        @Id
        @GeneratedValue
        private Long id;
        
        private String name;
        
        @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
        private List<Member> members = new ArrayList<>();
        
        public void addMember(Member member) {
                members.add(member);
                member.setTeam(this);
        }
}


@Entity
@Table(name = "Orders")
@Data
public class Order {
        @Id
        @GeneratedValue
        private Long id;
        
        private int orderAmount;
        
        @Embedded
        private Address address;
        
        @ManyToOne
        @JoinColumn(name = "member_id")
        private Member member;
        @ManyToOne(cascade = CascadeType.ALL)
        @JoinColumn(name = "product_id")
        private Product product;
        
        public void setMember(Member member) {
                if (this.member != null) {
                        this.member.getOrders().remove(this);
                }
                this.member = member;
                member.getOrders().add(this);
        }
}
@Entity
@Data
public class Product {
        @Id
        @GeneratedValue
        private Long id;
        
        private String name;
        private int price;
        private int stockAmount;
}
@Embeddable
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {
        private String city;
        private String street;
        private String zipcode;
}


public class Main {
        static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                save();
                emf.close();
        }




        private static void save() {
                logic(em -> {
                        Member m1 = new Member();
                        m1.setAge(20);
                        m1.setUsername("홍길동");
                        
                        Member m2 = new Member();
                        m2.setAge(30);
                        m2.setUsername("임꺽정");
                        
                        Member m3 = new Member();
                        m3.setAge(40);
                        m3.setUsername("유관순");
                        
                        Team t1 = new Team();
                        t1.setName("레드팀");
                        t1.addMember(m1);
                        t1.addMember(m2);
                        
                        em.persist(t1);
                        
                        Team t2 = new Team();
                        t2.setName("블루팀");
                        t2.addMember(m2);
                        t2.addMember(m3);
                        
                        em.persist(t2);
                        
                        Product p1 = new Product();
                        p1.setName("삼겹살");
                        p1.setPrice(10000);
                        p1.setStockAmount(10);
                        em.persist(p1);
                        
                        Product p2 = new Product();
                        p2.setName("오징어");
                        p2.setPrice(15000);
                        p2.setStockAmount(20);
                        em.persist(p2);
                        
                        Product p3 = new Product();
                        p3.setName("돈까스");
                        p3.setPrice(12000);
                        p3.setStockAmount(30);
                        em.persist(p3);
                        
                        Order o1 = new Order();
                        o1.setAddress(new Address("서울시", "을지로", "12345"));
                        o1.setMember(m1);
                        o1.setOrderAmount(1);
                        o1.setProduct(p1);
                        em.persist(o1);
                        
                        Order o2 = new Order();
                        o2.setAddress(new Address("서울시", "종로", "54321"));
                        o2.setMember(m1);
                        o2.setOrderAmount(1);
                        o2.setProduct(p2);
                        em.persist(o2);
                        
                        Order o3 = new Order();
                        o3.setAddress(new Address("서울시", "종로", "54321"));
                        o3.setMember(m2);
                        o3.setOrderAmount(1);
                        o3.setProduct(p2);
                        em.persist(o3);
                        
                        Order o4 = new Order();
                        o4.setAddress(new Address("서울시", "종로", "54321"));
                        o4.setMember(m3);
                        o4.setOrderAmount(1);
                        o4.setProduct(p2);
                        em.persist(o4);
                        
                        Order o5 = new Order();
                        o5.setAddress(new Address("서울시", "통일로", "55555"));
                        o5.setMember(m3);
                        o5.setOrderAmount(1);
                        o5.setProduct(p3);
                        em.persist(o5);
                });
        }


        static void logic(Consumer<EntityManager> logic) {
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        
                        logic.accept(em);
                        
                        tx.commit();
                } catch (Exception e) {
                        e.printStackTrace();
                        tx.rollback();
                } finally {
                        em.close();
                }
        }
}

Order 는 예약어로 Orders 변경했다.

 

기본 문법과 쿼리 API

SELECT, UPDATE, DELETE 문이 있다.

INSERT 문은 엔티티를 만들어 EntityManager.persist()메서드를 호출하므로 없다.

JPQL에서 UPDATE, DELETE 문은 벌크 연산이라고 한다.

 

SELECT

  • 대소문자 구분
    SQL 키워드를 제외하고, 엔티티와 필드는 구분한다.
  • 엔티티 이름
    클래스
    명이 아니라 엔티티 , 컬럼 명이 아니라 필드 명이다.
  • 별칭 필수

 

TypeQuery, Query

반환 타입이 명확하면, TypeQuery 아니면 Query

 

 

 

 

 

 

 

 

결과 조회 메서드

getSingleResult() 경우 명확히 건이라고 명시했기 때문에 결과가 없거나 이상이면 예외가 발생한다.

 

파라미터 바인딩

이름 기준 파라미터(Named parameters)

 

 

 

위치 기준 파라미터

 

 

이름 기준 파라미터를 사용할 것을 권장한다.

위치는 순서에 의존하게 되고, 이름은 순서에 의존하지 않으며, 똑같은 파라미터를 여러 사용해도 번만 setParameter 호출하면 된다.

 

바인드 변수는 필수로 사용해야 한다.

직접 문자열로 덧붙여가며 SQL 만들면 SQL 인젝션 공격에 취약해진다.

바인드 변수를 사용할 경우, DB 쿼리를 파싱해 이를 재사용한다.


프로젝션

select , 조회할 컬럼을 지정하는 것을 projection 이라 한다.

JPQL 에서 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(기본형) 있다.

 

엔티티를 대상으로 조회를 하면, 조회된 엔티티는 영속성 컨텍스트가 관리한다.

 

임베디드 타입을 프로젝션으로 사용할 있다. , 엔티티가 아니라 DB 기준에선 단순한 컬럼들 묶음이기에 from 절에는 없다.

엔티티가 아니기 때문에 영속성 컨텍스트에 관리 대상이 아니다.

 

 

 

스칼라 타입은 단순히 기본형 값을 늘어놓는

 

객체 변환 작업을 스칼라 타입으로 직접 받을 있다.

 

다만, 제약 조건으로 전체 경로를 포함한 클래스 이름이 필요하다.

 

 

페이징 API

페이징도 반복적이고 지루한 작업이다. 이를 간단한 설정으로 처리할 있다.

 

 

페이징 처리 방식은 방언에 따라 벤더별로 알아서 처리된다.

 

 

집합과 정렬

GROUP BY, HAVING

 

 

 

통계 쿼리는 실시간 처리하기에 부담이 많아 통계 결과만 저장하는 테이블을 별도로 만들어 주고 새벽에 통계 쿼리를 생성하는 것을 권장

 

정렬(ORDER BY)

JPQL 조인

내부 조인

inner 키워드 생략가능

연관된 Member.Team 엔티티를 조인 조건으로 사용한다.

따로 ON 필요가 없다.

 

 

외부 조인

outer 키워드는 생략가능

 

 

컬렉션 조인

일대다, 다대일 관계의 조인을 말한다. , 컬렉션이 사용된다

 

 

 

 

세타 조인

where 절을 이용한 조인으로 내부 조인만 지원한다.

 

 

 

 

JOIN ON

JPQL 에서 ON 절은 JPA2.1 부터 지원한다.

내부 조인에서 ON 절은 Where 절과 같으므로, 보통 외부 조인 시에만 사용한다.

 

 

 

페치 조인

sql 조인 종류는 아님

JPQL 에서 성능 최적화를 위해 제공하는 기능

 

연관된 엔티티를 지연 조회를 하는 경우가 많은데

이를 무시하고 번에 조회할 사용

 

 

페치가 없는 경우

 

 

toString() 위해 Member.team 관련된 레드팀, 블루팀으로 번의 추가 쿼리가 실행됐다.

 

페치 조인으로 조회된 연관된 엔티티는 프록시가 아닌 실제 엔티티다.

따라서 영속성 컨텍스트에서 분리되도 연관된 엔티티를 조회할 있다.

 

컬렉션 페치 조인

 

 

 

페치 조인으로 인해 지연 로딩이 무시되고 즉시 조회가 됐다.

더불어 책에서는 id=2 결과가 중복으로 나와 아래와 같이 중복이 발생한다.

이렇게 중복이 나는 조건은 일대다 fetch 조인.

다만 최신 버전에서는 중복이 자동으로 잡히는 듯하다. 따라서 다음 DISTINCT 필요하지 않을 듯하다.

 

 

페치 조인과 DISTINCT

JPQL 에서 DISTINCT 명령어는 SQL 에서 DISTINCT 수행하는 것과 더불어

어플리케이션에서도 중복 제거를 수행한다.

 

 

 

 

페치 조인과 일반 조인 차이

JPQL 결과를 반환할 연관관계까지 고려하지 않는다. SELECT 절에 지정한 엔티티만 조회한다.

따라서 연관관계 엔티티는 프록시 객체다. 실제 사용 조회가 된다.

 

fetch 키워드가 붙으면, 어떤 설정도 무시하고 무조건 즉시 로딩으로 조회된다.

따라서 연관관계 엔티티도 실제 엔티티다.

 

페치 조인의 특징과 한계

페치 조인은 번에 연관된 엔티티를 조회해서 SQL 질의 수를 줄여 성능 최적화를 있다.

또한 글로벌 패치 전략을 무시하고 즉시 조회를 있다.

따라서 이를 조합해 글로벌 패치 전략은 LAZY 도배를 하고, 필요할 Fetch 조인을 사용하는 것이 좋다.

즉시 조회기 때문에 영속성 컨텍스트가 닫이거나 준영속 상태가 되도 객체 그래프 탐색을 있다.

 

패치 조인 한계

  • 페치 조인 대상은 별칭을 없다.
    , 별칭을 통해 where select 등에서 컬럼을 사용할 없다.
    다만, 하이버네이트는 별칭을 지원한다.
    별칭을 잘못 사용하면, 연관된 데이터 무결성이 깨질 있어 조심해야 한다.
  • 이상 컬렉션을 페치할 없다.
    컬렉션 * 컬렉션 카테시안 곱이 발생하기 때문에 예외가 발생한다.
    구현체에 따라 되기도 하지만 사용하지 않는 것이 맞다.
  • 컬렉션 페치 조인을 하면 페이징 API 사용할 없다.
    컬렉션이 아닌 일대일, 다대일들은 페치 조인을 해도 페이징 API 사용할 있다.
    하이버네이트 기준 컬렉션 페치 조인 페이징 API 사용하면
    경로
    로그와 함께 주메모리에서 페이징 처리를 한다.
    잘못하면 메모리 초과로 심각한 에러가 발생할 있어 위험하다

 

페치 조인을 남용하지 말고, 여러 테이블에서 연관된 데이터를 취합해야 한다면, 별로 DTO 만들어 변환하는 것도 고려한다.

 

경로 표현식

JPQL 에서 "."()으로 접근하는 것을 의미한다.

코드에서 탐색이 아니다.

묵시적 조인이 일어난다.

경로 표현식의 용어 정리

  • 상태 필드
  • 연관 필드

 

 

상태 필드 경로 탐색

 

 

단일 연관 경로 탐색

 

 

 

경로 표현식에 의해 묵시적으로 일어나는 조인은 무조건 내부 조인이다.

 

 

 

컬렉션 연관 경로 탐색

 

 

 

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

  • 항상 내부 조인
  • 컬렉션은 경로 탐색의 , 탐색은 명시적 조인으로 별칭을 부여해 가능
  • 경로 탐색 위치는 어디는 생성되는 SQL from 조인

 

묵시적 조인은 일어나는 상황을 한눈에 파악하기 어렵다. 따라서 가급적 명시적 조인을 사용한다.

서브 쿼리

SQL 다르게 WHERE, HAVING 절에만 사용가능하다.

 

 

서브 퀴리 함수

  • [NOT] EXISTS (서브쿼리)
    서브 쿼리 결과가 존재하면
  • {ALL | ANY | SOME} (서브쿼리)
    ALL
    모두 만족
    ANY, SOME 하나라도 만족
    비교
    연산자와 함께 사용한다.
  • [NOT] IN (서브쿼리)
    서브쿼리 결과 하나라도 같은 것이 존재하면

 

 

 

 

 

 

jpql some sql에서 any

 

 

 

조건식

타입 표현

대소문자 구분은 없다.

문자 작은 따옴표
작은 따옴표 표현은 작은 따옴표 연속
'문자'
'She''s'
숫자 L, D, F 10L, 10.0D, 10.0F
날짜 DATE{d 'yyyy-mm-dd'}
TIME{t 'hh:mm:ss'}
DATETIME{ts 'yyyy-mm-dd hh:mm:ss.f'}
{d '2022-12-22'}
{t '23:59:59'}
{ts '2023-12-23' 23:59:59.123'}
불린 TRUE, FALSE
열거형 패키지 포함 전체 경로 kr.co.test.EnumType.A
엔티티 타입 엔티티 타입을 표현, 주로 상속과 관련해서 사용 TYPE(m) = Member

 

연산자 우선 순위

  • 경로 탐색 연산
    .
  • 수학 연산
    +, -, *, /
  • 비교 연산
    =, >, >=, <, <=, <>,
    [NOT] BETWEEN,
    [NOT] LIKE,
    [NOT] IN,
    IS [NOT] NULL,
    IS [NOT] EMPTY,
    [NOT] MEMEBER [OF],
    [NOT] EXISTS
  • 논리 연산
    NOT, AND, OR

 

 

컬렉션

컬렉션 식은 컬렉션에만 사용하는 특별한 기능

 

컬렉션 비교

  • {컬렉션 연관 경로} IS [NOT] EMPTY
    컬렉션이 비었으면

 

 

 

컬렉션의 멤버

  • {엔티티나 } [NOT] MEMBER [OF] {컬렉션 연관 경로}
    컬렉션에 포함된 값이면

 

 

스칼라

https://openjpa.apache.org/builds/2.4.2/apache-openjpa/docs/jpa_langref.html#jpa_langref_scalar_expressions

 

https://docs.jboss.org/hibernate/orm/4.3/devguide/en-US/html/ch11.html#ql-exp-functions

 

문자함수

  • CONCAT(문자, 문자,)
    HQL
      || 지원한다
  • SUBSTRING(문자, 위치, 길이)
  • TRIM(문자)

TRIM( BOTH 'A' FROM 'AAAA 문자 AAAA')

  • LOWER(문자)
  • UPPER(문자)
  • LENGTH(문자)
  • LOCATE(찾을값, 대상값 [, 검색 시작 위치])

 

수학 함수

  • ABS(숫자)
  • SQRT(숫자)
  • MOD(숫자, 제수)
  • SIZE(컬렉션)
    컬렉션 크기
  • INDEX(별칭)
    리스트 타입 컬렉션 위치 값을 구한다. @OrderColumn 사용하는 경우만 사용 가능

 

날짜 함수

  • CURRENT_DATE
  • CURRENT_TIME
  • CURRENT_TIMESTAMP

HQL 주어진 날짜에서 특정 값을 추출하는 함수를 지원한다.

 

CASE

단순 방법

  • CASE
    WHEN {
    조건식} THEN {결과}
    ELSE {
    결과}
    END

 

기본 방법

  • CASE
    WHEN {
    조건식} THEN {결과}
    WHEN {
    조건식} THEN {결과}
    ELSE {
    결과}
    END

 

 

NULLIF

  • NULLIF ({}, {비교값})
    값이 같으면 NULL 반환
    아니면
    반환

 

COALESCE

  • COALESCE({1}, {값2}, …)
    앞에서 부터 NULL 아니면, 값을 반환

 

다형성 쿼리

JPQL 부모 엔티티를 조회하면 자식 엔티티도 함께 조회

싱글 테이블 전략



@Entity
@Data
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
        @Id
        @GeneratedValue
        @Column(name = "item_id")
        private Long id;
        
        private String name;
        private int price;
        private int stockQuantity;
}


@Entity
@DiscriminatorValue("B")
@Data
public class Book extends Item{
        private String author;
        private String isbn;
}


@Entity
@DiscriminatorValue("M")
@Data
public class Movie extends Item{
        private String director;
        private String actor;
}


@Entity
@DiscriminatorValue("A")
@Data
public class Album extends Item{
        private String artist;
        private String etc;
}

 

 

 

조인 테이블 전략

 

테이블 클래스 전략

 

 

TYPE

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

 

조인 테이블 전략

 

테이블 클래스 전략

 

 

단일 테이블 전략

 

 

TREAT

상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 사용

FROM, WHERE 절에만 사용
하이버네이트는 SELECT 절도 사용 가능

 

 

사용자 정의 함수 호출

따로 자기가 사용하는 DB 방언을 상속해 직접 구현하고 등록해야 한다.

 

기타 정리

  • enum 동등 비교 연산만 된다
  • 인베디드 타입은 비교를 지원하지 않는다

 

EMPTY STRING

JPA 표준은 '' 길이 0 EMPTY STRING으로 취급한다.

DB 마다 ''   NULL 처리하는 경우가 있어 조심해야 한다.

 

NULL 정의

  • 없는 , NULL 수학적 연산을 시도하면 전부 NULL
  • NULL == NULL FALSE 이다. 예산이 불가능
     

  • NULL 연산은 전용 연산자를 사용한다

 

 

 

엔티티 직접 사용

 

 

 

JPQL 에서 엔티티 객체를 직접 사용하면, sql에서 기본키를 사용한다.

 

 

 

 

엔티티를 직접 사용하는 경우 파라미터도 기본 키로 치환된다.

다만, 엔티티를 직접 사용하는 경우 파라미터 인자로 엔티티를 받아야 한다.

"기본키 = 엔티티 파라미터" 또는 "엔티티 파라미터 = 기본키" 이렇게 불일치하는 경우는 안된다.

 

엔티티 직접 사용은 기본 뿐만 아니라 외래 키에도 동일하다.

 

 

 

Named 쿼리 : 정적 쿼리

JPQL 크게 동적 쿼리 / 정적 쿼리로 나눌 있다.

 

  • 동적 쿼리
    JPQL 직접 문자열로 완성해서 넘기는 쿼리
    JPQL 작성을 동적으로 가능
  • 정적 쿼리
    미리
    정의한 쿼리에 이름을 부여해 필요할 사용
    정의하면 변경 없음(정적)

 

Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱한다.

따라서 로딩 시점에 오류 파악이 가능하다. 추가로 파싱된 쿼리를 재사용한다.

 

@NamedQuery 또는 xml 정의한다.

 

@NamedQuery

 

 

 

XML 정의

어노테이션으로 작성하는게 편리하나, 자바는 특성상 멀티라인 편집을 지원하지 안아 불편한 점이 존재한다.

이럴 사용한다.

 

우선권

XML 애노티에션에 같은 설정이 존재하면 XML 우선권을 가진다.

JPA뿐만 아니라 거의 모든 설정이 규칙을 따른다.

 

Criteria

JPQL 자바 코드로 작성하도록 도와주는 빌더 클래스 API

 

코드로 SQL 작성하기 때문에 컴파일 타임에 오류를 발견할 있고, 동적 쿼리 작성이 쉬워진다.

 

다만, Criteria 자체가 가독성이 낮고, 복잡하다

 

기초

 

 

조회

엔티티 반환, 컬럼 조회

 

 

 

 

DISTINCT

 

 

NEW, construct()

 

 

 

 

튜플

Map 비슷한 특별한 반환 객체

별칭은 필수

 

 

 

Object[] 컬럼들을 다루는 것보다 훨씬 안전하다.

 

집합

 

 

조인

 

 

JoinType 생략하면 기본이 내부 조인

 

 

 

서브 쿼리

일반 서브 쿼리

 

 

상호 관련 서브 쿼리

 

 

IN

 

 

CASE

 

 

파라미터 정의

 

 

앞서 예제랑 사실 차이가 없다

? 부분이 바인딩 변수가 들어갈 자리인데

하이버네이트는 그냥 where 값을 넣어도 PrepareStatement 처리한다.

 

 

네이티브 함수 호출

 

 

동적 쿼리

 

 

코드로 동적 쿼리를 작성할 있어, 문자열로 동적 쿼리를 작성할 발생하는 문제가 없다.

 

Criteria 메타 모델 API

많은 부분을 코드로 대체했지만, 값을 가져오는 부분은 문자열을 직접 다룬다.

오타가 있고, 부분은 컴파일 시점에오류로 감지할 없다.

 

메타 모델 적용 방법

https://docs.jboss.org/hibernate/stable/jpamodelgen/reference/en-US/html_single/#d0e76

 

pom.xml

의존성 추가

컴파일 설정

엔티티 이름 + _ 클래스가 생성된 메타 모델이다.

정식 명칭은 Canonical Metamodel

 

이제 절대 오타가 없다.

 

 

QueryDSL

JPQL 쿼리 빌더 클래스

Criteria 보다 직관적이고 이해가 쉽다.

조회에 특화되어 있다.

 

QueryDSL 설정

java 상표권 문제로 jakarta 바뀌면서 자동 생성 실패하는 사람이 설정을 하면 가능성이 높다.

 

 

Q 시작하는 클래스가 Q클래스

 

시작

 

Criteria 비교해서 훨씬 이해하기 쉽다.

 

검색 조건 쿼리

참고용

ComparableExpressionBase

 

스테틱 임포트를 사용하면 간결한 표현력을 얻을 있다

모르는 사람이 봐도 SQL 지식만 있다면 바로 이해가 정도로 표현력이 직관적이다.

 

페이징

 

 

그룹

 

 

조인

내부 조인

 

 

외부 조인

 

 

세타 조인

 

 

서브 쿼리

 

 

DTO 반환

 

생성자는 인수 리스트 타입을 맞추면 된다.

나머지 방법은 필드명이 일치해야 한다. 불일치 경우 별칭을 주어 Dto 필드에 맞추면 된다.

 

 

 

수정, 삭제 배치 쿼리

JPQL 배치 쿼리와 같이 영속성 컨텍스트는 무시하고 DB 직접 쿼리한다는 점을 유념해야 한다.

 

 

 

동적 쿼리

 

 

메소드 위임

기능을 사용하면 쿼리 타입 검색 조건을 Q클래스로 만들 있다.

static 메서드로 만들고 @QueryDelegate 어노테이션을 사용하면

위와 같이 Q클래스에 해당 메서드를 위임 구성으로 호출하는 메서드가 생성된다.

 

 

 

 

 

네이티브 SQL

JPQL 표준 SQL 지원하는 대부분을 커버할 있다. 하지만 특정 DB 종속적인 기능을 지원하진 못한다.

때로는 DB 종속적인 기능이 필요할 때가 있다.

 

  • 특정 DB 지원하는 함수
  • 특정 DB 지원하는 힌트
  • UNION, INTERSECT 같은 집합 연산자
  • 특정 DB 지원하는 문법

 

JPA SQL 직접 사용할 있는 기능을 지원하는데. 이럴 경우 JDBC 직접 다루는 것과 차이는 네이티브 SQL 사용하면 엔티티를 조회할 있고 JPA 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 있다 점이 다르다.

 

네이티브 SQL 사용

 

엔티티 조회

 

 

조회

스칼라 값으로 조회했으므로, 엔티티가 아니라서 영속성 컨텍스트로 관리되지 않는다.

 

 

결과 매핑 사용

엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡할 경우 사용한다.

@SqlResultSetMapping 애노테이션으로 결과 매핑을 정의한다.

 

 

 

 

 

결과 매핑 애노테이션

@SqlResultSetMapping

@EntityResult

@FieldResult

@ColumnResult

 

Named 네이티브 SQL

 

 

 

 

Named 쿼리는 XML 정의할 있다.

XML 정의하는 만큼 자바 코드에서 가장 불편한 멀티라인 문자열 처리에서 자유롭다.

, 라인 수가 많고 SQL 최적화해야 하는 경우엔 어노테이션보다 XML 정의하는게 편하다.

 

어느 쪽으로 정의하던 이를 사용하는 코드는 변경이 없다.

 

페이징

 

 

스토어드 프로시저

 

순서 기반 파라미터 호출

 

 

 

이름 기반 파라미터 호출

 

 

Named 스토어드 프로시저

역시 애노테이션 방식 대신 XML 방식으로 기술할 있다.

 

 

 

 

객체지향 쿼리 심화

  • 벌크 연산(대용량 데이터 처리)
  • JPQL 영속성 컨텍스트
  • JPQL 플러시 모드

 

벌크 연산

대용량 엔티티를 모두 수정, 삭제를 변경 감지 기능으로 처리하는 것은 시간이 너무 오래 걸린다.

이럴 벌크 연산을 사용한다.

 

전체 가격을 10% 인상

 

벌크 연산 주의점

 

 

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.

따라서 영속성 컨텍스트는 캐시된 엔티티를 반환했다.

 

 

해결 방안

 

 

 

 

 

벌크 연산은 영속성 컨텍스트와 2 캐시는 무시하고, DB 직접 질의하기 때문에 영속성 컨텍스트와 DB 사이 데이터 불일치가 발생할 있다.

가능하면 벌크 연산을 가장 먼저 수행하는 것이 좋다. 불가능하다면, 벌크 연산 이후 바로 영속성 컨텍스트를 초기화해야 한다.

 

영속성 컨텍스트와 JPQL

쿼리 영속 상태인 것과 아닌

JPQL 엔티티, 임베디드 타입, 타입 등을 조회할 있다.

하지만, 엔티티만이 영속성 컨텍스트에서 관리된다.

 

JPQL 조회한 엔티티와 영속성 컨텍스트

 

p1 조회한 쿼리

p2 영속성 컨텍스트에 이미 있어 조회를 하지 않음

 

p3 조회 쿼리

JPQL 일단 DB 먼저 질의를 한다. 그리고 조회된 결과 엔티티의 식별자를 영속성 컨텍스트에 있은 엔티티 식별자와 비교 이미 존재하면 조회된 엔티티를 버린다. 존재 하지 않으면, 영속성 컨텍스트에 저장한다.

 

기존 엔티티를 버리고, 새로운 엔티티르 대체하지 않는 이유

영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장한다.

JPA 조회하던, JPQL 조회하던, 같은 인스턴스임을 보장해야 한다.

추가로 영속화된 엔티티가 수정 중이라면, 새로 대체할 경우 수정 중인 데이터가 유실된다.

 

find(), JPQL

find() 메서드는 엔티티를 영속성 컨텍스트에서 찾고 없으면 DB에서 찾는다. 이런 동작 방식 때문에 영속성 컨텍스트를 1 캐시라 부른다.

 

JPQL 항상 데이터베이스에 SQL 실행해서 결과를 조회한다.

 

JPQL 플러시 모드

플러시는 영속성 컨텍스트의 변경 내역을 DB 동기화하는

 

 

 

AUTO 트랜잭션 커밋 직전 또는 쿼리 실행 직전에 자동으로 플러시를 호출한다.

COMMIT 커밋 시에만 플러시를 호출한다.

 

쿼리와 플러시 모드

JPQL 영속성 컨텍스트를 건너뛰고 DB 바로 조회한다.

, JPQL 실행 전에 영속성 컨텍스트를 플러시하는 작업이 필요하다.

 

 

 

하지만, 해당 식별자로 이미 값이 존재해 버려진다.

 

 

 

커밋 직전에 더티 채킹으로 영속성 컨텍스트의 변경 사항이 DB 동기화된다.

 

 

 

 

플러시 모드와 최적화

FlushModeType.COMMIT 모드는 트랜잭션 커밋 시에만 플러시 한다.

잘못하면 영속성 컨텍스트와 DB 사이 데이터 무결성이 깨질 수가 있다.

 

FlushModeType.COMMIT 모드는 플러시 횟수를 줄여 최적화할 제한적으로 사용되야 한다.

 

만약 JPA 사용해 DB 질의하는 것이 아닌 직접 JDBC 사용하는 경우, JPA 이를 감지할 방법이 없기 때문에 무조건 수동으로 플러시를 해줘야 한다.

 

정리

  • JPQL SQL 추상화했다. 특정 DB SQL 의존하지 않는다.
  • Criteria, QueryDSL JPQL 생성 빌더 역할만 한다.
  • Criteria JPA 공식 기능, QueryDSL 오픈 소스지만 직관적이고 쉬워서 자주 사용된다.
  • JPA로도 네이티브 SQL 실행할 있다.
  • JPQL 벌크 연산을 지원한다

'개발 > JPA' 카테고리의 다른 글

13 웹 애플리케이션과 영속성 관리  (1) 2024.06.10
12 스프링 데이터 JPA  (1) 2024.06.03
09 값 타입  (0) 2024.05.20
08 프록시와 연관관계 관리  (1) 2024.05.13
07 고급 매핑  (0) 2024.05.06

+ Recent posts