2022-05-23-TIL
Today I Learned
Java Stream Inner Join
DB에서 가져온 두 리스트를 키로 이어 붙여야 할 때가 있다. SQL이라면 INNER JOIN으로 끝나지만, 서로 다른 저장소에서 가져온 데이터라면 자바 코드에서 합쳐야 한다.
가장 먼저 떠오르는 방법은 이중 반복이다. 한쪽 리스트의 각 원소마다 다른 쪽 리스트 전체를 훑어 키가 같은 것을 찾는다. 길이가 n, m이면 비교를 n x m번 하므로 데이터가 커지면 느려진다.
1
2
3
4
5
6
// 이중 반복: n x m번 비교
List<OrderView> result = orders.stream()
.flatMap(o -> members.stream()
.filter(m -> m.getId().equals(o.getMemberId()))
.map(m -> new OrderView(o, m)))
.collect(Collectors.toList());
한쪽을 키 기준의 Map으로 먼저 만들어 두면, 다른 쪽을 한 번만 훑으면서 해시 조회로 짝을 찾을 수 있다. DB의 해시 조인과 같은 발상이다. Collectors.toMap은 키가 중복되면 IllegalStateException을 던지므로, 한 키에 여러 원소가 있을 수 있다면 groupingBy로 Map<K, List<V>>를 만든다.
1
2
3
4
5
6
7
Map<Long, Member> memberById = members.stream()
.collect(Collectors.toMap(Member::getId, Function.identity()));
List<OrderView> result = orders.stream()
.filter(o -> memberById.containsKey(o.getMemberId())) // 짝이 없는 것은 버린다 (inner join)
.map(o -> new OrderView(o, memberById.get(o.getMemberId())))
.collect(Collectors.toList());
라이브러리를 써도 된다. jOOλ는 Stream을 확장한 Seq에 innerJoin, leftOuterJoin, crossJoin 같은 SQL 스타일 연산을 더한다. 다만 innerJoin(other, predicate)는 임의의 조건식으로 짝을 판단하는 API라서 키로 바로 찾는 해시 조회를 기대하기 어렵다. 데이터가 크면 위처럼 Map을 쓰는 편이 안전하다.
- https://helloino.tistory.com/61
- https://stackoverflow.com/questions/36913083/java-stream-api-inner-join-two-lists
- https://dzone.com/articles/become-a-master-of-java-streams-part-5-turn-joined
- https://blog.jooq.org/common-sql-clauses-and-their-equivalents-in-java-8-streams/
- https://github.com/jOOQ/jOOL
- https://sooin01.tistory.com/entry/Java%EC%97%90%EC%84%9C-%EC%84%9C%EB%A1%9C-%EB%8B%A4%EB%A5%B8-%EB%91%90-List-%EA%B0%84-Join-%EB%B0%A9%EB%B2%95
- https://velog.io/@yaincoding/Java8%EC%9D%98-%EC%8A%A4%ED%8A%B8%EB%A6%BCStream%EC%9C%BC%EB%A1%9C-%EC%BB%AC%EB%A0%89%EC%85%98-%EC%97%B0%EC%82%B0%EC%9D%84-%EB%8D%94-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9C%BC%EB%A1%9C-%ED%95%98%EC%9E%90
Q타입 조건절
QueryDSL에서 검색 조건이 사용자의 입력에 따라 붙었다 빠졌다 하는 동적 쿼리를 만드는 방법은 두 가지다.
첫째는 BooleanBuilder다. Querydsl 레퍼런스는 복잡한 조건식을 만들 때 BooleanBuilder를 쓰라고 하고, 이 객체는 가변이며 처음에는 비어 있다가 and, or를 호출할 때마다 결과가 쌓인다고 설명한다.
1
2
3
4
5
6
7
8
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(member.name.eq(name));
}
if (minAge != null) {
builder.and(member.age.goe(minAge));
}
List<Member> result = queryFactory.selectFrom(member).where(builder).fetch();
둘째는 조건마다 BooleanExpression을 돌려주는 메서드를 만들고, 값이 없으면 null을 돌려주는 방식이다. Querydsl은 where에 들어온 null 조건을 무시한다. 5.0.0 기준 DefaultQueryMetadata.addWhere는 넘어온 조건이 null이면 아무것도 하지 않고 반환한다.
1
2
3
4
5
6
7
8
9
10
11
List<Member> result = queryFactory.selectFrom(member)
.where(nameEq(name), ageGoe(minAge))
.fetch();
private BooleanExpression nameEq(String name) {
return name != null ? member.name.eq(name) : null;
}
private BooleanExpression ageGoe(Integer minAge) {
return minAge != null ? member.age.goe(minAge) : null;
}
두 번째 방식은 where만 보고 어떤 조건이 걸리는지 읽을 수 있고, 조건 메서드를 다른 쿼리에서 재사용하거나 조합(nameEq(name).and(ageGoe(minAge)))할 수 있다. 다만 조합할 때 앞쪽 메서드가 null을 돌려주면 NullPointerException이 나므로 주의해야 한다. 또 모든 조건이 null이면 where가 비어 전체 조회가 되므로, 조건이 하나도 없을 때의 동작을 따로 막아야 할 수 있다.
- https://jaime-note.tistory.com/69
댓글
아직 댓글이 없습니다