JPA?

정의

  • Java Persistence API ::= Java ORM(Object-Relational Mapping)을 위한 명세
  • 객체지향 설계와 RDB의 설계를 둘 다 해치지 않으면서 둘 사이의 매핑을 지원 ─ 즉, 필요한 SQL을 자동으로 구성

  • SQL과 유사한, 객체를 이용한 JPQL(Java Persistence Query Language) 지원
  • 객체 상속 관계 사용 가능
  • 객체 참조 관계 유지 가능

JPA 사용

Hibernate와 같은 JPA 구현체(JPA Provider)를 이용하거나, 직접 구현하여 이용하면 된다

Spring Data JPA

JPA에 대한 추상화된 모듈을 제공하여 보다 쉬운 사용을 가능하게 한다

JPA 사용 정리

예시 코드

설명링크
전체 프로젝트jpa-example
예시 코드 - 기본 CRUD
예시 코드 - Converter
예시 코드 - Embed
예시 코드 - @OneToOne 단방향
예시 코드 - @OneToOne 양방향
예시 코드 - @ManyToOne 단방향
예시 코드 - @ManyToOne, @OneToMany 양방향
예시 코드 - @OneToMany 단방향
예시 코드 - @ElementCollection
예시 코드 - @JoinTable
예시 코드 - InheritanceType.SINGLE_TABLE
예시 코드 - InheritanceType.JOINED
예시 코드 - InheritanceType.TABLE_PER_CLASS

EntityManager

Persistence context; 영속 컨텍스트

  • DB에서 읽어오거나 DB로 삽입하고자 추가한 객체들은 영속 객체(Persistence object)들로, EntityManager가 관리하는 영속 컨텍스트에 보관된다
  • 트랜잭션(@Transactional 포함) 안에서 엔티티 수정 시 트랜잭션 커밋 시점에 자동 반영. 또는 flush()로 즉시 반영

  • 트랜잭션 커밋 시점 또는 명시적인 flush() 호출로 영속 컨텍스트의 변경 사항이 DB에 실제 반영된다
  • 영속 객체는 (타입 + 식별자)를 키로 하는 Map으로 관리된다
    • 이를 이용해 조회(find)하려는 엔티티가 이미 컨텍스트에 있다면 DB에 쿼리를 보내지 않고 즉시 컨텍스트의 객체를 반환한다
    • @Id를 @GeneratedValue(strategy = GenerationType.IDENTITY)로 구성하는 경우, 커밋 전에 식별자를 얻기 위해 미리 insert 쿼리가 실행된다
  • 영속 객체 생애 주기
  • Entity Life Cycle
    <이미지 - Entity Life Cycle>
    • Managed : 변경이 존재하면 DB에 반영됨
    • Detached : EntityManager가 닫혔거나, 롤백되어 분리된 상태. 변경이 DB에 반영되지 않음
    • Removed : 커밋 또는 flush() 호출 시 DB에서 삭제

EntityManager를 직접 관리하는 경우

  1. EntityMaganerFactory를 만든다
  2. EntityManager가 필요할 때마다 팩터리로부터 하나 생성하고, 사용한 뒤에 직접 닫는다
  3. 트랜잭션이 여러 메서드에 걸쳐있는 경우, 동일한 EntityManager를 이용함을 직접 보장해야 한다
  4. 메서드 파라미터로 넘기거나 ThreadLocal을 이용하거나... 구현은 자유

EntityManager를 컨테이너에서 관리하는 경우

@PersistenceContext로 현재 EntityManager 인스턴스를 주입받으면 된다. Spring Data JPA 에서는 @Autowired도 가능

