본문으로 바로가기

객체지향 쿼리언어

category SPRING/JPA 2022. 3. 26. 03:04
728x90
반응형
SMALL

​객체지향 쿼리언어

  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
  • sql을 추상화해서 특정 DB에 의존할 필요가 없음

JPQL

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

SQL을 추상화 하기 때문에 특정 DB에 의존하지 않음

SQL문 보다 간결함

사용

String jpql = "select m from Member as m where m.username = 'kim'";                                                                  
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();

Member는 엔티티 이름이며, m.username 은 엔티티 객체의 필드명이다.

JPQL 조인

내부조인

INNER JOIIN을 사용한다. INNER는 생략할 수 있다.

String teamName = "팀A";
String query = "select m from Member m INNER JOIN m.team t "
             + " where t.name = :teamName";

List<Member> members = em.createQuery(query, Member.class)
                    .setParameter("teamName", teamName)
                    .getResultList();

JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.

m.team이 연관필드인데 연관 필드 는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말한다.

조인한 두개의 엔티티를 조회하려면 다음과 같이 JPQL 을 작성하자

String query = " select m, t from Member m JOIN m.team t"

List<Object[]> result = em.createQuery(query).getResultList();

for(Object[] row : result){
    Member member = (Member)row[0];
    Team team = (Team)row[1];
}

외부조인

select m from Member m LEFT [OUTER] JOIN m.team t

OUTER는 생략이 가능하다

컬렉션 조인

다대일 조인: 단일 값 연관 필드(m.team)

일대다 조인: 컬렉션 값 연관 필드 (m.members)

select t, m from Team t LEFT JOIN t.members m

컬렉션 조인시 JOIN 대신 IN 을 사용할 수 있지만 특별한 장점이 없으므로 JOIN 을 사용하자

세타 조인

WHERE 절을 이용하여 세타 조인을 할 수 있다.

세타 조인은 내부 조인만 지원한다.

select count(m) from Member m, Team t where m.username = t.name

JOIN ON 절

내부 조인의 ON 절은 WHERE절을 사용할 때와 결과가 같으므로 ON 절은 외부 조인에서만 사용한다.

select m, t from Member m left join m.team t on t.name = 'A'

페치 조인

JPQL에서 최적화를 위해 제공하는 기능이다.

연관된 엔티티나 컬렉션을 한번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.

엔티티 페치 조인

회원 엔티티를 조회하면서 연관된 팀 엔티티도 함께 조회

38. 객체지향 쿼리 언어(5)

select m from Member join fetch m.team

폐치 조인은 별칭을 사용할 수 없다.

폐치 조인 사용 코드
String jpql = "select m from Member m join fetch m.team";

List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for(Member member : members){
    //폐치 조인으로 회원과 팀으로 함께 조회해서 지연 로딩 발생 안함
    System.out.println("username = " + member.getUsername() + ", " + "teamname = " + member.getTeam().name());
}

출력결과

username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B

지연 로딩을 설정했다고 가정하자

페치 조인을 사용하여 회원과 팀을 함께 조회했으므로 연관됨 팀 엔티티는 프록시가 아닌 실제 엔티티이다. 지연 로딩이 일어나지 않는다.

프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.

컬렉션 페치 조인

select t from Team t join fetch t.members where t.name = '팀A'

JPA - JPQL 페치조인

일대다 조인은 결과가 증가할 수 있지만

다대일 조인은 결과가 증가하지 않는다.

페치 조인과 DISTINCT

JPQL의 DISTINCT 명령어는 SQL에 DISTINCT 를 추가하는 것은 물론이고

애플리케이션에 한 번 더 중복을 제거한다.

distinct 사용 전

select t from Team t join fetch t.members where t.name = '팀A'

결과

teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x300

위의 컬렉션 페치 조인은 팀A가 중복으로 조회된다.

distinct 사용 후

select distinct t from Team t join fetch t.members where t.name = '팀A'

조회 결과 자체는 달라지지 않았다 그러나 조회되는 엔티티가 달라졌다

38. 객체지향 쿼리 언어(5)

select distinct t 의 의미는 팀 엔티티의 중복을 제거하라는 것이다. 그래서 하나만 조회가 된다.

teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x300
페치 조인과 일반 조인의 차이
select t from Team t join t.members m where t.name = '팀A'

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

팀 엔티티만 조회하고 연관된 회원 컬렉션은 조회하지 않는다.

회원 컬렉션을 지연로딩으로 설정하면

38. 객체지향 쿼리 언어(5)

프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다.

즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한번 더 실행한다.

반면 페치 조인을 사용하면 연관된 엔티티도 함께 조회한다.

select t from Team t join fetch t.members where t.name = '팀A'

페치 조인의 특징과 한계

페치 조인을 사용하면 SQL 한번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화 할 수 있다.

@OneToMany(fetch = FetchType.LAZY)

다음처럼 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라 부른다.

페치 조인은 글로벌 로딩 전략보다 우선한다

때문에 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL 에서 페치 조인을 사용하면 페치 조인을 적용해서 조회한다.

