JPA는 SQL을 추상화한 객체 지향 쿼리 언어 JPQL을 제공, 객체를 대상으로 쿼리를 작성하며 특정 DB에 의존하지 않는다.

 

// Member m 별칭 사용 필수
String jpql = "select m From Member m where m.name like '%hello%'";

List<Member> result = em.createQuery(jpql, Member.class).getResultList();
//getResultList() : 항상 리스트 반환, 결과가 없으면 빈 리스트 반환
//getSingleResult() : 단일 객체 반환, 결과가 없거나 둘 이상이면 Exception 발생


//TypeQuery : 반환 타입이 명확할 때
TypedQeury<Member> query = em.createQuery("SELECT m From Member m", Member.class);

//Query : 반환 타입이 명확하지 않을 때
Query query = em.createQuery("SELECT m.username, m.age from Member m");
// 파라미터 바인딩
String jqpl = selct m from Member m where m.username=:username;
em.createQuery(jpql, Member.class).setParameter("username", username);

// DTO로 바로 조회 (패키지명 포함한 전체 클래스명)
// 순서와 타입이 일치하는 생성자 필요
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m

 

JPA는 where, having절에서만 서브 쿼리가 가능하고 from절에서는 서브 쿼리가 불가능하기 때문에 이 경우에는 join으로 풀어서 해결하거나 네이티브 SQL를 사용해서 해결한다. 어려운 경우 쿼리를 분해해서 2번 날리거나 서브 쿼리로 필터가 어느정도 된 결과를 애플리케이션에서 추가로 조작하는 등의 방법도 있다.

 

join을 직접 사용하지 않아도 연관 필드의 객체 그래프를 탐색하면 (m.team) 묵시적 내부 조인이 발생하는데 FROM절에 영향을 줄 수 있고 상황 파악이 어렵기 때문에 명시적으로 조인을 선언해주는 것이 좋다.

 

 


 

페치 조인 (join fetch),  DISTINCT

 

 

JPQL에서 제공하는 페치 조인을 사용하면 조인 시 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회할 수 있다. 글로벌 패치 전략을 지연 로딩으로 사용하는 경우 연관된 엔티티나 컬렉션은 사용 시점에 조회를 하는데 이때 연관 필드를 for문으로 탐색하게 되면 N+1 쿼리 문제가 발생하게 된다. 하지만 페치 조인을 사용하면 지연 로딩을 적용하지 않고 즉시 조회로 연관 엔티티의 값까지 전부 가져올 수 있다.

(일반 조인: SELECT T.* / 페치 조인: SELECT T.*, M.*)

 

String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        //그냥 조인의 경우 member.getUsername()으로 접근 시 멤버 조회 쿼리 발생 (N+1)
        System.out.println(“-> username = " + member.getUsername()+ ", member = " + member); }
}

 

페치 조인 대상에는 별칭을 가급적 사용하지 않는 것이 좋고, 둘 이상의 컬렉션은 페치 조인 할 수 없다.

일대일, 다대일 같은 단일 값 연관 필드가 아닌 대다 컬렉션을 페치 조인하고 페이징 API를 사용하면 하이버네이트는 경고 로그를 남기고 메모리에서 페이징을 하기 때문에 매우 위험하다.

 

 객체 그래프의 사상은 연관된 데이터를 전부 불러오는 것인데 일대다 조인은 데이터가 늘어나기 때문에 페이징을 쓰면 중간에 짤리게 된다. 그래서 일대다 조인의 경우에는 hibernate.default_batch_fetch_size 글로벌 설정으로 BatchSize를 설정하면 (애노테이션도 있지만 글로벌 설정 권장) 설정 size만큼(default 50) select * from x where in (?, ?, ? ...) 이런식으로 조회

 

그래도 페치 조인은 성능 최적화에 매우 중요하기 때문에 글로벌 페치 전략은 지연 로딩으로, 최적화가 필요한 곳은 페치 조인을 적용하는 것이 좋다. (만약 여러 테이블을 join 해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면 페치 조인 대신 기본 조인을 하고 필요한 데이터만 조회해서 DTO로 반환하는 것이 좋다.)

 

