- 참조: 자바 ORM 표준 JPA 프로그래밍
- 내부 조인(INNER JOIN)
JPQL 조인은 SQL 조인과 기능은 같지만 문법이 약간 다르다. Join 예제를 실행해보기 위해 실전예제에서 사용한 1 : N 관계인 Member 와 Order 엔티티를 사용해보자.
@Entity
public class Member extends DateMarkable{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
.............................
@Entity
@Table(name = "ORDERS")
public class Order extends DateMarkable{
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
위와 같이 정의한 후 1 명의 회원이 동일한 주소로 주문을 2 번 했다고 가정해보자. 코드를 간단히 작성하기 위해 OrderItem 엔티티까지는 작성하지 않았다.
Member member = new Member();
member.setName("User#1");
Address address1 = new Address("City#1", "Street#1", "Zipcode#1");
member.setAddress(address1);
em.persist(member);
Order order1 = new Order();
order1.setMember(member);
order1.setStatus(OrderStatus.ORDER);
Delivery delivery1 = new Delivery();
delivery1.setAddress(address1);
order1.setDelivery(delivery1);
em.persist(order1);
Order order2 = new Order();
order2.setMember(member);
order2.setStatus(OrderStatus.ORDER);
Delivery delivery2 = new Delivery();
delivery2.setAddress(address1);
order2.setDelivery(delivery2);
em.persist(order2);
JPQL 로 INNER JOIN 을 사용하여 조회하는 문법은 아래와 같다.
String jpql = "select o from Order o inner join o.member m where "
+ " m.name = :username";
List<Order> orders = em.createQuery(jpql, Order.class)
.setParameter("username", "User#1")
.getResultList();
orders.forEach(order -> {
Long orderId = order.getId();
Member member = order.getMember();
Delivery delivery = order.getDelivery();
System.out.println("orderId: " + orderId +
", member: " + member +
", delivery: " + delivery);
});
SQL 로는 from 절에 테이블이름을 기술하고 where 절에 join 에 참여할 속성들을 기술하거나 아니면 join 뒤에 테이블을 기술하고 on 절에 join 컬럼들을 긱술한다.
JPQL 은 테이블이 아니라 엔티티를 대상으로 한다고 했다. inner join 다음 테이블명이 아니라 o.member m 처럼 연관 필드와 alias 를 추가하여 join 을 한다.
위의 JPQL 을 실행할 때 변환된 SQL 은 아래와 같다.
Hibernate:
/* select
o
from
Order o inner join
o.member m
where
m.name = :username */ select
order0_.order_id as order_id1_6_,
order0_.insert_datetime as insert_d2_6_,
order0_.update_datetime as update_d3_6_,
order0_.delivery_id as delivery6_6_,
order0_.member_id as member_i7_6_,
order0_.order_date as order_da4_6_,
order0_.status as status5_6_
from
orders order0_
inner join
member member1_
on order0_.member_id=member1_.member_id
where
member1_.name=?
만약 Order 엔티티와 더불어 Member 엔티티도 조회하고 싶다면 select o, m 으로 조회해줄 수 있다. 물론 이 때에는 TypedQuery 를 사용할 수 없으므로 앞의 글에서 배운 프로젝션을 해줘야한다.
- 외부 조인(OUTER JOIN)
외부 조인은 내부 조인과 크게 다르지는 않다. 내부 조인에서 테이블 대신 엔티티를 대상으로 JPQL 을 작성해주었다는 점만 잘 기억하면 SQL 에서 우리가 보았던 LEFT OUTER JOIN 을 이용하면 된다.
String jpql = "select o from Order o left outer join o.member m where "
+ " m.name = :username";
위의 JPQL 을 수행하면 아래와 같은 SQL 로 변환된다.
Hibernate:
/* select
o
from
Order o left outer join
o.member m
where
m.name = :username */ select
order0_.order_id as order_id1_6_,
order0_.insert_datetime as insert_d2_6_,
order0_.update_datetime as update_d3_6_,
order0_.delivery_id as delivery6_6_,
order0_.member_id as member_i7_6_,
order0_.order_date as order_da4_6_,
order0_.status as status5_6_
from
orders order0_
left outer join
member member1_
on order0_.member_id=member1_.member_id
where
member1_.name=?
- 컬렉션 조인
컬렉션 조인은 내부 조인이나 외부 조인과 크게 다른 개념은 아니다. 위에서 Order 엔티티와 Member 엔티티는 N : 1 다중성을 지니므로 단일 값 연관 필드를 사용한다.
하지만 Member 엔티티를 기준으로 Order 엔티티를 조인할 수도 있다. 이때에는 1 : N 조인을 하는데 컬렉션 값 연관 필드를 사용하므로 컬렉션을 사용한다고 하여 컬렉션 조인이라고 부를 뿐이다.
String jpql = "select o from Member m left outer join m.orders o where "
+ " m.name = :username";
위의 JPQL 에서는 Member 엔티티가 주문한 Order 엔티티들을 List 로 참조하는 필드명인 orders 로 join 을 하였다. 변환된 SQL 은 아래와 같다.
Hibernate:
/* select
o
from
Member m
left outer join
m.orders o
where
m.name = :username */ select
orders1_.order_id as order_id1_6_,
orders1_.insert_datetime as insert_d2_6_,
orders1_.update_datetime as update_d3_6_,
orders1_.delivery_id as delivery6_6_,
orders1_.member_id as member_i7_6_,
orders1_.order_date as order_da4_6_,
orders1_.status as status5_6_
from
member member0_
left outer join
orders orders1_
on member0_.member_id=orders1_.member_id
where
member0_.name=?
- 세타 조인(THETA JOIN)
JPQL 은 WHERE 절을 사용해서 세타 조인을 지원한다. 책에서는 "세타 조인은 내부 조인만 지원한다." 라고 되어 있다. 사실 원래 세타 조인의 정의는 = 뿐만 아니라 <, > 등 다른 연산자로도 조건을 세울 수 있다. 이 연산자들 중 = 연산자를 사용하는 세타조인을 특별히 동등 조인이라고 한다. 즉 더 좁은 범위로 말하면 JPQL 은 세타 조인을 지원하는데 그 중 = 연산자를 사용하는 동등조인만 지원한다고 할 수 있다.
Member member = new Member();
member.setName("User#1");
em.persist(member);
Book book1 = new Book();
book1.setAuthor("User#1");
book1.setName("Book#1");
em.persist(book1);
Book book2 = new Book();
book2.setAuthor("User#1");
book2.setName("Book#2");
em.persist(book2);
위와 같이 Member 와 Book 엔티티를 각각 영속 시킨다. 각 책들의 저자를 User#1 이라고 설정해주었는데 데이터는 연관이 있지만 두 엔티티는 서로 직접 참조하고 있지는 않다.
String jpql = "select b from Member m, Book b where "
+ " m.name = b.author and m.name = :username";
List<Book> books = em.createQuery(jpql, Book.class)
.setParameter("username", "User#1")
.getResultList();
books.forEach(book -> {
String name = book.getName();
String author = book.getAuthor();
System.out.println("name: " + name +
", author: " + author);
});
앞에서 살펴보았던 JPQL 들은 연관된 엔티티들의 참조 필드명을 이용해서 Join 을 했다. Book 과 Member 처럼 아무런 참조 연관성이 없는 엔티티들도 특정 컬럼으로 조인을 할 수 있다.
위의 JPQL 을 수행하면 아래와 같이 SQL 을 수행한다.
Hibernate:
/* select
b
from
Member m,
Book b
where
m.name = b.author
and m.name = :username */ select
book1_.item_id as item_id2_3_,
book1_.insert_datetime as insert_d3_3_,
book1_.update_datetime as update_d4_3_,
book1_.name as name5_3_,
book1_.price as price6_3_,
book1_.stock_quantity as stock_qu7_3_,
book1_.author as author9_3_,
book1_.isbn as isbn10_3_
from
member member0_ cross
join
item book1_
where
book1_.item_type='BOOK'
and member0_.name=book1_.author
and member0_.name=?
name: Book#1, author: User#1
name: Book#2, author: User#1
- JOIN ON
실제 프로젝트를 수행하면 프로젝트 마다 SQL 작성 기준이 있다. 어떤 프로젝트는 데이터베이스 제품군을 2 개를 사용하여 ANSI 조인을 지켜야 하기도 하며, (+) 조인을 쓰는사람, LEFT 및 INNER 조인 과 함께 ON 절을 명시하는 사람도 있다. 심지어는 표준을 정해도 자기 마음대로 하는 개발자들도 존재한다.
개인적으로는 INNER, LEFT JOIN 을 명시하고 ON 절을 쓰는것을 선호하는 편이다. SQL 이 길어지는 단점도 있지만 처음보는 사람도 테이블간의 JOIN 관계를 명확하게 인지할 수 있기 때문에 좋다고 생각한다.
JPA 2.1 부터는 ON 절을 지원한다. 아래 예제를 살펴보자.
String jpql = "select o,m "
+ "from Order o "
+ "left outer join o.member m "
+ " on m.name = :username";
List<Object[]> orderAndMember = em.createQuery(jpql)
.setParameter("username", "User#3")
.getResultList();
orderAndMember.forEach(orderAndMemberObjs -> {
Order order = (Order) orderAndMemberObjs[0];
Member member = (Member) orderAndMemberObjs[1];
System.out.println("orderId: " + order.getId() +
", member: " + member);
});
책에서는 SQL 의 기본 사용법을 안다는 가정하에 자세하게 설명해주지는 않고 있지만 만약 이글을 보고 있는 독자가 SQL 초보라면 ON 절 문법을 사용할 수 있다라는 것보다 LEFT OUTER JOIN 에서 ON 절에 조건을 주는것과 WHERE 절에 조건을 주는것의 차이를 아는것이 더 중요하다고 생각한다.
Uesr#1 인 멤버와 주문 2 건을 영속시켰다고 가정하고 위의 JPQL 을 수행했을 때, "Order 가 2 건이 조회되지만 멤버정보는 null 이 나오겠네." 라고 예측할 수 있다면 위에서 언급한 ON 과 WHERE 절 조건의 차이를 알고 있는 사람이다. 만약 "데이터가 한건도 안나오겠네" 라는 생각이 든다면 SQL 의 ON 과 WHERE 절에 대한 차이를 알 필요가 있다.
JPQL 을 실제로 수행하면 변환되는 SQL 과 결과는 아래와 같다. on 절을 where 절로 바꿔서도 수행하면 데이터가 나오지 않는다. 한번 실행해보길 바란다.
Hibernate:
/* select
o,
m
from
Order o left outer join
o.member m
on m.name = :username */ select
order0_.order_id as order_id1_6_0_,
member1_.member_id as member_i1_4_1_,
order0_.insert_datetime as insert_d2_6_0_,
order0_.update_datetime as update_d3_6_0_,
order0_.delivery_id as delivery6_6_0_,
order0_.member_id as member_i7_6_0_,
order0_.order_date as order_da4_6_0_,
order0_.status as status5_6_0_,
member1_.insert_datetime as insert_d2_4_1_,
member1_.update_datetime as update_d3_4_1_,
member1_.city as city4_4_1_,
member1_.street as street5_4_1_,
member1_.zipcode as zipcode6_4_1_,
member1_.name as name7_4_1_
from
orders order0_
left outer join
member member1_
on order0_.member_id=member1_.member_id
and (
member1_.name=?
)
orderId: 2, member: null
orderId: 4, member: null
'Framework and Tool > JPA' 카테고리의 다른 글
JPA - 객체지향 쿼리 언어 - JPQL 경로 표현식 (0) | 2021.08.02 |
---|---|
JPA - 객체지향 쿼리 언어 - JPQL Fetch Join (0) | 2021.07.31 |
JPA - 객체지향 쿼리 언어 - JPQL Paging, 집합, 정렬 (0) | 2021.07.29 |
JPA - 객체지향 쿼리 언어 - JPQL 기본 (0) | 2021.07.29 |
JPA - 객체지향 쿼리 언어 - 개요 (0) | 2021.07.28 |
댓글