메서드

  • void persist(Object entity)
  • 객체를 영속 컨텍스트에 등록하여 변경이 DB에 반영되도록 한다

  • <T> T find(class<T> entityClass, ...)
  • 지정 (타입 + 기본키)를 이용해 객체를 조회한다. 영속 컨텍스트에 있다면 DB를 조회하지 않고 해당 객체를 반환한다. DB에도 없으면 null 리턴

  • <T> T getReference(class<T> entityClass, Object primaryKey)
    • 쿼리 지연 실행. 우선 T 프록시를 반환. 프록시 최초 사용 시 쿼리 실행되며, DB에 없으면 EntityNotFoundException
    • 사용하는 EntityManager를 닫기 전에 프록시를 사용해야 쿼리가 실행된다
    • Hibernate의 경우, final entity class에 대하여 쿼리 즉시 실행
  • void remove(Object entity)
  • 객체를 DB에서 삭제하도록 한다

  • void detach(Object entity)
  • 객체를 영속 컨텍스트로부터 분리한다. 따라서 영속 객체의 변경 사항이 DB에 반영되지 않도록 한다

  • void clear()
  • 영속 컨텍스트를 비우고 모든 객체를 detached 상태로 변경한다

  • <T> T merge(T entity)
  • 객체를 현재 영속 컨텍스트에 등록한다

  • boolean contains(Object entity)
  • 객체가 영속 컨텍스트에 포함됐는지 여부

  • void flush()
  • 영속 컨텍스트를 DB로 동기화한다

  • void refresh(Object entity, ...)
  • 특정 객체를 DB로부터 동기화한다

  • void lock(Object entity, LockModeType lockMode, ...)
  • 영속 컨텍스트의 객체를 잠근다

@Entity

  • DB 입출력 단위
  • 기본 생성자 필수. @Id 최소 한 개 지정 필수
  • 원활한 프록시 동작을 위해 protected 이상, non-final 클래스로 정의

↓ @Entity 예

@Entity public class MMonster {} @Entity(name = "m_monster_v2") public class MMonster {}

@Table

  • @Entity 클래스에 name, catalog, index, schema, UniqueConstraint 설정 추가
  • name 속성만 필요하다면 @Entity에 직접 설정 가능

@Id

기본키 컬럼 명시. 기본 타입 + Wrapper 타입, Date, BigInteger, BigDecimal 타입만(should) 가능