추가로 JPQL에서는 join을 할 때 DISTINCT를 사용하면 특별한 기능이 추가 되는데 특정 팀에 해당하는 멤버를 join 할 때, 테이블 관점에서 보면 특정 팀에 해당하는 멤버 수만큼 결과 row가 생기는 것이 맞지만 JPA의 객체 관점에서 보면 팀 객체가 중복되어서 리스트로 반환되는 것이다. 그래서 JPQL은 DISTINCT 사용 시 중복 엔티티를 제거해준다. 

 

 


 

기타

 

JPQL 다형성 쿼리

//JPQL type()
select i from Item i where type(i) IN (Book, Movie)

//SQL
select i from Item i where i.DTYPE in (‘B’, ‘M’)

//JPQL treat()
select i from Item i where treat(i as Book).auther = ‘kim’

//SQL
select i.* from Item i where i.DTYPE = ‘B’ and i.auther = ‘kim’

 

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값(연관 엔티티는 외래 값)을 사용한다.

 

Named 쿼리를 사용하면 JPQL을 애노테이션, XML에 정의해두고 애플리케이션 로딩 시점에 쿼리를 검증해서 정적 쿼리로 사용할 수 있다.

@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {

}

List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();

 

벌크 연산

 

한번에 너무 많은 데이터를 변경하는 경우 변경 감지 기능은 너무 많은 SQL을 생성하는데 벌크 연산을 사용하면 변경 감지 대신 쿼리 한번으로 많은 데이터를 변경할 수 있다. 벌크 연산은 먼저 flush()를 한 다음 영속성 컨텍스트가 아닌 데이터베이스에 직접 쿼리를 전송하는데, 이때 영속성 컨텍스트에 있는 엔티티들은 벌크 연산의 처리 결과가 적용이 안 되어있는 상태다. 그래서 데이터 정합성에 문제 위험이 있기 때문에 1)벌크 연산 후에는 em.clear()로 영속성 컨텍스트 초기화를 해주거나 2)벌크 연산을 먼저 수행하고 조회를 하는 식으로 사용하는 것이 좋다.

 

String qlString = "update Product p " + 
                  "set p.price = p.price * 1.1 " +
                  "where p.stockAmount < :stockAmount";
                  
// executeUpdate() 결과는 변경 된 엔티티 수 반환
// UPDATE, DELETE 지원
int resultCount = em.createQuery(qlString)
                    .setParameter("stockAmount", 10)
                    .executeUpdate();

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

 

프록시

em.find() : 데이터 베이스를 통해 실제 엔티티 객체 조회

em.getReference() : 데이터베이스 조회를 미루는 프록시(가짜) 엔티티 객체 조회

 

프록시 객체는 실제 엔티티 클래스를 상속 받아서 생성되며 실제 엔티티의 참조를 보관하고 있다가 프록시 객체가 호출이 되면 그때 영속성 컨텍스트에 초기화 요청을 해서 실제 엔티티를 생성하고 참조를 통해 실제 엔티티를 호출한다.

 

이때 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라 프록시 객체를 통해 실제 엔티티에 접근하는 것이고 영속성 컨텍스트에 조회 요청 시 실제 엔티티가 이미 있으면 em.geReference() 호출해도 실제 엔티티가 반환된다.

 


 

연관관계 매핑 (fetch)

 

즉시 로딩 (fetch = FetchType.EAGER)

 

즉시 로딩 사용 시, 예상치 못한 SQL이 발생할 수 있고 N+1 문제를 일으킨다. 객체 하나를 조회하기 위한 쿼리를 날렸는데 연관된 객체들을 전부 조회하는 것이다. @ManyToOne, @OneToOne은 기본이 EAGER로 되어있다.

 

 

지연 로딩 (fetch = FetchType.LAZY)

 

참조 객체를 프록시 객체로 가져오고 사용 시점에 실제 객체를 조회하게 된다. 대부분 지연 로딩을 사용하는 것이 좋으며 @OneToMany, @ManyToMany는 기본이 LAZY로 되어있다.

 


 

영속성 전이 (CASCADE)

 

영속성 전이를 사용하면 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 저장하는데 엔티티를 함께 영속화하는 편리함을 제공할 뿐, 연관관계 매핑과는 관련이 없다.

CascadeType.(ALL, PERSIST, REMOVE, MERGE, REFRESH, DETACH)에 따라 범위를 지정할 수 있다.

 

 

