[데이터베이스] - QueryDSL 사용 이유와 방법 ①
QueryDSL 사용 이유와 방법 ①
실무에서 개발자를 하기위해 공부를 하던 시절에는 데이터베이스 라는 것에 대해 깊이 생각하지 않았다. 컴퓨터 프로그래밍 기술이 계속적으로 발전 하고 개발자가 좀 더 편하게 개발할 수 있
eno1993.tistory.com
일반적으로 QueryDSL을 왜 사용하는지 그리고 가장 간단한 조회에 대해서 살펴봤다.
그럼 좀 더 복잡한 조회 와 업데이트 그리고 삭제 방법에 대해서 살펴보도록 하자.
테이블 정의
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username")
private String username;
@Column(name = "age")
private Integer age;
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "order_date")
private LocalDateTime orderDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
}
@Entity
@Table(name = "team")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Member> members = new ArrayList<>();
}
@Entity
@Table(name = "order_item")
public class OrderItem {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne
@JoinColumn(name = "item_id")
private Item item;
private int orderPrice;
private int count;
}
각각의 엔티티를 위와 같이 정의하면 아래와 같은 ER 다이어그램을 통해 연관 관계를 표현 할 수 있다.
조회
여러개의 테이블 JOIN, Projections.constructor
원하는 클래스로 변형해서 얻어오기 Projections.constructor
QMember member = QMember.member;
QOrder order = QOrder.order;
QTeam team = QTeam.team;
LocalDateTime targetDate = LocalDateTime.of(2022, 2, 1, 0, 0, 0);
List<OrderMember> result = queryFactory
.select(Projections.constructor(OrderMember.class,
member.id, member.username, member.age, order.id, order.orderDate, team.name))
.from(member)
.join(member.orders, order) // innerJoin
.join(member.team, team) // innerJoin
.on(order.orderDate.goe(targetDate))
.fetch();
위 예제는 QueryDSL을 통하여 일정 시간 이후의 주문 건에 대해서 특정 팀에 속해있는 멤버를 확인하고 OrderMember 라는 DTO로 조회하는 방법이다.
Select 절에 Projections 클래스를 사용하여 쿼리 결과를 특정 DTO로 치환해서 결과 값을 받을 수 있도록 했다. join 을 사용하여 order와 team 엔티티를 조인했지만 명시적으로 innerJoin을 적어주는것이 더 나은 방법이다. 물론 join만 사용하면 기본적으로 innerJoin이 호출된다. 또 두 테이블의 join이 완료 된 후 on절을 통해서 order의 조건을 수행했지만 order 테이블이 join된 이후 on절을 통해 테이블의 결과 값을 줄인 상태에서 team 을 join 하는 것이 성능적으로 더 나은 결과를 가져올 수 있다. (확실한 보장은 할 수 없다.)
order join → team join → on 조건절 수행 ▶ order join → on 조건절 수행 → team join
서브 쿼리, JPAExpressions
JPAExpressions 로 서브 쿼리 사용.
일반적으로 서브 쿼리를 사용할 상황은 IN, NOT IN / EXISTS, NOT EXISTS 절을 이용한 필터링 상황이나 스칼라 서브 쿼리 혹은 특정 테이블과 조인하는 경우에 사용이 된다.
QOrder order = QOrder.order;
QOrderItem orderItem = QOrderItem.orderItem;
List<OrderItem> items = queryFactory
.selectFrom(orderItem)
.where(orderItem.order.in(
JPAExpressions
.select(order)
.from(order)
.where(order.orderDate.between(startDate, endDate))
))
.orderBy(orderItem.quantity.desc())
.limit(3)
.fetch();
위 코드는 WHERE 조건절에서 IN 이용하여 서브쿼리를 넣어서 특정 기간의 주문에 해당하는 orderItem 들을 모두 조회하는 예제이다. JPAExpressions 로 위와 같은 서브쿼리를 생성 할 도 있지만 Join을 하는 과정에서 on 조건에 서브쿼리를 넣어서 조건을 달 수도 있다.
QMember member = QMember.member;
QOrder order = QOrder.order;
List<Tuple> result = queryFactory
.select(member.username, order.id)
.from(member)
.innerJoin(member.orders, order)
.on(order.price.eq(
JPAExpressions.select(order2.price.max())
.from(order2)
))
.fetch();
그룹핑, GroupBy Having 절
GroupBy 와 Having으로 그룹별 처리 하기.
여러개의 중복 컬럼 처리의 경우 GroupBy와 Having 절을 통해 원하는 형태로 값을 조회 할 수 있다.
QMember member = QMember.member;
QOrder order = QOrder.order;
QOrderItem orderItem = QOrderItem.orderItem;
List<OrderDto> result = queryFactory
.select(Projections.fields(OrderDto.class,
member.id,
member.username,
order.count(),
order.sum(orderItem.price.multiply(orderItem.count))))
.from(order)
.join(order.member, member)
.join(order.orderItems, orderItem)
.groupBy(member.id)
.having(3.lessThan(order.count()))
.fetch();
업데이트, JPAUpdateClause
JPAUpdateClause 로 업데이트 진행.
하나의 엔티티만 독립적으로 업데이트 할 경우에는 매우 간단하게 사용 할 수 있지만 업데이트 시에는 해당 엔티티에 조인된 엔티티를 직접 참조 할 수 없기 때문에 join을 활용하여 업데이트를 진행해야 한다.
QMember qMember = QMember.member;
QOrder qOrder = QOrder.order;
QOrderItem qOrderItem = QOrderItem.orderItem;
Long memberId = 1L; // 수정할 회원의 id
JPAUpdateClause updateClause = queryFactory.set(qOrderItem.orderPrice, 5000)
.where(qOrderItem.order.id.in(JPAExpressions.select(qOrder.id)
.from(qOrder)
.innerJoin(qOrder.member, qMember)
.where(qMember.id.eq(memberId)))
.and(qOrderItem.item.id.in(JPAExpressions.select(qOrderItem.item.id)
.from(qOrderItem)
.innerJoin(qOrderItem.order, qOrder)
.innerJoin(qOrder.member, qMember)
.where(qMember.id.eq(memberId)))));
updateClause.execute();
삭제, JPADeleteClause
JPADeleteClause로 delete 진행.
QMember qMember = QMember.member;
QOrder qOrder = QOrder.order;
QOrderItem qOrderItem = QOrderItem.orderItem;
Long memberId = 1L; // 삭제할 회원의 id
JPADeleteClause deleteClause = queryFactory.where(qOrderItem.order.id.in(JPAExpressions.select(qOrder.id)
.from(qOrder)
.innerJoin(qOrder.member, qMember)
.where(qMember.id.eq(memberId)))
.and(qOrderItem.item.id.in(JPAExpressions.select(qOrderItem.item.id)
.from(qOrderItem)
.innerJoin(qOrderItem.order, qOrder)
.innerJoin(qOrder.member, qMember)
.where(qMember.id.eq(memberId)))));
deleteClause.execute();
일반적으로 JPADeleteClause 로 해결하지만 JPAUdateClause 를 이용해서 삭제를 구현 할 수도 있다.
QMember qMember = QMember.member;
QOrder qOrder = QOrder.order;
QOrderItem qOrderItem = QOrderItem.orderItem;
Long memberId = 1L; // 삭제할 회원의 id
JPAUpdateClause updateCluase = queryFactory.where(qOrderItem.order.id.in(JPAExpressions.select(qOrder.id)
.from(qOrder)
.innerJoin(qOrder.member, qMember)
.where(qMember.id.eq(memberId)))
.and(qOrderItem.item.id.in(JPAExpressions.select(qOrderItem.item.id)
.from(qOrderItem)
.innerJoin(qOrderItem.order, qOrder)
.innerJoin(qOrder.member, qMember)
.where(qMember.id.eq(memberId)))))
.delete(qOrderItem);
updateClause.execute();
정리
실제로 DB에 접근해서 상황에 따라 내가 원하는 값을 가져오는 과정이 매우 복잡 할 수도 있다. 내가 본 가장 복잡한 쿼리는 백자 이상의 쿼리였고 테이블도 매우 많이 Join된 형태에 수 많은 조건을 달고 진행됬다. 이렇게 복잡한 쿼리를 사용해야 하는 과정 속에서 동적으로 쿼리를 작성할 수 있다는 것이 얼마나 다행인 줄 모른다.
실제로 위에서 나타낸 예제들은 실무에서 사용하기에 매우 간단한 예제 정도라고 생각한다. 어떤 서비스의 형태냐에 따라서 테이블이 복잡해 지는 정도는 천 차 만별이기 때문에 테이블이 복잡해 진다면 당연히 쿼리도 복잡해 지기 마련이다.
QueryDSL을 사용하는 것과 어떤 방식으로 Query를 날려서 최소의 비용을 발생시킬지는 무관한 이야기 이지만 어쨌든 개발자가 느끼기에 동적쿼리의 최강자인 QueryDSL의 존재에 감사를 표할 뿐이다.