글로벌 로딩 전략은 될수 있으면 지연로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다

  • 페치 조인 대상에는 별칭을 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

명시적 조인

JOIN을 직접 적어주는것

묵시적 조인

경로 표현식에 의해 묵시적을 조인이 일어나는것, 내부 조인만 할 수 있다.

Criteria

JPQL을 생성하는 빌더 클래스

장점은 문자가 아닌 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.

EX) 기존 jpql 에서 "memberee" 처럼 오타가 났다고 가정하자

select m from memberee

컴파일은 성공하고 애플리케이션을 서버에 배포할 수 있다.

그러나 해당 쿼리가 실행되는 런타임 시점에 오류가 발생한다는 치명적인 단점이 있다. 문자기반 쿼리의 단점이다

Criteria는 문자가 아닌 코드로 JPQL 을 작성하기에 컴파일 시점에 오류를 발견할 수 있다.

장점

  • 컴파일 시점에 오류발견 가능
  • IDE 사용하면 코드 자동완성 지원
  • 동적쿼리 작성이 편리

JPQL

select m from Member as m where m.username = 'kim'

Criteria

//Criteria 사용준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);

//루트 클래스(조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);

//쿼리 생성
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));

List<Member> resultList = em.createQuery(cq).getResultList();

m.get("username")

필드명을 문자로 작성했지만 메타 모델 을 사용하면

m.get(Member_.username)

으로 코드 쿼리로서 작성할 수 있다.

그렇지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다.

사용하기 불편하다

코드가 한눈에 들어오지 않는다 는 단점이 있다.

QueryDSL

장점은 코드 기반이면서 단순하고 사용하기 쉽다.

//준비
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;

//쿼리, 결과조회
List<Member> members = query.from(member)
                        .where(member.username.eq("kim"))
                        .list(member);

네이티브 SQL

JPQL 을 사용해도 특정 DB에 의존하는 기능을 사용할 때가 있다.

오라클에서 사용하는 CONNECT BY 기능 또는 특정 DB에서 동작하는 힌트와 같이

JPQL에서 지원하지 않는것은 네이티브 SQL을 사용하자

String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'"

List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

단점은 특정 DB에 의존하기 때문에 DB변경시 SQL문도 변경해야한다

객체지향 쿼리 심화

벌크연산

여러건을 한번에 수정하거나 삭제할때 벌크연산을 사용하자

ex)update 벌크연산

String sql = "update Product p set p.price = p.price * 1.1"+
" where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(sql).setParameter("stockAmount", 10)
                    .executeUpdate();

ex)delete 벌크연산

String sql = "delete from Product p where p.price < :price ";

int resultCount = em.createQuery(sql).setParameter("price", 100).executeUpdate();

executeUpdate() 메소드를 사용하여 한꺼번에 벌크연산을 할 수 있다.

벌크 연산 주의점

영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 하기 때문에 주의하여 사용해야 한다. 영속성 컨텍스트에 있는 정보와 DB에 있는 정보가 서로 다를수가 있다.

  • em.refresh() 사용
    em.refresh(member);
  • 벌크 연산을 수행한 후 엔티티를 사용해야 한다면 em.refresh()를 이용하여 엔티티를 조회하자
  • 벌크 연산 먼저 실행

​ 벌크 연산을 먼저 실행 후 엔티티를 조회하는 순서를 지키자(JPA와 JDBC를 함께 사용하는 방법에 유용)

  • 벌크 연산 수행 후 영속성 컨텍스트 초기화그러면 이후 엔티티 조회시 벌크 연산이 적용된 DB에서 엔티티를 조회한다.
  • 벌크 연산 수행 직후 영속성 컨텍스트를 바로 초기화 하여 남아있는 엔티티를 제거하자.

벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 DB에 직접 실행하여 영속성 컨텍스트와 DB간에 데이터 차이가 발생할수 있기 때문에 주의해서 사용하자

영속성 컨텍스트와 JPQL

쿼리 후 영속 상태인 것과 아닌것

엔티티가 아니면 영속성 컨텍스트에서 관리되지 않는다.

select m from Member m //엔티티 조회 (관리 o)
select o.address from Order o //임베디드 타입 조회 (관리x)
select m.id, m.username from Member m //단순 필드 조회(관리x)

임베디드 타입은 조회해서 값을 변경해도 영속성 컨텍스트가 관리하지 않으므로 변경 감지에 의한 수정이 발생하지 않는다.

물론 엔티티를 조회하면 엔티티가 갖고있는 임베디드 타입은 함께 수정된다.

정리해서, 조회한 엔티티만 영속성 컨텍스트가 관리한다.

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

JPQL로 조회한 엔티티가 영속성 컨텍스트에 있으면 JPQL로 DB에서 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다.

51. 객체지향 쿼리 언어(18)

왜 새로 조회한 것을 버리고 영속성 컨텍스트에 있는 기존 엔티티를 반환하는것일까?

영속성 컨텍스트의 수정 중인 데이터가 사라질 수 있으므로 위험하다

영속성 컨텍스트는 동일성을 보장한다.