@GeneratedValue, @SequenceGenerator, @TableGenerator

    ↓ 기본키 자동 설정

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId;
  • strategy = GenerationType.IDENTITY
  • DB 설정(MySQL auto_increment, PostgreSQL serial) 그대로 이용. insert 실행 후 실제 값이 설정된다

  • generator = "gen_name" : 시퀀스 또는 ID 생성 테이블 이용
  • ↓ 1. 시퀀스

    @Id @SequenceGenerator(name = "gen_name", sequenceName = "jpa_user2_user_id_seq", allocationSize = 1) @GeneratedValue(generator = "gen_name") private Long userId;

    ↓ 2. 테이블

    @Id @TableGenerator( name = "gen_name", table = "jpa_id", // 테이블 이름 pkColumnName = "id_name", // 테이블 기본키 pkColumnValue = "jpa_user2_user_id", // 기준 행을 찾기 위한 기본키 값 valueColumnName = "next_id", // id로 사용할 컬럼 이름 initialValue = 0, // 테이블에 행이 없는 경우 초기값으로 이용 allocationSize = 1 ) @GeneratedValue(generator = "gen_name") private Long userId;

@Temporal

date, time, timestamp 컬럼 명시. 각각 java.sql.Date, java.sql.Time, java.sql.Timestamp에 매핑

@Column

    필드 또는 getter 메서드에 대해 컬럼 명시
  • name : 컬럼 이름
  • unique : 이 컬럼만으로 unique key인 경우
  • length : 텍스트 길이
  • precision, scale : decimal 정밀도
  • insertable : insert 포함 여부
  • updatable : update 포함 여부
  • ↓ java

    @Temporal(TemporalType.TIMESTAMP) @Column(insertable = false, updatable = false) // ↑ false면 쿼리 실행 후 컬럼 값이 변경되도 객체에 반영되지 않음 private Date addDate;

@Transient 또는 transient 한정자

해당 필드는 persistence 대상에서 제외

@Enumerated

enum 매핑 : EnumType.ORDINAL, STRING 2가지만 가능

@Convert, @Converter

커스텀 컨버터 지정

@DynamicInsert, @DynamicUpdate

  • Non-null 컬럼만 insert, update 시 이용
  • 세션 당 최초 1번만 적용된다
  • 쿼리 실행 후 생략된 컬럼 값이 변경되더라도, 객체에 설정되지 않는다

다른 엔티티/값 포함

Value Class

  • 여타 언어들의 ValueType에 해당하는 클래스
  • 값을 이용해 다른 인스턴스와 비교하며, 자기 자신을 변경하지 않는다

  • 값을 변경하는 연산은 새 인스턴스를 반환한다
  • JPA에서는 @Embeddable 클래스로 Value Class를 정의한다

유의사항

  • 엔티티 포함이 많아질수록 코드 결합도가 증가한다
  • 모든 테이블이 엔티티인 것은 아니다
  • 서로 다른 두 엔티티의 라이프사이클은 독립적이다
  • 포함하려는 대상과의 관계가 엔티티:엔티티인지, 엔티티:밸류인지 명확히 파악해야 한다
  • 엔티티:밸류 관계를 엔티티:엔티티 관계로 사용하면 불필요한 추가 작업이 발생할 수 있다

  • @OneToMany 매핑을 사용하려는 경우, 정말로 모든 연관 엔티티가 필요한 지 고려할 것
  • @ManyToMany 매핑을 사용하려는 경우, JoinTable을 엔티티로 사용하는 게 낫지 않은 지 고려

Hibernate 컬렉션 자료형

사용 타입Hibernate가 실제로 인스턴스화하는 타입
ListArrayList
SetHashSet
MapHashMap
SortedSetTreeSet
SortedMapTreeMap

Sorted 자료형의 정렬자 지정

  • 요소가 Comparable하면 @org.hibernate.annotations.SortNatural 사용
  • 그렇지 않으면 @org.hibernate.annotations.SortComparator로 Comparator 지정

@org.hibernate.annotations.OrderBy

JPA의 것과 다르게 JPQL order by 절을 이용해 DBMS가 정렬하도록 한다

↓ java

@OrderBy("score desc")

@Embedded로 @Embeddable 타입 포함

  • 엔티티 필드 일부를 하나의 @Embeddable 클래스로 묶을 수 있다.
  • @AttributeOverride를 이용해 동일 @Embeddable 클래스를 여러 개 이용하는 경우에 대처할 수 있다
  • @Embeddable 클래스의 필드 일부도 다른 @Embeddable 클래스로 묶을 수 있다

@Embeddable 클래스로 복합키 정의

복합키 클래스는 Serializable 인터페이스를 구현하고, equals(), hashCode()를 적절히 재정의해야 한다.

@SecondaryTable 포함

  • 포함하는 대상은 secondary table, 포함하는 쪽은 primary table
  • Default로 primary table의 기본키를 그대로 이용하여 조회한다
  • pkJoinColumns으로 secondary table 기본키를 설정할 수 있다
  • Select에 이용되는 쿼리는 left outer join이다

엔티티 사이의 1:1, 1:N, N:1, M:N 매핑

@OneToOne

  • 1→1 단방향 매핑
    • @JoinColumn으로 기본키를 설정한다. @SecondaryTable과는 다르게 조인 컬럼 자체를 영속 필드로 가질 수는 없고, 대신 매핑된 엔티티 변경에 따라 자동으로 조인 컬럼 값을 변경한다
    • 두 테이블의 기본키가 동일한 경우 @JoinColumn 대신 @PrimaryKeyJoinColumn을 이용할 수 있다
    • 이 경우 신규 엔티티 삽입을 위해 식별자가 필요하므로, 참조하는 테이블의 신규 엔티티가 먼저 영속 컨텍스트에 등록되어야 한다

  • 1→1 단방향 매핑을 그대로 양방향 매핑으로 이용
  • ↓ java

    @OneToOne(mappedBy = "fieldName")

@ManyToOne, @OneToMany

  • N→1 단방향 매핑
  • @OneToOne 단방향 매핑과 다를 게 없다. 다만 N:1 관계이므로 두 테이블의 기본키가 동일할 수는 없다

  • N→1 단방향 매핑을 그대로 양방향 매핑으로 이용
  • ↓ java

    @OneToMany(mappedBy = "fieldName")
  • 1→N 단방향 매핑

@JoinTable

  • JoinTable? : 검색에 필요한 두 테이블의 키 컬럼만 모은 테이블
  • 1→N 단방향 매핑 예
  • @OneToMany 대신 @ManyToMany 이용 가능
  • mappedBy를 이용해 양방향 매핑 가능

fetch 속성 - 쿼리 지연 실행

Default로 처음부터 연관 테이블을 함께 조회(left outer join)한다. fetch 속성으로 변경할 수 있다

↓ java

@OneToOne(fetch = FetchType.LAZY)

cascade 속성 - 영속성 전이 규칙

  • 기본값은 {}으로 아무런 추가 작업이 없다
  • CascadeType.PERSIST : EntityManager::persist 시 연관 엔티티도 추가
  • CascadeType.REMOVE : EntityManager::remove 시 연관 엔티티도 삭제
  • CascadeType.DETACH : EntityManager::detach 시 연관 엔티티도 분리
  • CascadeType.REFRESH : EntityManager::refresh 시 연관 엔티티도 갱신
  • CascadeType.MERGE : EntityManager::merge 시 연관 엔티티도 추가
  • CascadeType.ALL

@ElementCollection으로 프로젝션

  • Embeddable 클래스도 가져올 수 있다
  • 당연히 JPQL로도 가능하다

클래스 상속과 테이블 매핑

InheritanceType.SINGLE_TABLE

클래스 계층 구조가 하나의 테이블을 공유하고, 각각 일부분씩 매핑. 구분을 위한 컬럼(@DiscriminatorColumn) 존재

InheritanceType.JOINED

각 클래스는 키를 공유하는 별개의 테이블에 대응

InheritanceType.TABLE_PER_CLASS

각 클래스는 별개 테이블에 대응 → 키 관리에 유의

@MappedSuperclass - 공유 컬럼 정의

add_date, upd_date 같은 일반적인 공유 컬럼을 한 곳에 정의할 수 있다

트랜잭션

RESOURCE_LOCAL 타입 트랜잭션

  • JPA의 EntityTransaction을 이용한 트랜잭션 처리
  • Spring Data JPA는 공유 EntityManager 인스턴스로 직접 트랜잭션 관리하는 것을 허용하지 않는다
  • ↓ java

    @Autowired private EntityManager manager; @Test void test() { assertFalse(manager.isJoinedToTransaction()); manager.getTransaction().begin(); // java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead manager.getTransaction().commit(); }

JTA(Java Transaction API) 타입 트랜잭션

  • 직접 트랜잭션을 관리하는 경우
  • ↓ java

    UserTransaction transaction; EntityManager manager; transaction.begin(); manager.joinTransaction(); // 트랜잭션에 참여시킨다. isJoinedToTransaction()으로 참여 여부 확인 가능 // 트랜잭션 시작 후에 EntityManager가 초기화됐다면 자동으로 참여된다 try { transaction.commit(); } catch { transaction.rollback(); }
  • 외부 컨테이너에서 관리하는 경우 : @Transactional 이용
    • @Transactional은 전후에 트랜잭션의 시작, 커밋을 자동으로 진행한다
    • RuntimeException을 포함하여, 예외를 감지하면 자동으로 롤백한다

    • 클래스, 메서드 둘 다에 적용 가능하고, 메서드에 붙으면 클래스의 것을 오버라이드한다
    • @Transactional 메서드가 다른 @Transactional 메서드를 호출하고, 해당 메서드에서 롤백이 발생하면 default로 전역 롤백한다
    • setGlobalRollbackOnParticipationFailure-boolean-로 변경 가능

    ↓ java

    @Test @Transactional @Rollback(false) // 테스트 자동 롤백하지 않도록 설정 void updateUser() { assertTrue(manager.isJoinedToTransaction()); }

잠금

선점 잠금; Pessimistic lock; select for update

트랜잭션 내에서, 차후 수정을 위해 미리 행을 잠근다

↓ java

// 즉시 entityManager.find(..., LockModeType.PESSIMISTIC_WRITE); // timeout hints = new HashMap<String, Object>(); hints.put("javax.persistence.lock.timeout", 1234); entityManager.find(..., LockModeType.PESSIMISTIC_WRITE, hints); // JPQL query = entityManager.createQuery(...); query.setLockMode(LockModeType.PESSIMISTIC_WRITE);

비선점 잠금; Optimistic lock

테이블에 버전 컬럼(정수, timestamp)이 존재하여, 순차적인 update만 허용하도록 강제

↓ java

@Entity public class Something { // ... @Version private Integer ver; }

하이버네이트 관련 추가 기록

  • 지연 로딩된 프록시 인스턴스의 실제 클래스 반환
  • ↓ java

    HibernateProxyHelper.getClassWithoutInitializingProxy(proxy)
  • 쿼리 생성 설정
  • DBMS 버전에 맞춰 설정 필요

    ↓ ini

    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect
  • 로깅 관련 설정
  • ↓ application.properties

    spring.jpa.properties.hibernate.show_sql=true # 실행 sql 출력 spring.jpa.properties.hibernate.format_sql=true # sql 보기 좋게 출력 spring.jpa.properties.hibernate.use_sql_comments=true # sql 주석 출력 logging.level.org.hibernate.type.descriptor.sql=trace # 컬럼 값 출력
  • @Subselect
  • 쿼리 결과를 엔티티로 사용할 수 있게 해준다

    ↓ java

    @Entity @Immutable // 읽기 전용 @Subselect("select ...") @Synchronize({"Entity1", "Entity2"}) // Subselect 실행 전에 Entity1, Entity2 타입 엔티티 중 변경이 있는 것은 DB에 반영 public class ClassName {}

JPA Query

JPQL; JPA Query Language

  • SQL과 유사한 쿼리 언어로, 테이블-컬럼 대신 엔티티-속성 이름을 이용한다
  • ↓ sql

    SELECT ... FROM ... [WHERE ...] [GROUP BY ... [HAVING ...]] [ORDER BY ...]
  • 엔티티 이름 ::= @Entity 적용한 클래스 이름. name 속성을 이용한 경우 해당 값
  • ↓ java

    query = entityManager.createQuery(query[, ResultType.class]); result = query.getResultList(); result = query.getSingleResult(); // 결과가 1개임이 보장되는 경우
  • 일반적인 SQL 문도 사용할 수 있다
  • ↓ java

    query = entityManager.createNativeQuery("insert into t_user_12 (id, name) values (?, ?)"); query.setParameter(1, 123).setParameter(2, "name"); query.executeUpdate();
  • JPQL은 항상 실행되며, 결과 엔티티가 이미 컨텍스트에 존재하면 버려진다
  • update, delete는 기존 영속 컨텍스트에 반영 안 됨
  • ↓ java

    query = entityManager.createQuery("update User set level = level + 1"); query.executeUpdate();

select 절

  • select 절은 반환할 매핑에 대한 완전한 정보를 가져야 한다. 따라서 "select *"은 유효한 문법이 아니다

↓ sql

select id, name from Card select c.id, c.name from Card as c select c.id, c.name from Card c select c from Card c select distinct name from Card select new io.github.donggi.jpa.entity.GachaOdds$Minimal(id.seqNo, objectId, odd) from jpa_gacha_odds

from 절(조인)

  • where 절에서 엔티티의 필드가 다른 엔티티를 참조하는 경우, 자동으로 join된다
  • join 명시
  • ↓ sql

    from Entity e join e.field f -- == INNER JOIN from Entity e inner join e.field f from Entity e left join e.field f -- == LEFT OUTER JOIN from Entity e left outer join e.field f -- 엔티티 정의에 명시된 join 조건은 자동으로 on으로 구성되므로 생략 가능 from Entity e join e.field f on f.num > 10
  • join fetch
    • Entity의 field가 다른 엔티티의 참조/컬렉션일 때, join은 해당 필드를 프록시/컬렉션 래퍼로 설정한다. 아래와 같이 join fetch로 실행하면 field까지 모두 가져와 설정한다
    • ↓ sql

      select e from Entity e [left [outer] | inner] join fetch e.field
    • distinct 사용없이 "select e1 from Entity1 e1 join fetch e1.field e2"를 실행한 리스트의 e1과 e2의 개수는 같음에 유의
    • distinct 사용없이 "select e1 from Entity1 e1 join fetch e1.field e2"를 페이징 실행하면 중복을 제거함에 유의
    • 데이터가 많은 경우 큰 오버헤드 발생

where 절

↓ sql

where x.field = 0 where x.field <> 0 where x.field between 0 and 10 where x.field in (0, 1, 2) where x.field not in (0, 1, 2) where x.field like '%ooo%' where x.field not like '%ooo%' where x.field is null where x.field is not null where x.collection is empty where x.collection is not empty where :obj member of x.collection where :obj not member of x.collection where (condition1 and condition2) or condition3

↓ java

query = entityManager.createQuery("... where x.field = ?"); query.setParameter(0, obj); query = entityManager.createQuery("... where x.field = :obj"); query.setParameter("obj", obj);

group by 절

집계함수 없이 사용 == distinct

↓ sql

select x.id from X x group by x.id

집계함수

함수반환 타입
countLong
max, min대상 타입
avgDouble
sumLong, Double, BigInteger, BigDecimal

집계함수 사용 without group by

↓ sql

select count(u) from User u

집계함수 사용 with having

↓ sql

select u.type, count(u) from User u where u.addDate > '2020-01-02' group by u.type having count(u) > 100

order by 절

  • select 절에 정의한 매핑에 포함되거나, 그로부터 직접 도달할 수 있는 필드만 이용 가능하다
  • 또, 정렬 가능하기 위해 필드의 타입은 Comparable해야 한다

↓ sql

order by x.field order by x.field1 desc, x.field2

페이징

↓ java

query = entityManager.createQuery(...); query.setFirstResult(10); // 10번째부터 query.setMaxResults(3); // 최대 3개 획득

서브쿼리

where, having 절에서 사용 가능

↓ sql

where exists(select ...) where 100 > all(select ...) where 100 = any(select ...) where x.field in (select ...)

조건식

  • case when condition then expr1 else expr2 end
  • case target when expr1 then expr2 else expr3 end
  • coalesce(expr1, expr2[, ...])
  • 최초로 null이 아닌 것 반환. SQL의 IFNULL 대신 사용 가능

  • nullif(expr1, expr2)
  • 두 값이 같으면 null. 다르면 expr1

리터럴

타입표기
문자열'string', 'It''s good'
123, 123L, 123D, 123F
시각DATE {d '2000-01-01'}
TIME {t '12:34:56'}
TIMESTAMP {ts '2000-01-01 12:34:56.789'}
Booleantrue, false
Enumpackage.EnumName.FIELD_NAME

함수

문자열

  • concat(str1, str2[, ...])
  • substring(str, start[, length])
  • trim([METHOD] [CHAR] from] str)
    • METHOD : LEADING, TRAILING, BOTH(default)
    • CHAR : default ' '
  • lower(str), upper(str)
  • length(str)
  • locate(search, str[, start])
  • start부터 search 문자열을 찾아 최초 위치(1부터 시작)를 반환한다. 없으면 0

  • abs(math_expression)
  • sqrt(math_expression)
  • mod(math_expression1, math_expression2)

시각

  • CURRENT_DATE
  • CURRENT_TIME
  • CURRENT_TIMESTAMP
  • ↓ Hibernate
  • YEAR(), MONTH(), DAY(), HOUR(), MINUTE(), SECOND()

컬렉션

  • size(collection)
  • index(collection_alias)
  • 특정 인덱스의 요소만 가져올 때 사용한다

    ↓ sql

    select c.name from UserCard uc join uc.cards c where index(c) = 3

타입

  • type(x) : x의 타입
  • treat(x as Entity) : x를 Entity로 캐스팅

Criteria

  • 문자열 대신 Java API로 쿼리 동적 생성
  • ↓ java

    var builder = entityManager.getCriteriaBuilder(); var cQuery = builder.createQuery([ResultType.class]) var root = cQuery.from(RootType.class); cQuery.select(root); var query = entityManager.createQuery(cQuery);
  • Single selection
  • ↓ java

    // select distinct name from Card cQuery.select(root.get("name")).distinct(true)
  • Multi selection
  • ↓ java

    // select name, some.extra from Card // return type == Object[] cQuery.select(builder.array(root.get("name"), root.get("some").get("extra"))) cQuery.multiselect(root.get("name"), root.get("some").get("extra")) // return type == Tuple cQuery.select(builder.tuple(root.get("name"), root.get("some").get("extra"))) // return type == ResultType cQuery.select(builder.construct(ResultType.class, root.get("name"), root.get("some").get("extra")))
  • Join
  • ↓ java

    // select g, o from Gacha g left join g.odds o var g = cQuery.from(Gacha.class); var o = g.join("odds", JoinType.LEFT); // join fetch를 원한다면 g.fetch(...) cQuery.multiselect(g, o);
  • Where
  • ↓ java

    // select c from Card where id > :x var c = cQuery.from(Card.class); cQuery.where(builder.gt(c.get("id"), x));
  • Group by
  • ↓ java

    var c = cQuery.from(Country.class); cQuery.multiselect(c.get("currency"), builder.sum(c.get("population"))); cQuery.where(builder.isMember("Europe", c.get("continents"))); cQuery.groupBy(c.get("currency")); g.having(builder.gt(builder.count(c), 1)); // builder::count, max, min, avg, asum, sumAsLong, sumAsDouble
  • Order by
  • ↓ java

    var c = cQuery.from(Country.class); cQuery.orderBy(builder.asc(c.get("currency")), builder.desc(c.get("population")))
  • Literal
  • ↓ java

    builder.literal(...);
  • Type
  • ↓ java

    builder.equal(expression.type(), builder.literal(TargetType.class));
  • Expressions
  • ↓ java

    builder::and, or, not, isTrue builder::gt, greaterThan, lt, lessThan, equal, between, isNull builder::sum, diff, prod, quot(/), mod, abs, neg, sqrt builder::like, notLike, length, locate, lower, upper, trim, concat, substring builder::isEmpty, isMember, in, size builder::currentDate, currentTime, currentTimestamp

@StaticMetamodel

엔티티 필드에 대한 메타데이터를 제공하여 Criteria 이용시 정적인 타입 체크를 강화해준다

↓ 엔티티

@Entity public class Team { @Id private Long id; @OneToMany(mappedBy = "team") private Set<Player> players; }

↓ 메타 모델

@StaticMetamodel(Team.class) public class Team_ { public static SingularAttribute<Team, Long> id; public static SetAttribute<Team, Player> players; }

↓ Criteria 사용

var team = cQuery.from(Team.class); builder.between(team.get(Team_.id), 100L, 1000L);

메타 모델 자동 생성

"jpa model generator" 검색하면 여러 외부 라이브러리 또는 IDE 애드온이 검색되니 참고

Spring Data JPA 사용

JpaRepository<Entity, ID>

  • 이 인터페이스를 상속하면 findAll, findById, save, delete, flush 등의 메서드를 자동으로 구성한다
  • JpaRepository -> PagingAndSortingRepository -> CrudRepository

  • 명명규칙에 맞는 새 메서드들을 자동으로 구현한다
  • 출처 : jpa.query-methods.query-creation
    KeywordSampleJPQL snippet
    AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
    OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
    Is, EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
    BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
    LessThanfindByAgeLessThan… where x.age < ?1
    LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
    GreaterThanfindByAgeGreaterThan… where x.age > ?1
    GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
    AfterfindByStartDateAfter… where x.startDate > ?1
    BeforefindByStartDateBefore… where x.startDate < ?1
    IsNull, NullfindByAge(Is)Null… where x.age is null
    IsNotNull, NotNullfindByAge(Is)NotNull… where x.age not null
    LikefindByFirstnameLike… where x.firstname like ?1
    NotLikefindByFirstnameNotLike… where x.firstname not like ?1
    StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
    EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
    ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
    OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
    NotfindByLastnameNot… where x.lastname <> ?1
    InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
    NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1
    TruefindByActiveTrue()… where x.active = true
    FalsefindByActiveFalse()… where x.active = false
    IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)

JPQL

↓ java

@Query("select new io.github.donggi.jpa.entity.GachaOdds$Minimal(id.seqNo, objectId, odd) from jpa_gacha_odds where id.gachaId = :gachaId") public List<GachaOdds.Minimal> findMinimalOdds(@Param("gachaId") Long gachaId); // + SpEL @Modifying @Transactional @Query(value = "insert into match_info (accouunt_id, friend_id, match_id) values (:#{#me.key.myID}, :#{#me.key.frID}, :#{#me.matchID}), (:#{#fr.key.myID}, :#{#fr.key.frID}, :#{#fr.matchID})", nativeQuery = true) public void createNewMatch(@Param("me") MatchInfo me, @Param("fr") MatchInfo fr);

페이징

  • Pageable
  • ↓ java

    @Query("...") public Page<ReturnType> methodName(..., Pageable pageable);
  • findFirst, findTop, findFirstN, findTopN 명명규칙 이용
  • ↓ java

    public List<Card> findFirst10(); public List<Card> findFirst10ByName();

Specification을 이용한 검색 조건 조합

  1. Specification을 인자로 받는 검색 메서드 정의
  2. ↓ java

    @Repository public interface ItemDao extends JpaRepository<Item, Long> { public List<Item> findAll(Specification<Item> spec); }
  3. Specification 정의
  4. ↓ java

    public class ItemSpecs { public static Specification<Item> nameLike(String name) { return (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), '%' + name + '%'); } }
  5. Specification static 메서드를 이용한 새 조건 생성
    • not(Specification<T>)
    • where(Specification<T>)
    • and(Specification<T>)
    • or(Specification<T>)