[JPA]/JPA 프로그래밍 - 기본편

[JPA] 객체지향 쿼리 언어2 - 중급 문법

쿠릉쿠릉 쾅쾅 2022. 4. 3. 16:36
728x90

 

 

경로 표현식

  • 결론 : 묵시적 내부 조인(inner join)이 일어나므로 단일값 연관 필드 / 컬렉션 값 연관 필드 경로 표현식을 쓰지 말 것.
    • 실무에서 묵시적 내부 조인은 절대 쓰지 말아야 한다.
  • 경로 표현식은 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
  • 경로 표현식에는 상태 필드 / 단일 값 연관 필드 / 컬렉션 값 연관 필드 3가지가 있다.
select m.username          // 상태 필드
    from Member m
        join m.team t      // 단일 값 연관 필드
        join m.orders o    // 컬렉션 값 연관 필드
    where t.name = '팀A'
  • m.username / m.team / t.name / m.orders 모두 경로 표현식이다.
  • 상태 필드 : 단순히 값을 저장하기 위한 필드다. 일반적인 자바 기본 타입의 컬럼들을 말한다.
    • 예) m.useranme / t.name
    • 더 이상 경로 탐색이 되지 않는다.
  • 연관 필드 : 연관 관계를 위한 필드, 임베디드 타입
    •  단일 값 연관 관계 필드 : 대상이 엔티티인 것을 말한다. (@ManyToOne / @OneToOne)
      • 예) m.team
      • 묵시적으로 내부 조인(inner join)이 일어난다.
      • 계속 경로 탐색할 수 있다.
        • ex) select m.team.name from Member m
      • 임베디드 타입도 단일 값 연관 필드지만 연관관계가 없으므로 조인이 일어나지 않는다.
    • 컬렉션 값 연관 필드 : 대상이 컬렉션인 것을 말한다. (@OneToMany / @ManyToMany)
      • 예) m.order
      • 묵시적으로 내부 조인(inner join)이 일어난다.
      • 기본적으로 경로 탐색을 할 수 없으나, from절에서 별칭을 얻으면 별칭으로 탐색할 수 있다.

 


 

명시적 조인 / 묵시적 조인

  • 명시적 조인 : join 키워드 직접 사용
    • select m from Member m join m.team t
  • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 쿼리 발생
    • 내부 조인만 가능하다.
    • select m.team from Member m

조인은 SQL 튜닝에 중요한 포인트이기 때문에 묵시적 조인을 아예 쓰지 말아야 한다. 항상 명시적 조인을 사용해야 한다.

 


 

페치 조인

  • SQL에서 지원하는 조인 문법이 아니다.
  • JPQL에서 지원하는 성능 최적화를 위해 제공하는 기능이다.
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다.
  • join fetch 명령어를 사용한다.
    • [LEFT [OUTER] | INNER] JOIN FETCH 조인_경로 
  • 실무에서 엄청 많이 사용한다.

 

1. 엔티티 페치 조인

/* JPQL 쿼리 */
select m from Member inner join fetch m.team


/* SQL 쿼리 */
SELECT M.*, T.* from MEMBER M  INNER JOIN TEAM T ON M.TEAM_ID = T.ID
  • 페치 조인 사용시 연관된 모든 엔티티의 값(m.*, t.*)을 가져온다.
  • 기존의 inner join에서 값을 Object[]로 받아야하는 것과 달리 Mebmber 내부에 team 변수의 값이 다 채워진 상태로 Member 타입으로 받을 수 있다.
  • 그래서 성능 최적화를 위해 제공되는 기능이라고 한다.
  • 즉, 객체 그래프를 그대로 유지하면서 값을 받을 수 있다.

 

🧷 참고) 객체 그래프 탐색

더보기

객체 그래프 탐색

 

  •  객체에서 회원(Member)이 소속된 팀(Team)을 조회할 때는 참조를 사용해서 연관된 팀을 찾으면 된다. 이것을 객체 그래프 탐색이라고 한다.
    • ex) Team team = member.getTeam();

SQL에서는 방향이 없어서 양쪽 다 참조가 가능하지만 객체지향은 참조 방향이 단방향이다. 그러므로 SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래픔 탐색 범위가 정해진다. 비즈니스 로직에 따라 사용하는 객체 그래프가 언제 끊어질지 알 수 없는 객체를 함부로 탐색할 수 없다.

 

JPA를 사용하면 객체 그래프 탐색 문제 해결 가능

JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다. JPA는 연관된 객체를 사용하는 시점에 적절한 SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 조회할 수 있다. (지연 로딩 기능)

지연 로딩 기능 : 실제 객체를 사용하는 시점에 데이터 베이스 조회를 한다. 그전까지는 조회를 미룬다.

 

 

🔍 페치 조인 사용 전

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

String query = "select m from Member m";

List<Member> result = em.createQuery(query, Member.class)
        .getResultList();
for (Member member : result) {
    System.out.println("member.getUsername() = " + member.getUsername() +
            ", " + member.getTeam().getName());
}
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_1_,
            member0_.age as age2_1_,
            member0_.TEAM_ID as TEAM_ID5_1_,
            member0_.type as type3_1_,
            member0_.username as username4_1_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
member.getUsername() = 회원1, 팀A
member.getUsername() = 회원2, 팀A
Hibernate: 
    select
        team0_.id as id1_4_0_,
        team0_.name as name2_4_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
member.getUsername() = 회원3, 팀B
  • n+1 문제 발생
  • 처음 Member 엔티티를 조회할 때 Team 정보는 프록시 객체로 되어 있다.
  • 그러므로 프록시 객체를 조회할 때 여러 줄의 쿼리문이 나가게 되어 n+1 쿼리 개수 문제 발생

🔍페치 조인 사용 후 

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

String query = "select m from Member m join fetch m.team";  // 페치 조인 사용

List<Member> result = em.createQuery(query, Member.class)
        .getResultList();
for (Member member : result) {
    System.out.println("member.getUsername() = " + member.getUsername() +
            ", " + member.getTeam().getName());
}
Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.id as id1_1_0_,
            team1_.id as id1_4_1_,
            member0_.age as age2_1_0_,
            member0_.TEAM_ID as TEAM_ID5_1_0_,
            member0_.type as type3_1_0_,
            member0_.username as username4_1_0_,
            team1_.name as name2_4_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id
member.getUsername() = 회원1, 팀A
member.getUsername() = 회원2, 팀A
member.getUsername() = 회원3, 팀B
  • fetch join 쿼리를 사용하여 n+1 문제 해결
  • fetch join을 사용하여 처음 Member 엔티티를 조회할 때 Team 엔티티를 프록시 객체가 아닌 실제 엔티티로 조회 했다.
  • 그래서 Member 엔티티에서 Team 엔티티를 접근해도 쿼리가 여러개가 생기지 않는다.
  • fetch join을 통해 지연로딩 기능을 즉시 로딩 기능으로 바꾼 것이다.

 

2. 컬렉션 페치 조인

일대다 관계에서 페치조인을 사용할 수 있다.

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

String query = "select t from Team t join fetch t.members";  // 페치 조인 사용

List<Team> result = em.createQuery(query, Team.class)
        .getResultList();

for (Team team : result) {
    System.out.println("team = " + team.getName() +
            " | members=" + team.getMembers().size());

    for (Member member : team.getMembers()) {
        System.out.println("-> member = " + member);

    }

}
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch t.members */ select
            team0_.id as id1_4_0_,
            members1_.id as id1_1_1_,
            team0_.name as name2_4_0_,
            members1_.age as age2_1_1_,
            members1_.TEAM_ID as TEAM_ID5_1_1_,
            members1_.type as type3_1_1_,
            members1_.username as username4_1_1_,
            members1_.TEAM_ID as TEAM_ID5_1_0__,
            members1_.id as id1_1_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
team = 팀A | members=2
-> member = jpql.Member@46c00568
-> member = jpql.Member@f9b5552
team = 팀A | members=2
-> member = jpql.Member@46c00568
-> member = jpql.Member@f9b5552
team = 팀B | members=1
-> member = jpql.Member@6d2d99fc

출력결과를 보면 team = 팀A | members=2 부분이 중복으로 2번 출력됐다. Member 엔티티를 모두 조회할 때 member1과 member2의 팀이 서로 팀A으로 같은팀이다. 그래서 같은 결과가 두 번 나왔다. 이처럼 일대다 관계에서 조회할 때는 데이터 개수가 뻥튀기 될 수 있다.

