Spring DATA JPA
はじめに
Spring DATA JPAは、Spring Frameworkの拡張ライブラリ。springframework-jdbc
シリーズか springframework-orm
シリーズのようだが、安定したら本流に組み込まれるのかもしれない。
この記事の執筆時点のバージョンは SPRING DATA JPA 1.1.0 GA
Spring DATA JPAは、JPAの機能をベースに 汎用的な Repositoryの機能を提供する。
ちなみに、Repositoryというのは、ドメイン駆動設計(Domain Driven Design)のパターンのひとつで、ドメインのEntityのCollectionのように振舞う責務を持つ。例えば CustomerRepository
ならば、システムに存在するCustomer
EntityたちをCollectionに保持するかのように振舞う。
PoEAAにもある。参照
もちろん本当のCollectionに保持したら大変なことになるので、バックエンドではデータベースアクセスが行われたりするわけだが、そういったことを抽象化する。
そんなことしたら性能とか気になるとかいう意見も同意。そういったことも含めて Domain Driven Designの書籍に書いてますので是非読んだらいいと思います。
基本的な使い方
JPAをSpringで使用する方法(ややこしいいが springframework-orm.jarのこと)は数年前から大きくは変わっていないので、ほかの記事の説明に譲ります。
以降は、Order
というEntityが、Long
型のid
プロパティ(Primary Key)を持つ場合例で、つまり下記のようなEntityを例とする。
// 普通のJPAのEntity
@Entity
public class Order {
@Id
private Long id;
}
その時の OrderRepository
は下記。
// Spring DATA JPAの Repository
@Repository
public interface OrderRepository extends JpaRepository<Order, Long>
{
}
JpaRepository
を拡張した interface
を定義する。それだけでRepository
完成。実装クラスは無しでもOK。JpaRepository
の型引数には、Entityクラスの型とその@Idの型を指定する。
ちなみに @Repository
を付けたBeanの操作で発生した SQLException
は、Spring内部で DataAccessException
(のサブタイプ)に変換してくれる。これ自体は SpringのJPA機能(ややこしいいが springframework-orm.jar
のこと)やJDBC機能を使っていれば、その依存ライブラリの中で行われる。
DataAccessException
への変換機能については、少し古いが、この辺の記事 が参考になるかと
基本操作
Repositoryを利用するクライアント側のコードを中心に、基本的なデータアクセスをする方法をいくつか。
クライアント側は例えば @Autowired
アノテーションを定義して repository
プロパティにInjectしてもらう。
@Autowired
private OrderRepository repository;
insert
Repository
の save()
メソッドを使用する
Order order = new Order();
// ....
repository.save(order);
内部で EntityManager.persist()
を呼んでいると思われ
update (単一のEntity)
同じく Repository
の save()
メソッドを使用する
order.setDate(orderedDate);
repository.save(order);
内部で EntityManager.merge()
を呼んでいると思われ
delete (単一のEntity)
Repository
の delete()
メソッドにPKを指定して実行する
repository.delete(orderId);
EntityManager.remove()
はEntityの実体を指定する必要があるため、Id値を知っている場合はクエリを必要とする分無駄が発生する。
Repository内部の実装はどうなっているか見たところ 、結局中身は一緒だった。。実行時効率はともかく、開発効率はあがるということで。
query (単一のEntity)
Repository
の findOne()
メソッドにPKを指定して実行する
repository.findOne(orderId);
内部で EntityManager.find()
を呼んでいると思われ
query (条件指定で複数Entity)
Spring DATA JPAのクエリは独特の方法を提供している。
findByメソッド・シグネチャ
Repository
の interface
にfindBy
で始まるメソッドを定義すると、そのメソッドの(findByに続く)名前や型、シグネチャによって、CoC的にクエリ条件が決定するというもの。
具体的な例として、OrderRepository
に findByNameStartingWith(String p)
を定義すると、FROM Order o WHERE o.Item LIKE p%
相当のクエリ結果を取得するメソッドになる。
@Repository
public interface OrderRepository extends JpaRepository<Order, Long>
{
List<Order> findByItemStartingWith(String itemPrefix);
}
値の一致、大小比較、日付時刻の比較なども同じようにできる。詳しくはリファレンス文書 参照されたし
@Queryアノテーション
また、クエリ条件として、JPQL式やNatvie Queryを指定することもできる。
@Repository
public interface OrderRepository extends JpaRepository<Order, Long>
{
@Query("select o from Order o where o.customerEmailAddress = :address")
List<Order> findByCustomerEmail(@Param("address")String emailAddress);
}
Specification
Specificationもドメイン駆動設計のパターンの1つ。ドメインにおいて意味を持つ条件(仕様)を表現するクラスのこと。Spring DATA JPAには、Specification
クラスをRepository
に渡すと、その条件に合致するEntity
をクエリする機能がある。
ドメイン駆動設計のSpecification
はboolean isSatisfiedBy(T)
とかand
or
not
で組み合わせることができるとか、もっと小難しい話があった気がするが、もう忘れてしまったのと話が脱線するので深追いしないことにする。
Specification
オブジェクトを構築するコード例
public class OrderSpecifications {
public static Specification<Order> isOrderedInPastDays(final long days) {
return new Specification<Order>() {
@Override
public Predicate toPredicate(Root<Order> root,
CriteriaQuery<?> query, CriteriaBuilder cb) {
long previousPastDays = System.currentTimeMillis() - 36000000 * 24 * days;
return cb.greaterThanOrEqualTo(
root.get(root.getModel().getSingularAttribute("date", Date.class)),
new Date(previousPastDays)
);
}
};
}
}
Specification<T>
の toPredicate
を実装し、JPAの Criteria APIの Predicate
を返す。
クエリ条件として JPAの Criteria APIの Predicate
を返すコードを書くので、実装上はそんなに便利ではない(Criteria APIが使いにくいことを考えるとむしろ不便)。だが、ドメイン駆動設計では、ビジネスロジックがSQLなどのクエリに入り込んでしまうことをよろしくないこととしているので、ドメインの設計上の意図やビジネスロジック(条件)をオブジェクトで表現するためにSpecification
を Repository
のクエリに対して用いるということなら使えばよいでしょう。という感じ。
Repository
を定義する interface
は JpaSpecificationExecutor
も extends
する
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order>
import static OrderSpecifications.*;
// ...
List<Order> recentOrders = repository.findAll(isOrderedInPastDays(4));
update/delete (条件指定で複数Entity)
上記の@Query
と同じ方法でJPQL式を指定する。@Modifying
を一緒につけると更新できる。
@Repository
public interface OrderRepository extends JpaRepository<Order, Long>
{
@Modifying
@Query("update Order o set o.item = :itemName where o.date < :orderedDate")
int SetItemBefore(@Param("orderedDate")Date date, @Param("itemName")String newItemName);
}
まとめ&感想
Spring DATA JPAは、Repositoryのinterface定義に findByAgeLessThan(int age)のように定義するだけで、Injectされた実装クラス(Springが提供)が気を利かせてそのメソッド名とシグネチャから、クエリを生成するというもの。
もちろん メソッド名や型がその責務を示すべきであることは完全に同意するけど、ちょっと硬派すぎるというか、まじめにドメインモデルを設計しないと、意味の分からないメソッドを大量生産しそう。。
というか、これがすんなり当てはまるように設計するべき。というメッセージなのかもしれない。
ダメだった人のために(?) JPQLやNativeQueryも使える。クエリ系ではなく一括更新の場合はそれを使う以外はなさげ。
Specification
は便利機能というより設計意図を表現するためのものっぽい
参考までに、動かしてみたときのコードはこちら
Repository
クライアントコード