본문 바로가기
Framework and Tool/JPA

JPA - 객체지향 쿼리 언어 - JPQL Join

by ocwokocw 2021. 7. 31.

- 참조: 자바 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

댓글