고아 객체 (orphanRemoval)

 

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 옵션으로, 참조가 제거된 엔티티를 고아 객체로 보고 삭제하며 CascadeType.REMOVE처럼 동작한다.

참조하는 곳이 하나일 때 사용해야하며, 특정 엔티티가 개인 소유할 때 주로 사용한다. (@OneToOne, @OneToMany만 가능)

 

(CascadeType.ALL + orphanRemoval=true 로 설정 시, 부모 엔티티가 자식 엔티티의 생명 주기를 관리할 수 있다.)

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

 

관계형 데이터베이스는 상속 관계가 없지만 JPA는 객체 상속과 유사한 슈퍼타입, 서브타입의 논리 모델으로 상속관계 매핑을 구현할 수 있다.

 

@Inheritance(strategy=InheritanceType.XXX ) : 상속 선언, 전략 설정

@DiscriminatorColumn(name="DTYPE") : 부모 클래스에서 자식 테이블을 구분할 수 있는 타입 Column

@DiscriminatoerValue("XXX") : 자식 클래스에 입력하는 DTYPE 이름으로 생략 시, 엔티티명으로 생성

 

 

@InheritanceType

 

1. JOINED

각각 테이블로 생성, 외래 키 참조 무결성 제약조건을 따르면서 저장 공간을 효율적으로 나눌 수 있지만 조회 시 조인을 많이 사용하며 쿼리가 복잡하고 INSERT를 2번 호출해야하는 단점이 있다.

 

2. SINGLE_TABLE

DTYPE으로 자식 테이블을 구분할 수 있기 때문에 하나의 테이블에 전부 집어넣는 단일 테이블 방식으로 조회 쿼리가 단순하고 빠르지만 테이블이 너무 커질 수 있고 자식 Column에 null 값을 허용해야 하는 단점이 있다.

 

3. TABLE_PER_CLASS 

조회 시 UNION SQL이 필요하고 성능이 느리고 자식 테이블을 통합해서 쿼리하기 어려운 문제 등으로 권장 X

 

 

@MappedSuperclass

 

공통 속성이 필요한 경우 이를 상속으로 뺄 수 있는데 이는 실제 테이블에 영향을 주는 것은 아니고 단순하게 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할만 한다. 직접 생성하거나 사용할 일이 없으므로 추상 클래스로 사용하는 것이 좋고 엔티티가 생기는 것이 아니기 때문에 당연히 조회, 검색 등은 할 수 없다.

 

 

 

 

 

 

기본적으로 관계형 데이터베이스는 논리적으로 관계가 있는 테이블끼리 연관관계를 설정할 수 있는데 객체 지향 설계에서도 자율적인 객체들간에 협력 관계를 만들기 위해 두 객체 사이에 연관관계를 정의해주어야 한다. 하지만 객체의 연관관계와 테이블 간 연관관계에는 큰 차이가 있다.

 

테이블은 외래 키로 JOIN을 해서 쉽게 테이블의 연관관계를 만들 수 있지만 객체는 테이블에 맞추어 모델링을 하면 객체 지향적인 협력 관계를 만들어내기가 어렵다. 예를 들면 멤버 객체에서 팀 객체의 PK값인 teamId를 외래 키(FK)로 저장하려면 먼저 팀 객체에서 Id를 조회해서 가져와야하고 나중에 팀 객체에 접근하려면 다시 teamId를 통해 찾아야 하는데 이렇게 식별자로 다시 조회하는 것은 객체 지향적이지 않다. 

 

반면 객체의 참조를 사용해서 테이블의 외래 키를 매핑하면 멤버 객체는 팀 객체의 참조를 저장하고 객체 그래프 탐색으로 바로 연관관계를 조회할 수 있다. 멤버 객체에서 팀 객체의 참조를 통해 접근하기 때문에 멤버 -> 팀의 단방향 연관관계라고 한다.

 

JPA에서 외래 키를 매핑할 때는 @JoinColumn을 사용하며 name 기본값은 "필드명_참조 테이블 기본 키 컬럼명", 그 외 unique, nullable, updatable 등이 있다.

 

양방향 연관관계의 경우 테이블은 외래 키 하나로 JOIN을 하면 두 테이블이 자연스럽게 양방향 관계가 되지만 객체의 양방향 연관관계는 객체가 서로 양쪽을 참조하는 것으로 서로 다른 단방향 연관관계 2개를 만드는 것과 같다.

 

 