select from Team t join fetch t.members where t.name = '팀A'
  • 이것을 수행하면 Team은 하나지만 Member가 1개 이상일 수 있다.
  • 그래서 중복 결과가 나오면서 데이터 개수가 늘어날 수 있다.

  • 팀A는 1개지만 그에 해당하는 멤버는 회원1과 회원2로 두개이기 때문에조회 결과는 위 표 처럼 2개의 row가 된다.
  • 즉, 팀의 개수가 멤버의 개수와 동일할 수 있다.
  • 이러한 중복 출력은 distinct 명령어로 제거할 수 있다.

 

3. 페치 조인과 DISTINCT

  • JPQL의 DISTINCT 명령어는 SQL의 DISTINCT 기능에다가 어플리케이션에서 한번 더 중복을 제거한다.
  • 이 특징을 이용해서 컬렉션 페치 조인에서 리스트가 중복되서 나오는 문제를 해결할 수 있다.
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

// distinct 사용하여 중복 결과 제거
String query = "select distinct t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
        .getResultList();

for (Team team : result) {
    System.out.println("team = " + team.getName() +
            " | members=" + team.getMembers().size());

    for (Member member : team.getMembers()) {
        System.out.println("-> member = " + member);

    }

}
Hibernate: 
    /* select
        distinct t 
    from
        Team t 
    join
        fetch t.members */ select
            distinct team0_.id as id1_4_0_,
            members1_.id as id1_1_1_,
            team0_.name as name2_4_0_,
            members1_.age as age2_1_1_,
            members1_.TEAM_ID as TEAM_ID5_1_1_,
            members1_.type as type3_1_1_,
            members1_.username as username4_1_1_,
            members1_.TEAM_ID as TEAM_ID5_1_0__,
            members1_.id as id1_1_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
team = 팀A | members=2
-> member = jpql.Member@56ccd751
-> member = jpql.Member@6872f9c8
team = 팀B | members=1
-> member = jpql.Member@bdecc21
  • distinct 명령어를 통해 중복된 결과를 제거했다.

 

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

🔍 일반 조인

  • 연관 관계를 고려하지 않고 select 절에 지정한 엔티티만 조회한다.
  • 연관된 엔티티에 대해서는 프록시나 컬렉션으로 반환한다.
  • 지연 로딩 기능 사용.
  • n+1 문제 발생

🔍 페치 조인

  • 연관 관계의 모든 엔티티를 한꺼번에 조회한다.
  • 즉시 로딩 기능 사용.
  • n+1 문제 해결
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.

일반 조인 사용

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

String query = "select t from Team t join t.members m";  // 페치 조인 사용

List<Team> result = em.createQuery(query, Team.class)
        .getResultList();

for (Team team : result) {
    System.out.println("team = " + team.getName() +
            " | members=" + team.getMembers().size());

    for (Member member : team.getMembers()) {
        System.out.println("-> member = " + member);

    }

}
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        t.members m */ select
            team0_.id as id1_4_,
            team0_.name as name2_4_ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_1_0_,
        members0_.id as id1_1_0_,
        members0_.id as id1_1_1_,
        members0_.age as age2_1_1_,
        members0_.TEAM_ID as TEAM_ID5_1_1_,
        members0_.type as type3_1_1_,
        members0_.username as username4_1_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀A | members=2
-> member = jpql.Member@30cecdca
-> member = jpql.Member@37c5fc56
team = 팀A | members=2
-> member = jpql.Member@30cecdca
-> member = jpql.Member@37c5fc56
Hibernate: 
    select
        members0_.TEAM_ID as TEAM_ID5_1_0_,
        members0_.id as id1_1_0_,
        members0_.id as id1_1_1_,
        members0_.age as age2_1_1_,
        members0_.TEAM_ID as TEAM_ID5_1_1_,
        members0_.type as type3_1_1_,
        members0_.username as username4_1_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = 팀B | members=1
-> member = jpql.Member@7051777c
  • 그냥 조인을 사용하면 select 절에서 Team 엔티티만 조회한다.

페치 조인 사용

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

String query = "select t from Team t join fetch t.members";  // 페치 조인 사용

List<Team> result = em.createQuery(query, Team.class)
        .getResultList();

for (Team team : result) {
    System.out.println("team = " + team.getName() +
            " | members=" + team.getMembers().size());

    for (Member member : team.getMembers()) {
        System.out.println("-> member = " + member);

    }

}
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch t.members */ select
            team0_.id as id1_4_0_,
            members1_.id as id1_1_1_,
            team0_.name as name2_4_0_,
            members1_.age as age2_1_1_,
            members1_.TEAM_ID as TEAM_ID5_1_1_,
            members1_.type as type3_1_1_,
            members1_.username as username4_1_1_,
            members1_.TEAM_ID as TEAM_ID5_1_0__,
            members1_.id as id1_1_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
