NHN Cloud Meetup 編集部
持続可能なソフトウェアのコーディング(6)JPAの利点
2020.03.12
2,144
JPA/Hibernate
RDBMSをリポジトリとして使用するプロジェクトにおいて、最近はほとんどJPAが使用されているようです。
なぜ、JPAはオブジェクト指向プログラミングとマッチするのでしょうか?
理由としては、主にこのようなものが挙げられるでしょう。
– 自動でクエリを生成してくれる
– キャッシュがあるため早い
上記は、JPA/Hibernate、Spring Data JPAの利点ではありますが、必ずしもその理由から使用すべきだ、ということではないようです。JPA/Hibernateは、オブジェクト指向プログラミングを行うための、非常に適切な永続化(Persistence)フレームワークです。特に、カスケード属性を用いた遷移の永続性(transitive persistence)を利用すれば、より便利なオブジェクト指向プログラミングが可能となります。ここで、上記の内容を少し補足してみます。
- メソッドでクエリを組むことができる
- QueryDSL、Criteriaクエリ、JPQLが該当するでしょう。しかし、QueryDSLはJPA標準ではありません。オープンソースプロジェクトの1つで、JPQLを作る一種のDSLです。
- 自動でクエリを生成してくれる
- メソッド名にクエリが自動的に実行される機能を指しています。Spring Data JPAフレームワークにおいて、naming strategyによって自動的にクエリが生成されます。これもまたJPA標準ではないので、Spring Data JPAの依存性を追加する必要があります。
- キャッシュがあるため早い
- JPA/Hibernateの1次キャッシュであるEntityManagerを指しています。キャッシュという表現から誤解が生じやすいですが、永続性のためのデータストアです。EntityManagerは、データベーストランザクションと密着しており、トランザクションが完了したら、保存されたデータは消去されます。
なぜ、オブジェクト指向プログラミングを行うために、JPAを永続化フレームワークで作成しなければならないのでしょうか?一例を挙げてみましょう。製品情報のProductEntityと、商品のカラー情報を持つColourEntityがあるとします。そして、これらは次のようなビジネスモデルを持っています。
- Product生成時、Colourを一緒に生成する
- 1つのProductにつき、最大10個のColourを持つことができる
- 特定のProductを削除すると、Colourも一緒に削除する
- Product名はColour名のプレフィックス(prefix)として使用される
- Product名が変更されると、Colour名も一緒に変更される
このような状況で、ProductをモデリングしたProductEntityと、ColourをモデリングしたColourEntityの関係について考えてみましょう。ひとまず、データベースに情報を保存することは考えないようにしましょう。
さて、Colourは、Productの属性であり、両者の関係は非常に密接で(削除、作成など)分離して考えるのは困難です。そのため、ColourEntityオブジェクトは、ProductEntityオブジェクトのStringと同様に、ProductEntityの属性になります。またビジネスモデルによれば、ProductEntityが生成されると、ColourEntitiesも生成され、ProductEntityが変更されると、ColourEntityも一緒に修正する必要があります。削除も同様ですね。その結果、ColourEntityは、ProductEntityのライフサイクルのようになります。
上記の機能を実装するには、ProductEntityクラスにupdateName()メソッドを追加すればよいでしょう。そして、ProductEntityの属性であるColourEntitiesの名前も、updateName()メソッド内で一緒に変更する必要があります。ところが、RDBモデルではProductとColourは、tbl_product、tbl_colourテーブルにモデリングする必要があり、両方のテーブルは、1:Nの関係を持っています。データベースの観点からプログラミングすると、tbl_productのレコードを変更するクエリと、tbl_colourのレコードを変更するクエリを実行する必要があります。つまり、ProductEntity.updateName()メソッドで、上記の機能を実装することは困難です。
このように、RDBMSモデルとオブジェクト指向プログラミングが、相互にマッチしない場合、JPAを利用すると、より大幅にプログラミングすることができます。JPAの@OneToMany関連を使用する場合は、カスケード(Cascade)オプションを利用してみましょう。すると、上記のような状況で、ProductEntityとColourEntityは、次のようになります。
@Getter @Entity public class ProductEntity { @Id private Long productId; private String name; // Produtと1:Nの関係ですが、機能上は最大10個までColourを持つことができるので、EAGERを宣言します。 // ビジネスロジック上、最大10個まで可能な設定です。 // ProductEntityをEMでProductEntityをpersistすると、ColourEntityも一緒にfetchされます。 @OneToMany( mappedBy = "product", fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, // Product生成時、Colour Entityも一緒に生成するため CascadeType.MERGE, // Product名の変更時、Colour Entityも一緒に変更するため CascadeType.REMOVE // Produt削除時、ColourEntityも一緒に削除するため } ) private List<ColourEntity> colourEntities; // ProductEntityを生成するstatic factoryメソッドが2つが宣言されています。 // そのため、ビジネスモデルを表明するassert構文がプライベートコンストラクタで使用されました。 private ProductEntity(Long productId, String name, List<String> colourNames){ AssertionUtil.notNull(productId); AssertionUtil.notEmpty(name); // ColourNamesは最大10個できるように表明します。 AssertionUtil.notGreaterThan(colourNames, 10); this.productId = productId; this.name = name; this.colourEntities = Optional.ofNullable(colourNames) .stream() .flatMap(Collection::stream) .map(colour -> ColourEntity.of(this.name, colour)) .collect(Collectors.toList()); } public static final ProductEntity of(Long productId, name, List<String> colourNames){ return new ProductEntity(productId, name, colourNames); } public static final ProductEntity of(Long productId, name){ return new ProductEntity(productId, name, Collections.emptyList()); } // ProductEntity名の変更時、Colour名も一緒に変更する必要があります。 public ProductEntity updateName(String name){ AssertionUtil.notEmpty(name); this.name = name; this.colourEntities = colourEntities.stream() .forEach(colourEntity -> colourEntity.updateColourPrefix(this.name)); } }
@OneToMany cascade = カスケードオプションによって、クエリを実行することなく、ProductEntityのupdateName()内で、tbl_productと、tbl_colourテーブルのname値を更新することができます。さて、もう少しオブジェクト指向プログラミングにできないでしょうか?
- シャイコーディング
- 機械的なsetterメソッドがありません。
- 意味を持つupdateName()メソッドで、関連するすべての情報を一度に処理します。
- DRY原則
- 関連するメソッドだけを呼び出します。
- 表明プログラミング
- private ProductEntityコンストラクタをご確認ください。ビジネスロジックによって、さまざまな情報に対して表明文(assertion)が入っていますね。
- 不変式
- 常に名前がnullではなく、10個未満の個数を有する、という不変式を持っています。
- updateProductNameとプライベートコンストラクタのみがデータをセットし、表明文があるため、上記の不変式に合致しない状態はありません。
遷移の永続性
遷移の永続性に対するCascadeTypeは、次のような性質を持っています。
public enum CascadeType { ALL, PERSIST, // 特定のエンティティを保存するとき、関連するエンティティを保存する。 MERGE, // 特定のエンティティを修正するとき、関連するエンティティも修正する。 REMOVE, // 特定のエンティを削除するとき、関連するオブジェクトを削除する。 REFRESH, // 特定のエンティティをEntity Managerで更新(refresh)するとき、関連するオブジェクトも更新する。 DETACH; // 特定のエンティティをEntityManagerから除外するとき(detach)、関連するオブジェクトも除外する。 private CascadeType() { } }
孤立オブジェクト
親エンティティとサブエンティティが相互に接続されていると考えてみましょう。サブエンティティは、親エンティティへの参照がなくなると孤立します。このようなエンティティは、DBMSから削除した方がよいでしょう。次のコードを見てみましょう。
public class ProductEntity { // 省略 @OneToMany( mappedBy = "product", fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE }, orphanRemoval = true // colourEntityがproductEntityに対して参照を失ったら自動削除します。 ) private List<ColourEntity> colourEntities; // 省略 public void soldoutAllColours(){ // Listのclear()を実行すると、colourEntityが参照を失います。 colourEntities.clear(); } }
なお、orphanRemoval属性は、@OneToMany、@OneToOneでのみ使用可能です。@ManyToOneの場合でも、orphanRemovalになれば、親エンティティを削除することになり、親エンティティを参照していた他のサブエンティティが、むしろサブになる奇妙な状況に陥ります。下図において、RED ColourとCoat Productの間で、orphanRemovalになったと考えてみましょう。Coatと関連関係にある他のカラーはどうなるでしょうか?
さいごに
JPAのカスケード機能をご存知の方は大勢いらっしゃいますが、実際に使用している方は少ないようです。その理由は、クエリコントロールができず、パフォーマンスに影響を及ぼすのではないか、という漠然とした不安感が影響しているようです。しかし、ここで最も重要なことは、ビジネスロジックをどのように精巧に作るかということです。これによって、カスケードを簡単に使用することができるようになるでしょう。上記の例でも「カラーは、1つの製品に10個まで登録することができる」という制約によって作成することができました。もし、10個以上登録したらどうしましょうか?答えは、リファクタリングすればよいのです。まさか製品のカラーが100件を超えることはないでしょう。
JPAに対する誤解も解きたいと思い、「持続可能なソフトウェアのコーディング方法」の記事を書き始めました。Springとオブジェクト志向について、もっと詳しく紹介したかったのですが、時間が足りないようです。
最後まで読んでいただきありがとうございました。