find() vs JPQL

em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 DB에서 찾는다. 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점이 있다.(때문에 1차캐시라고 부른다.)

//최초조회, DB에서 조회
Member member1 = em.find(Member.class, 1L);
//영속성 컨텍스트에 있으므로 DB를 조회하지 않음
Member member2 = em.find(Member.class, 1L);

member1 == member2는 주소 값이 같은 인스턴스

JPQL은?

//첫번째 호출: DB에서 조회
Member member1 = em.createQuery("select m  from Member m where m.id = :id", Member.class).setParameter("id",1L).getSingleResult();

//두번째 호출: DB에서 조회
Member member2 = em.createQuery("select m from Member m m where m.id = :id", Member.class).setParameter("id",1L).getSingleResult();

member1 == member2는 주소 값이 같은 인스턴스

em.find()를 2번 사용한 로직과 마찬가지로 주소 값이 같은 인스턴스를 반환한다.

그러나 내부 동작방식은 다르다.

JPQL은 항상 DB에 SQL을 실행해서 결과를 조회한다

51. 객체지향 쿼리 언어(18)

  • JPQL은 항상 DB를 조회한다
  • JPQL로 조회한 엔티티는 영속 상태이다
  • 영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다

JPQL과 플러시 모드

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

JPA는 플러시가 일어날때 영속성 컨텍스트에 등록/수정/삭제한 엔티티를 찾아서 그에 따른 SQL을 만들어 DB에 반영한다.

플러시를 호출하려면 em.flush() 를 직업 사용하거나 플러시모드 에 따라서 커밋 직전이나 쿼리실행 직전에 자동으로 플러시가 호출된다.

em.setFlushMode(FlushModeType.AUTO) //커밋 또는 쿼리 실행시 플러시
em.setFlushMode(FlushModeType.COMMIT) //커밋시에만 플러시

FlushModeType.Auto가 기본값이다.

따라서 JPA 는 커밋 직전 혹은 커밋 실행 직전에 자동으로 플러시를 호출

FlushModeType.COMMIT

해당 옵션은 성능 최적화를 위해 꼭 필요할 때만 사용해야한다.

쿼리와 플러시 모드

JPQL은 영속성 컨텍스트의 데이터를 고려하지 않고 DB에 데이터를 조회한다.

따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 DB에 반영해야한다.

//가격 1000원을 2000원으로 변경
product.setPrice(2000);

//가격 2000원인 상품조회
Product product2 = em.createQuery("select p from Product p where p.price = 2000", Product.class).getSingleResult();

이 구문에서 플러시 모드를 따로 설정하지 않으면 플러시 모드가 Auto이므로 쿼리 실행 직전에 영속성 컨텍스트가 플러시 된다.

그러나 만약 이 상황에 플러시 모드를 COMMIT 으로 설정했으면 쿼리시에는 플러시 하지 않으므로 방금 수정한 데이터를 조회할 수 없다.

플러시 모드
em.setFlushMode(FlushModeType.COMMIT); //커밋 시에만 플러시

//1.em.flush() 직접호출

//가격 1000원을 2000원으로 변경
product.setPrice(2000);

//가격 2000원인 상품조회
Product product2 = em.createQuery("select p from Product p where p.price = 2000", Product.class).setFlushMode(FlushModeType.AUTO) //2.setFlushMode()설정
.getSingleResult();

첫줄에 플러시 모드를 COMMIT 으로 직접 설정했다.

때문에 쿼리 실행시 플러시를 자동으로 호출하지 않으며 em.flush()를 직접 호출하거나 setFlushMode() 로 해당 쿼리에서만 사용할 플러시 모드를 AUTO 로 변경하면 된다.

쿼리에 설정하는 플러시 모드는 엔티티 매니저에 설정하는 것보다 우선권을 갖는다.

왜 COMMIT 모드를 사용할까?

FlushModeType.COMMIT 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않는다.

이는 DB와 영속성 컨텍스트간의 데이터가 다른 데이터 무결성에 심각한 피해를 줄수 있다.

그러나 자주 일어나는 반복적인 상황에 이 모드를 사용하면 쿼리시 발생하는 플러시 횟수를 줄여 성능을 최적화 할 수 있다.

//비즈니스 로직
등록()
쿼리() //플러시
등록()
쿼리() //플러시
등록()
쿼리() //플러시
커밋() //플러시

FlushModeType.AUTO : 쿼리와 커밋할 때 총 4번 플러시

FlushModeType.COMMIT : 커밋 시에만 1번 플러시

JPA를 사용하지 않고 JDBC 를 이용하여 SQL을 실행할 때도 플러시모드를 고민해야한다.

JDBC로 쿼리를 직접 실행하면 JPA는 JDBC가 실행한 쿼리를 인식할 방법이 없다.

JDBC는 AUTO 모드를 설정해도 플러시가 일어나지 않는다.

이때는 JDBC로 쿼리를 실행하기 전에 em.flush를 호출해서 영속성 컨텍스트의 내용을 DB로 동기화한다.

728x90
반응형
LIST