IK.AM

@making's tech note


Spring Data JPAをはじめよう

🗃 {Programming/Java/org/springframework/data/jpa}
🗓 Updated at 2011-12-30T19:12:47Z  🗓 Created at 2011-12-30T19:12:47Z   🌎 English Page

本記事は「GETTING STARTED WITH SPRING DATA JPA」の翻訳です。1年近く前の記事ですが、Spring Data JPAの説明をするのに良い記事だったので訳してみました。ちょい意訳しているし変な訳が入っているけど気にせずに!すごい誤訳があれば@makingへ。


Spring Data JPAプロジェクトの最初のマイルストーンとなるリリースをちょうど行ったので、Spring Data JPAの特徴の簡単なイントロダクションを行いたいと思います。(訳注:2011/12現在1.0.2.RELEASEがリリースされています)おそらくご存知のとおり、SpringフレームワークはJPAベースのデータアクセス層構築のサポートを提供しています。ですからSpring Data JPAはこのサポートに何を追加するのでしょうか?この問いに対する答えをサンプルドメインに対するデータアクセスコンポーネントを使って説明したいと思います。ここでは単純なJPA+Springを使い、改善の余地を明らかにしていきます。これが終わった後、それらの問題点を示すため、Spring Data JPAの特徴を用いた実装にリファクタリングします。サンプルプロジェクトは同様にステップバイステップでリファクタリングしており、GitHubに公開しています。

ドメイン

簡単のため、よくあるドメインを使って説明します。すなわち、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<Account> 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(…)のどちらを呼べば良いか悩まなくてもすむように、Accountidフィールドをみて、Accountオブジェクトが未管理オブジェクトか管理オブジェクトかを判断しています。このロジックはもちろん共通のスーパークラスに抽出した方がよいです。ドメインオブジェクト毎のレポジトリ実装に毎回このコードを繰り返して書きたくないので。クエリメソッドもまたかなり簡潔です。クエリを作成してパラメータをバインドし、クエリを実行して結果を返すだけです。これはあまりに単純なので、この実装コードがちょっとの想像すればメソッドシグニチャから生成可能なboilerplate (再利用を意図した標準的な文例集)とみなせるでしょう。AccountListが返ることを期待すれば、クエリはメソッド名と近くなり、単純にメソッドパラメータをバインドします。ご覧のとおり、改善の余地があります。

Spring Dataレポジトリサポート

実装のリファクタリングを始める前に、サンプルプロジェクトにテストケースが含まれていて、リファクタリング中でも実行でき、コードが動くことを検証できるので、見ておいてください。それではどのように実装を改善するかを見ていきましょう。

Spring Data JPAは管理ドメインオブジェクトごとのインタフェースを作成することから始めるレポジトリのプログラミングモデルを提供しています。

public interface AccountRepository extends JpaRepository<Account, Long> { … }

このインタフェースは2つの役目を果たしています。1つ目はJpaRepositoryを継承することにより、Accountの保存や削除などの一連の一般的CRUDメソッドを得ることです。2つ目はSpring Data JPAの機構がこのインタフェースをクラスパスからスキャンし、SpringのBeanを生成できるようすることです。

Springがこのインタフェースを実装したBeanwを作成できるようにするためにすることは、Spring JPA名前空間を使用して適切な要素を用い、レポジトリのサポートを有効にすることだけです。

<jpa:repositories base-package="com.acme.repositories" />

この設定により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<Account> 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<Account, Long> {
 
  List<Account> 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<Account> findByCustomer(Customer customer) {
    return repository.findByCustomer(customer);
  }
}

(訳注:原文にはミスがあったので修正)

ここでちょっとトランザクションハンドリングに関する説明を加えさせてください。とても単純なケースであればレポジトリのCRUDメソッドはトランザクショナルでクエリメソッドに関しては既に@Transactional(readOnly = true)がレポジトリインターフェースに付与されているので、AccountServiceImplクラスから@Transactionalアノテーションを完全に取ってしまえばよいです。今回のセットアップでは、(このケースでは不必要ですが、)サービスレベルのメソッドがトランザクショナルであるとマークされており、トランザクション処理で何が起こるかサービスレベルを見ればはっきりと分かるのでベストである。加えて、サービス層のメソッドが修正されて、レポジトリのメソッドを複数回呼ぶようになった場合でも、レポジトリ層の内部トランザクションが外のサービス層で既に開始されたトランザクションに単純に合流するので、全てのコードが1つのトランザクションで実行されます。レポジトリのトランザクションの振る舞いや変更方法についてはレファレンスドキュメントに詳細が載っています。(訳注:最新版のリンクに変更)

再びテストケースを実行して、動作するか確認してみてください。ここで立ち止まってください。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を追加することもできます。詳細についてはリファレンスドキュメントを参照してください。この機構はGrailsやSpring Rooのようなクエリメソッドプログラミングモデルをもたらします。

次に使いたいクエリを明示的に指定したい場合について考えましょう。JPAのネームドクエリを命名規約にしたがって(今回の場合、Account.findByCustomer)、エンティティ上のアノテーションかorm.xmlに定義する方法とその代わりにレポジトリメソッドに@Queryアノテーションをつける方法があります。

@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {
 
  @Query("<JPQ statement here>")
  List<Account> 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<Customer> findAll() {
    return em.createQuery("select c from Customer c", Customer.class).getResultList();
  }
 
  @Override
  public List<Customer> 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<Customer> 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<Customer, Long> { … }

@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<Customer> findAll() {
    return repository.findAll();
  }
 
  @Override
  public List<Customer> 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<Customer> 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<Customer, Long> {
 
  Page<Customer> findByLastname(String lastname, Pageable pageable);
}

@Override
public Page<Customer> findAll(Pageable pageable) {
  return repository.findAll(pageable);
}
 
@Override
public Page<Customer> 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<Customer, Long> {
 
    Page<Customer> findByLastname(String lastname, Pageable pageable);
}

@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {
 
    List<Account> findByCustomer(Customer customer);
}

<jpa:repositories base-package="com.acme.repositories" />

タイプセーフなCRUDメソッド、クエリ実行、ページネーションを手に入れました。このいけてる仕組みはJPAベースのレポジトリだけでなく非リレーショナルなデータベースにも使えます。最初の非リレーショナルデータベースのサポートとしてMongoDB対応が近日中にリリースされます。(訳注:2011/12時点で1.0.0.RC1までリリースされています)MongoDBに対して全く同じ特徴を使うことができますし、他のデータベースにも対応する予定です。また他にも機能(エンティティの検査や、カスタムデータアクセスとの統合)があるので次の記事で見ていきたいと思います。


✒️️ Edit  ⏰ History  🗑 Delete