はじめに
タイトルにあることを実現するためには何種類か方法があります。
- children側を
org.hibernate.annotations.Where
で修飾する -
join on
+ EntityGraph -
org.hibernate.annotations.Filter
(これは難しいのでパス)
どの方法が自分にとって良いか検証した結果を以下に記載します。
コード
org.hibernate.annotations.Where
を使う
Article-Commentsモデル+リポジトリ
典型的なOneToMany
なパターンです
@Getter
@Entity @Table(name = "articles")
public class Article {
@Id @GeneratedValue
@Column(name = "id")
private Integer id;
@Where(clause = "active = true")// ここ
@OneToMany(mappedBy = "article")
private Set<Comment> comments;
}
@Getter
@Entity @Table(name = "comments")
public class Comment {
@Id @GeneratedValue
@Column(name = "id")
private Integer id;
@Column(name = "active", nullable = false)
private Boolean active;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "article_id", nullable = false)
private Article article;
}
public interface ArticleRepository extends JpaRepository<Article, Integer> {
@Query("select a from Article a left join fetch a.comments where a.id = ?1")
Optional<Article> findById_JoinFetch(Integer id);
@Query("select a from Article a where a.id = ?1")
@EntityGraph(attributePaths = "comments")
Optional<Article> findById_EntityGraph(Integer id);
@Query("select a from Article a join a.comments c on c.active = false where a.id = ?1")
@EntityGraph(attributePaths = "comments")
Optional<Article> findById_Inactive(Integer id);
}
何もしない場合
@Sql(statements = {
"INSERT INTO articles(id) VALUES (1);",
"INSERT INTO comments(id, article_id, active) VALUES (11, 1, 1), (12, 1, 1), (13, 1, 0);"
})
@Transactional(readOnly = true)
@Test
public void test_plain() {
Optional<Article> actual = target.findById(1);
assertThat(actual.get().getComments(), hasSize(2));
}
この時に実行されるSQL文は以下になります。
Hibernate:
select
article0_.id as id1_0_0_
from
articles article0_
where
article0_.id=?
Hibernate:
select
comments0_.article_id as article_3_1_0_,
comments0_.id as id1_1_0_,
comments0_.id as id1_1_1_,
comments0_.active as active2_1_1_,
comments0_.article_id as article_3_1_1_
from
comments comments0_
where
(
comments0_.active = 1
)
and comments0_.article_id=?
当然ですがSQL文は複数回実行されます。
しかし「記事+アクティブなコメントのみを取得したい」という要求は満たしています。
join fetch
を使う場合
@Sql(statements = {
"INSERT INTO articles(id) VALUES (1);",
"INSERT INTO comments(id, article_id, active) VALUES (11, 1, 1), (12, 1, 1), (13, 1, 0);"
})
@Transactional(readOnly = true)
@Test
public void test_join_fetch() {
Optional<Article> actual = target.findById_JoinFetch(1);
assertThat(actual.get().getComments(), hasSize(2));
}
この時に実行されるSQL文は以下になります。
Hibernate:
select
article0_.id as id1_0_0_,
comments1_.id as id1_1_1_,
comments1_.active as active2_1_1_,
comments1_.article_id as article_3_1_1_,
comments1_.article_id as article_3_1_0__,
comments1_.id as id1_1_0__
from
articles article0_
left outer join
comments comments1_
on article0_.id=comments1_.article_id
and (
comments1_.active = 1
)
where
article0_.id=?
SQL文は1本にまとまり、結果も想定どおりです。
前者よりは良さそうです。
EntityGraphを使う場合
@Sql(statements = {
"INSERT INTO articles(id) VALUES (1);",
"INSERT INTO comments(id, article_id, active) VALUES (11, 1, 1), (12, 1, 1), (13, 1, 0);"
})
@Transactional(readOnly = true)
@Test
public void test_entity_graph() {
Optional<Article> actual = target.findById_EntityGraph(1);
assertThat(actual.get().getComments(), hasSize(2));
}
この時に実行されるSQL文は以下になります。
Hibernate:
select
article0_.id as id1_0_0_,
comments1_.id as id1_1_1_,
comments1_.active as active2_1_1_,
comments1_.article_id as article_3_1_1_,
comments1_.article_id as article_3_1_0__,
comments1_.id as id1_1_0__
from
articles article0_
left outer join
comments comments1_
on article0_.id=comments1_.article_id
and (
comments1_.active = 1
)
where
article0_.id=?
join fetch
の場合と同じです。
突然「記事+非アクティブなコメント」を取得しなければならないことになった
Q : 以下のテストは通るでしょうか?
@Sql(statements = {
"INSERT INTO articles(id) VALUES (1);",
"INSERT INTO comments(id, article_id, active) VALUES (11, 1, 1), (12, 1, 1), (13, 1, 0);"
})
@Transactional(readOnly = true)
@Test
public void test_inactive() {
// @Query("select a from Article a join a.comments c on c.active = false where a.id = ?1")
// @EntityGraph(attributePaths = "comments")
Optional<Article> actual = target.findById_Inactive(1);
assertThat(actual.get().getComments(), hasSize(1));// 非アクティブなコメントだけ取れていて欲しい
}
A : 通りません
実行されるSQL文は以下です。
Hibernate:
select
article0_.id as id1_0_0_,
comments1_.id as id1_1_1_,
comments1_.active as active2_1_1_,
comments1_.article_id as article_3_1_1_,
comments1_.article_id as article_3_1_0__,
comments1_.id as id1_1_0__
from
articles article0_
inner join
comments comments1_
on article0_.id=comments1_.article_id
and (
comments1_.active = 1
)
and (
comments1_.active=0
)
where
article0_.id=?
明らかに通らないので何か他の方法を考える必要がありそうです。
所感
- メリット
- 暗黙的にフィルタ(
where
,join on
)してくれる - 選択肢が多い(join fetchなし, join fetch, EntityGraphいずれでも実現可能)
- デメリット
- クエリの見通しは落ちる
- Parent -> Childへのクエリ全てが影響を受けるのでクエリが増えてくると制御に苦しむ可能性がある
- 逆のフィルタ条件を使いたい時に厳しい
- Hibernate独自の機能なのでプロバイダに依存する
というわけで「便利な時もあるけど融通の効かないヤツ」といった印象です。
join on
+ EntityGraph
Department-Usersモデル+リポジトリ
これも典型的なOneToMany
パターンです
@Getter
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue
@Column(name = "id")
private Integer id;
// @Whereは使わない
@OneToMany(mappedBy = "department")
private Set<User> users;
}
@Getter
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
@Column(name = "id")
private Integer id;
@Column(name = "active", nullable = false)
private Boolean active;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "department_id", nullable = false)
private Department department;
}
public interface DepartmentRepository extends JpaRepository<Department, Integer> {
@Query("select d from Department d left join d.users u on u.active = true")
Optional<Department> findById_Join(Integer id);
@Query("select d from Department d left join d.users u on u.active = true")
@EntityGraph(attributePaths = "users")
Optional<Department> findById_EntityGraph(Integer id);
@Query("select d from Department d left join d.users u on u.active = false")
@EntityGraph(attributePaths = "users")
Optional<Department> findById_Inactive(Integer id);
}
join on
のみの場合
Q : 以下のテストは通ると思いますか?
@Sql(statements = {
"INSERT INTO departments(id) VALUES (1);",
"INSERT INTO users(id, department_id, active) VALUES (11, 1, 1), (12, 1, 1), (13, 1, 0);"
})
@Transactional(readOnly = true)
@Test
public void test_join() {
// @Query("select d from Department d left join d.users u on u.active = true")
Optional<Department> actual = target.findById_Join(1);
assertThat(actual.get().getUsers(), hasSize(2));
}
A : 通りません
実行されるSQL文は以下です
Hibernate:
select
department0_.id as id1_2_
from
departments department0_
left outer join
users users1_
on department0_.id=users1_.department_id
and (
users1_.active=1
)
Hibernate:
select
users0_.department_id as departme3_3_0_,
users0_.id as id1_3_0_,
users0_.id as id1_3_1_,
users0_.active as active2_3_1_,
users0_.department_id as departme3_3_1_
from
users users0_
where
users0_.department_id=?
1クエリ目はともかく2クエリ目で普通に全部取得しちゃっています。
一見動きそうに見える(自分的には)だけにこれはショックです。
join on
+ EntityGraph
前者にグラフの指定を追加しただけのものです。
@Sql(statements = {
"INSERT INTO departments(id) VALUES (1);",
"INSERT INTO users(id, department_id, active) VALUES (11, 1, 1), (12, 1, 1), (13, 1, 0);"
})
@Transactional(readOnly = true)
@Test
public void test_join_entity_graph() {
Optional<Department> actual = target.findById_EntityGraph(1);
assertThat(actual.get().getUsers(), hasSize(2));
}
テストは通ります。
グラフの指定の有無がポイントになり、これを忘れてしまうと怖いことが分かりました。
突然「部署+非アクティブなユーザ」を取得しなければならないことになった(2回目)
@Sql(statements = {
"INSERT INTO departments(id) VALUES (1);",
"INSERT INTO users(id, department_id, active) VALUES (11, 1, 1), (12, 1, 1), (13, 1, 0);"
})
@Transactional(readOnly = true)
@Test
public void test_inactive() {
Optional<Department> actual = target.findById_Inactive(1);
assertThat(actual.get().getUsers(), hasSize(1));
}
今回は上記のテストは普通に通ります、SQL文は以下。
Hibernate:
select
department0_.id as id1_2_0_,
users1_.id as id1_3_1_,
users1_.active as active2_3_1_,
users1_.department_id as departme3_3_1_,
users1_.department_id as departme3_3_0__,
users1_.id as id1_3_0__
from
departments department0_
inner join
users users1_
on department0_.id=users1_.department_id
and (
users1_.active=0
)
join fetch on
の場合
以下のクエリは実行できそうに見えます
@Query("select d from Department d left join fetch d.users u on
u.active=true where d.id = :id")
が、現在のところjoin fetch on
という仕様はないので↑の様なクエリは定義できず起動時にエラーで落ちます。
(他にありえないJPQLクエリ書いてた場合も同様に落としてくれるので助かる)
所感
- メリット
- フィルタ条件を
@Where
よりは柔軟に制御できる - クエリ内で完結するという安心感、見通しもいい
- プロバイダに依存しない(表面上は)
- フィルタ条件を
- デメリット
- fetchできていない場合に思わぬバグを生み出す危険性がある
- けどテスト書いてれば回避できる
- 選択肢が狭い(
join on
+ EntityGraphのみ、他にもあるかも?)- グラフが適用されないバグがあった場合とかに辛い
- fetchできていない場合に思わぬバグを生み出す危険性がある
まとめ
どちらにも長所・短所があるのでそれぞれの要件に応じて使い分けるのが良さそう。
@Where
が良さそうなケース
- 今後要件は絶対にブレないし、絶対にフィルタ条件を固定するという強い信念がある
-
@Where
が与える(意図しない)影響を自分やチームメンバーが制御できる自信がある
join on
+EntityGraphが良さそうなケース
- 色々な条件でフィルタする可能性がある
- プロバイダに絶対依存したくないマン
- 静的クエリだけどフィルタ条件を引数に取る必要がある場合
-
join p.children c on c.foo = ?1
みたいな
-
- 動的クエリでフィルタ条件が変わる様な場合
// あまりいい例じゃないけどこんなイメージ
static Specification<Parent> spec(Integer id, @Nullable String foo) {
return (root, query, cb) -> {
Join<Parent, Child> jChildren = root.join(Parent_.children);
if (foo != null) {
jChildren.on(cb.equal(jChildren.get(Child_.foo), foo);
} else {
jChildren.on(cb.equal(jChildren.get(Child_.foo), "bar");
}
// ...
return cb.equal(root.get(Parent_.id), id);
};
}
最後に
自分にはjoin on
+EntityGraphが合ってると思ったのでメインに採用しました。(他にいい方法あったら知りたい)
ちなみにシンプルなモデルだと大丈夫ですが
-
@Where
がなぜか効かない - EntityGraphがなぜか適用されない
なんていうこともあったりするので即座にSQLログ確認できる状態にしておくのはどの方法を選択するにしても絶対に必須です。