SpringDataJPAでの動的なfetchType変更の実現について
開発環境
FW:Spring Boot v1.5.6
言語:Java 8
ORM:Spring Data JPA
DB:postgresql
前提・実現したいこと
上記の環境で、以下のようなリレーションシップを持つエンティティで、TestEntityParentに対してリポジトリTestEntityParentDaoを使用してDBへのクエリの発行を行っています。
その際に無駄なフェッチをなくしSQL効率を上げるために、発行するクエリによって各属性のfetch属性を動的に変更したいです。
元コード
・TestEntityParentエンティティ
@Entity(name = "test_entity_parent")
public class TestEntityParent {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "test_entity_parent_id_seq")
@SequenceGenerator(name = "test_entity_parent_id_seq", sequenceName = "test_entity_parent_id_seq", allocationSize = 1)
@Column(columnDefinition = "default nextval('test_entity_parent_id_seq')")
private Integer id;
private String name;
//動的フェッチ対応前はfetch = FetchType.EAGERを指定
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy(clause="id")
private Set<TestEntityChildren> childrens = new LinkedHashSet<>();
//動的フェッチ対応前はfetch = FetchType.EAGERを指定
@OneToOne(mappedBy = "parent",cascade=CascadeType.ALL, orphanRemoval = true)
private TestEntityAnotherChildren testEntityAnotherChildren;
/* getter、setter省略 */
}
・TestEntityChildrenエンティティ
@Entity(name = "test_entity_children")
public class TestEntityChildren {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "test_entity_children_id_seq")
@SequenceGenerator(name = "test_entity_children_id_seq", sequenceName = "test_entity_children_id_seq", allocationSize = 1)
@Column(columnDefinition = "default nextval('test_entity_children_id_seq')")
private Integer id;
@ManyToOne
@JoinColumn(name = "p_id")
private TestEntityParent parent;
/* getter、setter省略 */
}
・TestEntityAnotherChildrenエンティティ
@Entity(name = "test_entity_anoterchildren")
public class TestEntityAnotherChildren {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "test_entity_anoterchildren_id_seq")
@SequenceGenerator(name = "test_entity_anoterchildren_id_seq", sequenceName = "test_entity_anoterchildren_id_seq", allocationSize = 1)
@Column(columnDefinition = "default nextval('test_entity_anoterchildren_id_seq')")
private Integer id;
@OneToOne
@JoinColumn(name = "p_id")
private TestEntityParent parent;
@ManyToOne
@JoinColumn(name = "ap_id")
private TestEntityAnotherParent a_parent;
//動的フェッチ対応前はfetch = FetchType.EAGERを指定
@OneToMany(mappedBy = "a_children", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<TestEntityGrandSon> grandsons = new LinkedHashSet<>();
/* getter、setter省略 */
}
・TestEntityAnotherParentエンティティ
@Entity(name = "test_entity_anoterparent")
public class TestEntityAnotherParent {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "test_entity_anoterparent_id_seq")
@SequenceGenerator(name = "test_entity_anoterparent_id_seq", sequenceName = "test_entity_anoterparent_id_seq", allocationSize = 1)
@Column(columnDefinition = "default nextval('test_entity_anoterparent_id_seq')")
private Integer id;
private String name;
//動的フェッチ対応前はfetch = FetchType.EAGERを指定
@OneToMany(mappedBy = "a_parent", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy(clause="id")
private Set<TestEntityAnotherChildren> a_childrens = new LinkedHashSet<>();
/* getter、setter省略 */
}
・TestEntityGrandSonエンティティ
@Entity(name = "test_entity_grandson")
public class TestEntityGrandSon {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "test_entity_grandson_id_seq")
@SequenceGenerator(name = "test_entity_grandson_id_seq", sequenceName = "test_entity_grandson_id_seq", allocationSize = 1)
@Column(columnDefinition = "default nextval('test_entity_grandson_id_seq')")
private Integer id;
@ManyToOne
@JoinColumn(name = "ac_id")
private TestEntityAnotherChildren a_children;
/* getter、setter省略 */
}
・TestEntityParentDaoリポジトリ
public interface TestEntityParentDao extends JpaRepository<TestEntityParent,Integer> {
public List<TestEntityParent> findAll();
public TestEntityParent findById(Integer id);
}
調べたところ、エンティティグラフを見つけ、
2種類の定義方法をそれぞれ試みたところ下記のような問題点が浮上しました。
・アノテーションを使用した定義
アノテーションを使用した静的定義だとエンティティグラフの属性ノード・サブグラフの定義が固定化され、エンティティグラフでfetch方式をEAGERにした場合にSQLがまとまりleft outer joinで結合されるため、望んでいない結合結果になってしまい汎用性に欠けてしまいます。
また、findAll()時以外はTestEntityChildrenのfetch属性をLAZYにし、それ以外をEAGER、
findAll()時は全エンティティをEAGERといったような定義ができませんでした。
(fetch属性にfetchType.EAGERを明示的に指定した場合、エンティティグラフでLAZYに指定してもEAGERが優先されてしまうため)
変更後コード
・TestEntityParentエンティティ
@NamedEntityGraph(
name="TestEntityParent.graph",
attributeNodes={
@NamedAttributeNode(value="id")
,@NamedAttributeNode(value="name")
,@NamedAttributeNode(value="childrens", subgraph = "childrens")
,@NamedAttributeNode(value="testEntityAnotherChildren", subgraph = "testEntityAnotherChildren")
},
subgraphs = {
@NamedSubgraph(name = "testEntityAnotherChildren", attributeNodes = {
@NamedAttributeNode("id")
,@NamedAttributeNode("parent")
,@NamedAttributeNode(value="a_parent", subgraph = "a_parent")
,@NamedAttributeNode(value="grandsons", subgraph = "grandsons")
})
,@NamedSubgraph(name = "a_parent", attributeNodes = {
@NamedAttributeNode("id")
,@NamedAttributeNode("name")
,@NamedAttributeNode("a_childrens")
})
,@NamedSubgraph(name = "grandsons", attributeNodes = {
@NamedAttributeNode("id")
,@NamedAttributeNode("a_children")
})
,@NamedSubgraph(name = "childrens", attributeNodes = {
@NamedAttributeNode("id")
,@NamedAttributeNode("parent")
})
}
)
@Entity(name = "test_entity_parent")
public class TestEntityParent {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "test_entity_parent_id_seq")
@SequenceGenerator(name = "test_entity_parent_id_seq", sequenceName = "test_entity_parent_id_seq", allocationSize = 1)
@Column(columnDefinition = "default nextval('test_entity_parent_id_seq')")
private Integer id;
private String name;
//動的フェッチ対応前はfetch = FetchType.EAGERを指定
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy(clause="id")
private Set<TestEntityChildren> childrens = new LinkedHashSet<>();
//動的フェッチ対応前はfetch = FetchType.EAGERを指定
@OneToOne(mappedBy = "parent",cascade=CascadeType.ALL, orphanRemoval = true)
private TestEntityAnotherChildren testEntityAnotherChildren;
/* getter、setter省略 */
}
・TestEntityParentDaoリポジトリ
public interface TestEntityParentDao extends JpaRepository<TestEntityParent,Integer> {
@EntityGraph(value = "TestEntityParent.graph", type = EntityGraphType.LOAD)
public List<TestEntityParent> findAll();
@EntityGraph(value = "TestEntityParent.graph", type = EntityGraphType.LOAD)
public TestEntityParent findById(Integer id);
}
・クエリ発行用メソッド内での独自定義
メソッド内でEntityManagerクラスのcreateEntityGraphメソッドを使用してエンティティグラフを生成し、クエリ発行用メソッドごとにエンティティグラフの定義を変更するように対応。
fetch属性をEAGERにした際に望んでいない結合結果になってしまう問題もJPQLでDISTINCTを使用することで回避。
実現は可能でしたが、クエリごとに定義しなければならないためコード量が膨大になってしまいます。
また、この方法だとエンティティグラフを使用しながらSpringDataJPAの命名規則によるクエリの自動生成を使用することができません。
追加クラス
・TestEntityParentService
@Service
@Transactional
public class TestEntityParentService {
@Autowired
EntityManager em;
public List<TestEntityParent> findAll() {
EntityGraph<TestEntityParent> graph = em.createEntityGraph(TestEntityParent.class);
graph.addAttributeNodes("id");
graph.addAttributeNodes("name");
Subgraph<TestEntityChildren> childrenSubgraph = graph.addSubgraph("childrens");
childrenSubgraph.addAttributeNodes("id");
childrenSubgraph.addAttributeNodes("parent");
Subgraph<TestEntityAnotherChildren> anotherChildrenSubgraph = graph.addSubgraph("testEntityAnotherChildren");
anotherChildrenSubgraph.addAttributeNodes("id");
anotherChildrenSubgraph.addAttributeNodes("parent");
Subgraph<TestEntityAnotherParent> anoterParentSubgraph = anotherChildrenSubgraph.addSubgraph("a_parent");
anoterParentSubgraph.addAttributeNodes("id");
anoterParentSubgraph.addAttributeNodes("name");
anoterParentSubgraph.addAttributeNodes("a_childrens");
Subgraph<TestEntityGrandSon> grandsonSubgraph = anotherChildrenSubgraph.addSubgraph("grandsons");
grandsonSubgraph.addAttributeNodes("id");
grandsonSubgraph.addAttributeNodes("a_children");
TypedQuery<TestEntityParent> query = em.createQuery(
"SELECT DISTINCT tep FROM test_entity_parent tep ORDER BY tep.id",
TestEntityParent.class);
query.setHint("javax.persistence.loadgraph", graph);
List<TestEntityParent> testEntityParentList = query.getResultList();
return testEntityParentList;
}
public TestEntityParent findById(Integer id) {
EntityGraph<TestEntityParent> graph = em.createEntityGraph(TestEntityParent.class);
graph.addAttributeNodes("id");
graph.addAttributeNodes("name");
Subgraph<TestEntityAnotherChildren> anotherChildrenSubgraph = graph.addSubgraph("testEntityAnotherChildren");
anotherChildrenSubgraph.addAttributeNodes("id");
anotherChildrenSubgraph.addAttributeNodes("parent");
Subgraph<TestEntityAnotherParent> anoterParentSubgraph = anotherChildrenSubgraph.addSubgraph("a_parent");
anoterParentSubgraph.addAttributeNodes("id");
anoterParentSubgraph.addAttributeNodes("name");
anoterParentSubgraph.addAttributeNodes("a_childrens");
Subgraph<TestEntityGrandSon> grandsonSubgraph = anotherChildrenSubgraph.addSubgraph("grandsons");
grandsonSubgraph.addAttributeNodes("id");
grandsonSubgraph.addAttributeNodes("a_children");
TypedQuery<TestEntityParent> query = em.createQuery(
"SELECT tep FROM test_entity_parent tep WHERE tep.id = " + id,
TestEntityParent.class);
query.setHint("javax.persistence.loadgraph", graph);
TestEntityParent testEntityParent = query.getSingleResult();
return testEntityParent;
}
}
こういった場合、どのように対応するべきなのでしょうか。
エンティティグラフ以外でも何か対応する方法がありましたらご教授いただけると幸いです。
初めての質問で分かりづらい箇所があるかと思いますが、よろしくお願いいたします。