team = 팀A | members=2
-> member = jpql.Member@46c00568
-> member = jpql.Member@f9b5552
team = 팀A | members=2
-> member = jpql.Member@46c00568
-> member = jpql.Member@f9b5552
team = 팀B | members=1
-> member = jpql.Member@6d2d99fc
  • select절을 보면 Team, Member 엔티티를 모두 조회했음을 알 수 있다.

 


 

5. 페치 조인의 한계

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

🔍 패치 조인 대상에는 별칭을 줄 수 없다.

  • 연관된 엔티티를 모두 조회하기 때문에 별칭을 사용할 수 없다.
  • 하이버네이트에서는 사용 가능하나, 가급적 사용하지 말 것
  • fetch join 대상은 on / where 등에서 필터링 조건으로 사용하면 안된다.
  • 별칭을 준 후 on절에서 별칭으로 조건을 주면 OneToMany 관계에서 Collection 형태로 조회 되는 데이터가 전부 조회되지 않고 일부만 나오기 때문에 문제가 생길 수 있다.  

🔍 둘 이상의 컬렉션은 페치 조인을 할 수 없다.

  • 컬렉션 * 컬렉션의 카테시안 곱이 만들어지므로 주의해야 한다.
  • 하이버네이트를 사용할 경우 MultipleBagfetchException이 발생한다.

🔍 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다.

  • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인을해도 페이징 기능을 쓸 수 있다.
  • 일대다에서는 데이터 개수가 뻥튀기가 되므로 페이징 기능을 사용할 수 없다.
    • 일대다 같은 경우 SQL을 다대일로 접근해서 페이징 API를 사용한다.
  • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징한다. (매우 위험한 방식)
    • 데이터 개수가 100만건 일경우 100만건이 모두 메모리에 올라가고 페이징하는 방식이기 때문이다.
    • 성능 이슈가 발생한다.

💡 해결 방안

✔ 일대다를 대다일로 방향을 전환해서 해결한다.

String query = "select t from Team t";

▼ 다대일로 반향 전환

String query = "select m from Member m join fetch m.team t";

✔ BatchSize()

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @BatchSize(size = 100)
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}
  • 지연로딩 상태이지만 조회할 때 members를 BatchSize의 size 만큼 조회한다.

BatchSize()는 글로벌 설정으로 할 수 있다.

/* persistence.xml */

<property name="hibernate.default_batch_fetch_size" value="100" />

글로벌 설정으로 하는것이 편하다.

  •  

 

6. 페치 조인 정리

  • 모든 것을 페치 조인으로 해결할 수 없다.
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
  • 여러 테이블을 조인해서 엔티티가 아닌 전혀 다른 결과를 내야하면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터를 조회해서 DTO로 반환하는 것이 효과적이다.

 


 

다형성 쿼리

  • 상속관계(@Inheritance)로 구성된 엔티티를 JPA에서 조회하면 그 자식 엔티티도 같이 조회할 수 있다.

 

1. TYPE

  • 상속 구조에서 조회 대상을 특정 자식으로 한정할 때 사용한다.
    • 예) Item 중에서 Book, Movie 엔티티를 조회해라
/* JPQL */
select i from Item i where type(i) in (Book, Movie)


/* SQL */
select i from i where i.DTPE in ('B', 'M')

 

2. TREAT

  • 상속 구조에서 부모 타입을 특정 타입으로 다룰 때 사용한다.
  • JPA 표준은 from / where 절만 사용 가능하다.
  • 하이버네이트에서는 select 절에도 가능하다.
/* JPQL */
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'

Item을 자식 타입인 Book으로 다뤘다. (다운 캐스팅)
그래서 Book 필드인 author에 접근할 수 있다.

 


 

엔티티 직접 사용

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
/* JPQL */
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용


/* SQL */ 
select count(m.id) as cnt from Member m

 

1. 기본 키 값

String query = "select m from Member m where m = :member";
Member findMember = em.createQuery(query, Member.class)
        .setParameter("member", member1)
        .getSingleResult();
Hibernate: 
    /* select
        m 
    from
        Member m 
    where
        m = :member */ select
            member0_.id as id1_1_,
            member0_.age as age2_1_,
            member0_.TEAM_ID as TEAM_ID5_1_,
            member0_.type as type3_1_,
            member0_.username as username4_1_ 
        from
            Member member0_ 
        where
            member0_.id=?
  • member1 엔티티가 sql에서  member1.id 기본 키 값으로 쓰인다.
  • member가 영속성 컨텍스트에 없어도 된다. 식별자만 가지고 있으면 된다.

 

