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;
Repositoryの save() メソッドを使用する
Order order = new Order();
// ....
repository.save(order);
内部で EntityManager.persist() を呼んでいると思われ
同じく Repositoryの save() メソッドを使用する
order.setDate(orderedDate);
repository.save(order);
内部で EntityManager.merge() を呼んでいると思われ
Repositoryの delete() メソッドにPKを指定して実行する
repository.delete(orderId);
EntityManager.remove() はEntityの実体を指定する必要があるため、Id値を知っている場合はクエリを必要とする分無駄が発生する。
Repository内部の実装はどうなっているか見たところ 、結局中身は一緒だった。。実行時効率はともかく、開発効率はあがるということで。
Repositoryの findOne() メソッドにPKを指定して実行する
repository.findOne(orderId);
内部で EntityManager.find() を呼んでいると思われ
Spring DATA JPAのクエリは独特の方法を提供している。
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);
}
値の一致、大小比較、日付時刻の比較なども同じようにできる。詳しくはリファレンス文書 参照されたし
また、クエリ条件として、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もドメイン駆動設計のパターンの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
クライアントコード