객체의 양방향 매핑 규칙

 

- 객체의 두 관계중 하나를 연관관계의 주인으로 지정하여 연관관계의 주인만 외래 키를 등록, 수정할 수 있다.

주인이 아닌 쪽은 읽기만 가능하며 mappedBy로 지정을 해줘야 한다.

 

 

연관관계의 주인만 외래 키를 등록, 수정 가능하기 때문에  당연히 외래 키가 있는 쪽(N)을 주인으로 정하는 것이 좋다. (반대의 경우에는 외래 키를 수정하려면 반대쪽 객체에 접근해서 수정을 해야 함)

연관관계 주인이 아닌 쪽(mappedBy)는 읽기만 가능하기 때문에 값을 설정 안 해도 되지만 순수 객체 상태를 고려하여 양쪽에 값을 설정하는 것이 좋다. 

 

기본적으로 단방향 매핑으로 설계를 하며 양방향은 필요할 때 추가하는 것이 좋다. 연관관계 주인이라고 해서 비즈니스 로직을 기준으로 생각하면 안 되고 (멤버가 팀에 속해 있으니 팀이 주인이겠군 -> X), 외래 키를 보유한 쪽을 기준으로 정하며 외래 키는 항상 많은 쪽에 있다.

 


 

연관관계를 매핑할 때는 위에서의 단방향, 양방향과 연관관계의 주인, 그리고 마지막으로 다중성에 대해 고려해야 한다.

 

 

1. 다대일 (@ManyToOne)

 

- 글로벌 페치 전략 fetch = FetchType.EAGER(기본값)

- 영속성 전이 기능 cascade

 

다대일 단방향은 가장 많이 사용하는 연관관계로, 항상 많은 쪽(N)이 참조 필드(외래 키)를 가지고 있기 때문에 외래 키를 가진 쪽에서 참조로 접근을 하는 단방향으로 설계를 한다. 단방향 연관관계는 참조 필드에 @JoinColumns를 붙여서 정의할 수 있다.

 

 

 

2. 일대다 (@OneToMany)

 

- 양방향 시 mappedBy = "연관관계의 주인 필드"

- 글로벌 페치 전략 fetch = FetchType.LAZY(기본값)

- 영속성 전이 기능 cascade

 

일대다 단방향은 외래 키가 다른 테이블에 있고 연관관계 관리를 위해 추가 SQL이 실행되기 때문에 사용하지 않는 것이 좋다. (사용하려면 @JoinColumn 필수, 없으면 Join Table 방식으로 중간에 테이블을 하나 생성함)

 

 

 

3. 일대일 (@OneToOne)

 

일대일 관계는 주 테이블과 대상 테이블 중에 외래 키를 선택할 수 있으며 외래 키에 유니크 제약 조건을 추가한다.

 

주 테이블에 외래 키를 설정하면 주 테이블에서 참조를 통해 대상 테이블에 접근하기 때문에 객체 지향적이며 JPA 매핑이 편리해지고 주 테이블만 조회하면 대상 테이블에 데이터가 있는지 확인이 가능하다. 단점은 값이 없으면 외래 키에 null 을 허용해야 한다.

 

대상 테이블에 외래 키는 주 테이블과 대상 테이블의 관계가 일대일에서 일대다로 변경될 때(많은 쪽이 외래 키) 테이블 구조가 유지되기 때문에 데이터베이스 관점에서 좋지만 프록시 적용이 안 되어 지연 로딩이 되지 않고 항상 즉시 로딩만 되는 단점이 있다. (프록시와 지연 로딩은 뒤에서 다룬다.) 대상 테이블에 외래 키의 경우 단방향은 JPA에서 지원하지 않는다.

 

 

 

4. 다대다 (@ManyToMany)

 

다대다 관계의 경우 @ManyToMany, @JoinTable로 연결 테이블을 지정하고 컬렉션을 사용하면 객체 2개로 연결 테이블을 나타낼 수 있지만 실제로는 연결 테이블이 키값만 있지 않기 때문에 연결 테이블을 엔티티로 만들고 @OneToMany, @ManyToOne으로 연결하는 것이 좋다.

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

+ Recent posts