2. 외래 키 사용

String query = "select m from Member m where m.team = :team";
List<Member> members = em.createQuery(query, Member.class)
        .setParameter("team", teamA)
        .getResultList();
Hibernate: 
    /* select
        m 
    from
        Member m 
    where
        m.team = :team */ select
            member0_.id as id1_1_,
            member0_.age as age2_1_,
            member0_.TEAM_ID as TEAM_ID5_1_,
            member0_.type as type3_1_,
            member0_.username as username4_1_ 
        from
            Member member0_ 
        where
            member0_.TEAM_ID=?
  • 기본키와 로직이 동일하다.

 


 

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();

실무에서 Named쿼리를 사용할 때 지금 형태가 아닌 SpringDateJPA에서 지원하는 Named쿼리를 사용한다.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    @Query("select u from User u where u.emailAddress =?1")
    User findByEmailAddress(String emailAddress);
   
}

JPA의 Named 쿼리가 스프링 데이터 JPA에서 @Query 애노테이션으로 쓰인다.

 

🧷 참고 

  • em.createQuery("select ...") 처럼 JPQL을 직접 문자로 넘기는 것을 동적 쿼리라고 한다.
  • Named 쿼리를 정적 쿼리라고 한다.

 


 

벌크 연산

  • JPQL로 여러 건을 한 번에 수정하거나 삭제할 때 사용한다.
    • 예) 재고 10개 미만인 모든 상품의 가격을 10% 상승할 때
    • JPA 변경 감지 기능으로 실행하려면 SQL 쿼리 문을 너무 많이 사용해야한다.

1. 벌크 연산

🔍 update 벌크 연산

ex.1

String sql = "UPDATE Product p " +
    "SET p.prce = p.price * 1.1 " +
    "WHERE p.stockAmount < :stockAmount";

int resultCount = em.createQuery(sql)
        .setParameter("stockAmount", 10)
        .executeUpdate();
  • executeUpdate() : 벌크 연산으로 영향을 받은 엔티티 개수를 반환한다.

ex.2

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

em.flush();
em.clear();

// 벌크 연산 실행전 em.flush() 자동 호출
int resultCount = em.createQuery("update Member m set m.age = 20")
        .executeUpdate();

em.clear();

  • 벌크 연산하기 전에 미리 em.flush()가 자동으로 실행된다.

🔍 delete 벌크 연산

String sql = "DELETE FROM Product p " +
    "WHERE p.price < :price";

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

 

2. 벌크 연산시 주의사항

  • 벌크연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 특징이 있어서 주의해야 한다.
  • 그래서 영속성 컨텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있다.

🔍 해결 방안

💡 em.refresh(entity) 사용

  • 벌크 연산 직후에 em.refresh() 를 사용하여 데이터베이스에 다시 상품 조회를 하면 된다.

💡 벌크 연산 먼저 실행

  • 벌크 연산을 가장 먼저 실행하면 이미 변경된 내용을 데이터베이스에서 가져온다.
  • 가장 실용적인 해결책이다.

💡 벌크 연산 수행 후 영속성 컨텍스트 초기화

  • 벌크 연산 후에 무조건 em.clear() 메서드를 호출하여 영속성 컨텍스트를 초기화 시킨다.
  • 영속성 컨텍스트가 초기화되면 데이터베이스에서 다시 조회한다.

 

 

 

 

 


👀 참고 자료

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

https://joont92.github.io/jpa/JPQL/

 

[jpa] JPQL

JPA에서 현재까지 사용했던 검색은 아래와 같다. 식별자로 조회 EntityManager.find() 객체 그래프 탐색 e.g. a.getB().getC() 하지만 현실적으로 이 기능만으로 어플리케이션을 개발하기에는 무리이다. 그

joont92.github.io

 

https://bros.tistory.com/m/16

 

1.3.3 객체 그래프 탐색

1. 설명 객체에서 회원(Member)이 소속된 팀을 조회할 때는 다음처럼 참조를 사용해서 연관된 팀을 찾으면 되는데, 이것을 객체 그래프 탐색이라 한다. Team team = member.getTeam(); member.getOrder().getOrd..

bros.tistory.com

 

728x90