--- title: Spring Data JPAをはじめよう tags: [] categories: ["Programming", "Java", "org", "springframework", "data", "jpa"] date: 2011-12-30T19:12:47Z updated: 2011-12-30T19:12:47Z --- 本記事は「[GETTING STARTED WITH SPRING DATA JPA][1]」の翻訳です。1年近く前の記事ですが、Spring Data JPAの説明をするのに良い記事だったので訳してみました。ちょい意訳しているし変な訳が入っているけど気にせずに!すごい誤訳があれば[@making][2]へ。 ---- Spring Data JPAプロジェクトの最初のマイルストーンとなるリリースをちょうど行ったので、Spring Data JPAの特徴の簡単なイントロダクションを行いたいと思います。(訳注:2011/12現在1.0.2.RELEASEがリリースされています)おそらくご存知のとおり、SpringフレームワークはJPAベースのデータアクセス層構築のサポートを提供しています。ですからSpring Data JPAはこのサポートに何を追加するのでしょうか?この問いに対する答えをサンプルドメインに対するデータアクセスコンポーネントを使って説明したいと思います。ここでは単純なJPA+Springを使い、改善の余地を明らかにしていきます。これが終わった後、それらの問題点を示すため、Spring Data JPAの特徴を用いた実装にリファクタリングします。サンプルプロジェクトは同様にステップバイステップでリファクタリングしており、[GitHub][3]に公開しています。 ### ドメイン 簡単のため、よくあるドメインを使って説明します。すなわち、`Account`(口座)をもつ`Customer`(顧客)を扱います。 @Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String firstname; private String lastname; // … methods omitted }
@Entity public class Account { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToOne private Customer customer; @Temporal(TemporalType.DATE) private Date expiryDate; // … methods omitted } `Account`は後のステージで使用する有効期限を持っています。それ以外にクラスやマッピングに関して特別なことはありません(単純なJPAアノテーションを使用しています)。 それでは`Account`オブジェクトを管理するコンポーネントを見ていきましょう。 @Repository @Transactional(readOnly = true) class AccountServiceImpl implements AccountService { @PersistenceContext private EntityManager em; @Override @Transactional public Account save(Account account) { if (account.getId() == null) { em.persist(account); return account; } else { return em.merge(account); } } @Override public List findByCustomer(Customer customer) { TypedQuery query = em.createQuery("select a from Account a where a.customer = ?1", Account.class); query.setParameter(1, customer); return query.getResultList(); } } 後でリファクタリングする際にレポジトリ層を導入する際に名前が被るのを避けるため、わざと`*Service`という名前にしました。ですが、概念的にこのクラスはサービスというよりもレポジトリという方が合っています。それではここで何を行っているでしょうか。 クラスには`@Repository`アノテーションを付加しており、JPAの例外をSpringの`DataAccessEception`の階層の例外に変換できるようにしています。その他、`@Transactional`アノテーションを用いて`save(…)`の操作をトランザクション範囲内で行えるようにしていたり、`findByCustomer(…)`に大しては`readOnly`-flag(これはクラスレベルの設定ですが)を有効にしています。これによりデータベースレベル同様に永続性プロバイダの内部でもパフォーマンスの最適化がはかられます。 利用者が`EntityManager`に対して、`merge(…)`と`persist(…)`のどちらを呼べば良いか悩まなくてもすむように、`Account`の`id`フィールドをみて、`Account`オブジェクトが未管理オブジェクトか管理オブジェクトかを判断しています。このロジックはもちろん共通のスーパークラスに抽出した方がよいです。ドメインオブジェクト毎のレポジトリ実装に毎回このコードを繰り返して書きたくないので。クエリメソッドもまたかなり簡潔です。クエリを作成してパラメータをバインドし、クエリを実行して結果を返すだけです。これはあまりに単純なので、この実装コードがちょっとの想像すればメソッドシグニチャから生成可能なboilerplate (再利用を意図した標準的な文例集)とみなせるでしょう。`Account`の`List`が返ることを期待すれば、クエリはメソッド名と近くなり、単純にメソッドパラメータをバインドします。ご覧のとおり、改善の余地があります。 ### Spring Dataレポジトリサポート 実装のリファクタリングを始める前に、サンプルプロジェクトに[テストケース][4]が含まれていて、リファクタリング中でも実行でき、コードが動くことを検証できるので、見ておいてください。それではどのように実装を改善するかを見ていきましょう。 Spring Data JPAは管理ドメインオブジェクトごとのインタフェースを作成することから始めるレポジトリのプログラミングモデルを提供しています。 public interface AccountRepository extends JpaRepository { … } このインタフェースは2つの役目を果たしています。1つ目は`JpaRepository`を継承することにより、`Account`の保存や削除などの一連の一般的CRUDメソッドを得ることです。2つ目はSpring Data JPAの機構がこのインタフェースをクラスパスからスキャンし、SpringのBeanを生成できるようすることです。 Springがこのインタフェースを実装したBeanwを作成できるようにするためにすることは、Spring JPA名前空間を使用して適切な要素を用い、レポジトリのサポートを有効にすることだけです。 この設定により`com.acme.repositories`以下の全てのパッケージに対して`JpaRepository`を継承したインタフェースをスキャンしSpringのbeanを生成します。裏側では`SimpleJpaRepository`の実装になっています。それではまず、さきほどの`AccoutService`実装を少しリファクタリングして、新たに導入されたレポジトリインタフェースを使うようにしてみましょう。 @Repository @Transactional(readOnly = true) class AccountServiceImpl implements AccountService { @PersistenceContext private EntityManager em; @Autowired private AccountRepository repository; @Override @Transactional public Account save(Account account) { return repository.save(account); } @Override public List findByCustomer(Customer customer) { TypedQuery query = em.createQuery("select a from Account a where a.customer = ?1", Account.class); query.setParameter(1, customer); return query.getResultList(); } } このリファクタリングの結果、レポジトリの`save(...)`の呼び出しに委譲するだけになっています。デフォルトでは、前述の例と同様に、レポジトリ実装はエンティティの`id`プロパティが`null`の場合に未管理オブジェクトとみなします(必要であればより詳細な制御を行うこともできます)。加えて、メソッドへの`@Transactional`アノテーションを取り除くことができます。Spring Data JPAのレポジトリ実装ではCRUDメソッドは既に`@Transactional`でアノテート済みです。 次に、クエリメソッドをリファクタリングします。保存メソッドと同じく、クエリメソッドに関しても処理を委譲させましょう。レポジトリインタフェースにクエリメソッドを導入して、元々作成していたメソッドを、新しい方のメソッドに委譲させます。 @Transactional(readOnly = true) public interface AccountRepository extends JpaRepository { List findByCustomer(Customer customer); }
@Repository @Transactional(readOnly = true) class AccountServiceImpl implements AccountService { @Autowired private AccountRepository repository; @Override @Transactional public Account save(Account account) { return repository.save(account); } @Override public List findByCustomer(Customer customer) { return repository.findByCustomer(customer); } } (訳注:原文にはミスがあったので修正) ここでちょっとトランザクションハンドリングに関する説明を加えさせてください。とても単純なケースであればレポジトリのCRUDメソッドはトランザクショナルでクエリメソッドに関しては既に`@Transactional(readOnly = true)`がレポジトリインターフェースに付与されているので、`AccountServiceImpl`クラスから`@Transactional`アノテーションを完全に取ってしまえばよいです。今回のセットアップでは、(このケースでは不必要ですが、)サービスレベルのメソッドがトランザクショナルであるとマークされており、トランザクション処理で何が起こるかサービスレベルを見ればはっきりと分かるのでベストである。加えて、サービス層のメソッドが修正されて、レポジトリのメソッドを複数回呼ぶようになった場合でも、レポジトリ層の内部トランザクションが外のサービス層で既に開始されたトランザクションに単純に合流するので、全てのコードが1つのトランザクションで実行されます。レポジトリのトランザクションの振る舞いや変更方法については[レファレンスドキュメント][5]に詳細が載っています。(訳注:最新版のリンクに変更) 再びテストケースを実行して、動作するか確認してみてください。ここで立ち止まってください。`findByCustomer(...)`の実装は一切行っていないですよね?どのように動きましたか? ### クエリメソッド Spring Data JPAが`AccountRepository`インタフェースに対するSpringのBeanインスタンスを作成する際、全てのクエリメソッドを検査し、それぞれのクエリを取得します。デフォルトではSpring Data JPAでは自動的にメソッド名をパースして、そこからクエリを作成します。このクエリはJPA criteria APIを使用して実装しています。今回の場合、`findByCustomer(...)`メソッドは論理的にはJPQLクエリ`select a from Account a where a.customer = ?1`と等価です。メソッド名を解析するパーサはかなり巨大なキーワード集合をサポートしています。`And`,`Or`,`GreaterThan`,`LessThan`,`Like`,`IsNull`,`Not`等など。お好みで`OrderBy`を追加することもできます。詳細については[リファレンスドキュメント][6]を参照してください。この機構はGrailsやSpring Rooのようなクエリメソッドプログラミングモデルをもたらします。 次に使いたいクエリを明示的に指定したい場合について考えましょう。JPAのネームドクエリを命名規約にしたがって(今回の場合、`Account.findByCustomer`)、エンティティ上のアノテーションか`orm.xml`に定義する方法とその代わりにレポジトリメソッドに`@Query`アノテーションをつける方法があります。 @Transactional(readOnly = true) public interface AccountRepository extends JpaRepository { @Query("") List findByCustomer(Customer customer); } それではSpring Data JPAの特徴を適用する前、適用した後の`CustomerServiceImpl`を比較してみましょう。 @Repository @Transactional(readOnly = true) public class CustomerServiceImpl implements CustomerService { @PersistenceContext private EntityManager em; @Override public Customer findById(Long id) { return em.find(Customer.class, id); } @Override public List findAll() { return em.createQuery("select c from Customer c", Customer.class).getResultList(); } @Override public List findAll(int page, int pageSize) { TypedQuery query = em.createQuery("select c from Customer c", Customer.class); query.setFirstResult(page * pageSize); query.setMaxResults(pageSize); return query.getResultList(); } @Override @Transactional public Customer save(Customer customer) { // Is new? if (customer.getId() == null) { em.persist(customer); return customer; } else { return em.merge(customer); } } @Override public List findByLastname(String lastname, int page, int pageSize) { TypedQuery query = em.createQuery("select c from Customer c where c.lastname = ?1", Customer.class); query.setParameter(1, lastname); query.setFirstResult(page * pageSize); query.setMaxResults(pageSize); return query.getResultList(); } } それでは`CustomerRepository`を作成し、CRUDメソッドを除外します。 public interface CustomerRepository extends JpaRepository { … }
@Repository @Transactional(readOnly = true) public class CustomerServiceImpl implements CustomerService { @PersistenceContext private EntityManager em; @Autowired private CustomerRepository repository; @Override public Customer findById(Long id) { return repository.findById(id); } @Override public List findAll() { return repository.findAll(); } @Override public List findAll(int page, int pageSize) { TypedQuery query = em.createQuery("select c from Customer c", Customer.class); query.setFirstResult(page * pageSize); query.setMaxResults(pageSize); return query.getResultList(); } @Override @Transactional public Customer save(Customer customer) { return repository.save(customer); } @Override public List findByLastname(String lastname, int page, int pageSize) { TypedQuery query = em.createQuery("select c from Customer c where c.lastname = ?1", Customer.class); query.setParameter(1, lastname); query.setFirstResult(page * pageSize); query.setMaxResults(pageSize); return query.getResultList(); } } まあまあですね。共通的なシナリオ扱う場合に2つのメソッドが残っています。与えられたクエリに対して全てのエンティティにアクセスしたくはなく、1ページに相当する分だけアクセスしたいのです(例えば、ページサイズが10あるのに対して1ページ目など)。これはクエリを適切に制限する2つの整数によって指定されます。これには2つの問題があります。引数の2つの整数は実際にコンセプトを表していますが、明確にはなっていません。加えて、単純に`List`を返していますが、実際のデータのページや、最初のページかどうか、最後のページかどうか、全部で何ページあるかどうかといったメタ情報が欠落しています。Spring Dataは抽象的な2つのインタフェースを提供します。`Pageable`(ページネーションへのリクエスト情報)と`Page`(メタ情報を含む結果情報)です。それではレポジトリインタフェースに`findByLastname(...)`を加えて、`findAll(...)`と`findByLastname(...)`を次のように書き換えましょう。 @Transactional(readOnly = true) public interface CustomerRepository extends JpaRepository { Page findByLastname(String lastname, Pageable pageable); }
@Override public Page findAll(Pageable pageable) { return repository.findAll(pageable); } @Override public Page findByLastname(String lastname, Pageable pageable) { return repository.findByLastname(lastname, pageable); } シグニチャを変えてもテストケースがうまく動くことを確認してください。ここでは2つのコード削減ポイントがあります。CRUDメソッドにページネーションをサポートできることと、クエリ実行機構が`Pageable`パラメータを認識できることです。この段階でクライアントがレポジトリインタフェースを直接使えばラップしただけのクラスは不要になります。実装コードを全て取り除いてしまいます。 ### まとめ このブログ記事で、レポジトリ層に2つのインタフェースと3つのメソッド、およびXMLに1行各子で、かなりの量のコードを削減できました。 @Transactional(readOnly = true) public interface CustomerRepository extends JpaRepository { Page findByLastname(String lastname, Pageable pageable); }
@Transactional(readOnly = true) public interface AccountRepository extends JpaRepository { List findByCustomer(Customer customer); }
タイプセーフなCRUDメソッド、クエリ実行、ページネーションを手に入れました。このいけてる仕組みはJPAベースのレポジトリだけでなく非リレーショナルなデータベースにも使えます。最初の非リレーショナルデータベースのサポートとしてMongoDB対応が近日中にリリースされます。(訳注:2011/12時点で[1.0.0.RC1までリリースされています][7])MongoDBに対して全く同じ特徴を使うことができますし、他のデータベースにも対応する予定です。また他にも機能([エンティティの検査][8]や、[カスタムデータアクセスとの統合][9])があるので次の記事で見ていきたいと思います。 [1]: http://blog.springsource.org/2011/02/10/getting-started-with-spring-data-jpa/ [2]: http://twitter.com/#!/making [3]: https://github.com/SpringSource/spring-data-jpa-examples/tree/master/spring-data-jpa-showcase [4]: https://github.com/SpringSource/spring-data-jpa-examples/tree/master/spring-data-jpa-showcase/src/test/java/org/springframework/data/jpa/showcase/before [5]: http://static.springsource.org/spring-data/data-jpa/docs/1.0.x/reference/html/#transactions [6]: http://static.springsource.org/spring-data/data-jpa/docs/1.0.x/reference/html/#jpa.query-methods.query-creation [7]: http://www.springsource.org/spring-data/mongodb [8]: http://static.springsource.org/spring-data/data-jpa/docs/1.0.x/reference/html/#jpa.auditing [9]: http://static.springsource.org/spring-data/data-jpa/docs/1.0.x/reference/html/#repositories.custom-implementations