Spring Dataは、異なるデータストアに対するアクセスの統一化と簡易化を目的とするSpringSourceのプロジェクトであり、リレーショナルデータベースとNoSQLの両方をサポートします。
どのようなデータソースに対しても、通常、リポジトリ(あるいはDAO、データアクセスオブジェクト)はドメインオブジェクトに対するCRUD(Create-Read-Update-Delete )操作やfinderメソッド、ソート処理やページ付け処理を提供します。Spring Dataが提供するのはこのような処理の汎用インターフェイス(CrudRepository、PagingAndSortingRepository)です。また、データストアに特有の実装も提供します。
読者の皆さんはSpringのテンプレートオブジェクト(JdbcTemplateのような)を使ってリポジトリの実装をしたことがあると思います。確かにテンプレートオブジェクトは強力です。しかし、もっと改善できるはずです。Spring Dataのリポジトリを塚エアある一定の規約(この規約は利用するデータストアに依存します)に従うfinderメソッドのインターフェイスを書くだけです。Spring Dataは実行時にそのインターフェイスの適切な実装を利用します。例えば次の通りです。
public interface UserRepository extends MongoRepository |
この記事では、JPA、MongoDB、Neo4jの3つのサブプロジェクトを比較します。JPAはJEEスタックの一部でリレーショナルデータベースに対する統一的なアクセス方法を提供し、O/Rマッピングを実現します。MongoDBはスケーラブルで、高パフォーマンスなオープンソースのドキュメント指向データベースです。Neo4jはグラフデータベースで、完全にトランザクションをサポートしグラフ構造を保存できます。
Spring Dataは下記をサポートします。
- テンプレートの利用
- オブジェクト/データストアのマッピング
- リポジトリのサポート
Spring Data RedisやSpring Data Riakのような他のSpring Dataプロジェクトはひとつのテンプレートしか提供しません。対応するデータストアがマップ処理や問い合わせを受け付けない非構造化データを保存するからです。
テンプレート、オブジェクトマッピング、リポジトリのサポートについて詳細を見てみましょう。
テンプレート
Spring Dataのテンプレート(そしてその他のSpringのテンプレート)の目的はリソースの確保と例外の転換です。
データストアはTCP/IPでリモートからアクセスされるのが普通です。次の例はMongoDBのテンプレートを使う場合の構成です。
host="localhost" port="27017" dbname="test" /> |
まず、コネクションのファクトリを定義します。この場合はMongoTemplateを参照しています。MongoDBの場合、Spring DataプロジェクトはMongoDB Javaドライバに依存します。
一般的にはこのような低レベルのデータストアAPIは独自の例外ハンドリング戦略を持っています。Springの場合、未チェックの例外を使い、開発者がキャッチできるようにします。テンプレートの実装は低レベルの例外をキャッチして、対応するSpringの例外として再スローします。この再スローされる例外はSpringのDataAccessExceptionのサブクラスです。
テンプレートは保存や更新、削除や問い合わせやmap/reduceジョブの実行など、データストアに固有の処理を実行します。しかし、これらの処理は対応するデータストアでのみ実行できます。
Spring Data JPAはテンプレートを提供しません。JPAの実装自体がJDBC APIのトップに位置する抽象レイヤだからです。JPAのEntityManagerがテンプレートに対応します。例外はリポジトリの実装によってハンドルされます。
オブジェクト/データストアのマッピング
JPAはO/Rマッピング(オブジェクトのグラフとリレーショナルデータベースのテーブルをマップする)の標準を提供します。JPA仕様を実装したO/Rマッパーで最も一般的なのはおそらくHibernateでしょう。
Spring Dataでは、O/Rマッパーは拡張され、オブジェクトのようなデータ構造を持つNoSQLに対応しています。しかし、データ構造がかなり違う可能性があるので、O/Rマッピングのための共通のAPIを作るのは難しいです。そのため、それぞれのデータストア用のアノテーションが用意され、マッピングのためのメタ情報が提供されます。次の例は、シンプルなドメインオブジェクトUserをデータストアのマッピングする場合です。
JPA |
MongoDB |
Neo4j |
@Entity @Table(name="TUSR") public class User { @Id private String id; @Column(name="fn") private String name; private Date lastLogin; ... } |
@Document( collection="usr") public class User { @Id private String id; @Field("fn") private String name; private Date lastLogin; ... } |
@NodeEntity public class User { @GraphId Long id; private String name; private Date lastLogin; ... } |
JPAのエンティティに慣れているなら、標準的なJPAのアノテーションがあるのがわかるでしょう。Spring Dataはこのアノテーションを再利用します。その他のアノテーションは利用されません。マッピングそのものはJPAの実装が行います。MongoDBとNeo4jは同じようなアノテーションを必要とします。あるクラスをコレクション(MongoDBのコレクションはリレーショナルデータベースのテーブルのようなものです)やノード(ノードとエッジはNeo4jのようなグラフデータベースの主要な型です)にマッピングするためのクラスレベルのアノテーションがあります。
JPAのエンティティには一意な識別子が必要です。MongoDBのドキュメントとNeo4jのノードも同様です。
MongoDBは@Idアノテーション(JPAと同じではありません。パッケージはorg.springframework.data.annotationです)で、Neo4jは@GraphIdです。これらの属性が設定されるのはドメインオブジェクトが永続化されてからです。MongoDBのドキュメントの属性名がJavaの属性とマッチしない場合、その他の永続化属性用に@Fieldアノテーションを使います。
他のオブジェクトへの参照もサポートされています。例えばはユーザのロールは次のように永続化されます。
JPA |
MongoDB |
Neo4j |
@OneToMany private List |
private List |
@RelatedTo( type = "has", direction = Direction.OUTGOING) private List |
JPAでは@OneToManyを使います。nの側がもう一方のテーブルに保存されており、通常はジョインで取得されます。MongoDBはコレクション間のジョインがサポートされていません。デフォルトでは参照オブジェクトは同じドキュメント内に保存されます。他のドキュメントへの参照を保持できますが、これはクライアントサイドでのジョインになります。Neo4jの場合、関連はエッジとして表現されます。エッジはNeo4jの基本データ型のひとつです。
まとめると、MongoDBとNeo4jはJPAのO/Rマッピングと同じようなオブジェクトマッピングを使いますが、JPAと完全に同じではありません。データ構造が異なるからです。しかし、Javaのオブジェクトをデータストアのデータ構造にマッピングするという考え方は一緒です。
リポジトリサポート
ビジネスアプリケーションでデータを永続化したことがあるなら、ある種のDAOオブジェクトを書いたことがあるでしょう。普通、単一のレコードに対するCRUD操作やfinderメソッドなどが定義されます。finderメソッドのパラメータはクエリに設定されます。
JPAの場合、CRUD操作はEntityManagerインターフェイスを通じて実行されます。しかし、カスタムのfinderメソッドを書くのは面倒です。次のように名前付きのクエリとパラメータを定義し、クエリを実行する必要があります。
@Entity @NamedQuery( name="myQuery", query = "SELECT u FROM User u where u.name = :name" ) public class User { ... } @Repository public class ClassicUserRepository { @PersistenceContext EntityManager em; public List<User> findByName(String Name) { TypedQuery<User> q = getEntityManger().createNamedQuery("myQuery", User.class); q.setParameter("name", fullName); return q.getResultList(); } ... |
この面倒さはTypedQueryの流れるようなインターフェースを使うことで多少軽減できます。
@Repository public class ClassicUserRepository { @PersistenceContext EntityManager em; public List<User> findByName(String name) { return getEntityManger().createNamedQuery("myQuery", User.class) .setParameter("name", fullName) .getResultList(); } ... |
... しかし、セッターと呼び出し、クエリを実行するメソッドを書かなかればなりません。Spring Data JPAを使えば、同じ問い合わせを次のように書けます。
package repositories; public interface UserRepository extends JpaRepository<User, String> { List<User> findByName(String name); } |
Spring Data JPAの場合、JPQLクエリは対応するJAPエンティティのクラスファイルに@NamedQuerysを宣言する必要はありません。代わりにリポジトリメソッドのアノテーションにクエリを記述します。
@Transactional(timeout = 2, propagation = Propagation.REQUIRED) @Query("SELECT u FROM User u WHERE u.name = 'User 3'") List<User> findByGivenQuery(); |
Spring Data MongoDBとSpring Data Neo4jでも同様です。次の例は、Cipherを使ったNeo4jへの問い合わせです。
public interface UserRepository extends GraphRepository<User> { User findByLogin(String login); @Query("START root=node:User(login = 'root') MATCH root-[:knows]->friends RETURN friends") List<User> findFriendsOfRoot(); } |
もちろん、finderメソッドの命名規則はデータストアごとに違います。例えば、MongoDBは地理空間問い合わせをサポートします。下記のような書き方です。
public interface LocationRepository extends MongoRepository |
finderメソッドに特別なパラメータを与えることで、ページングやソート処理も実行できます。
リポジトリの利点は、
- 開発者は定型的なコードを書く必要がなくなる
- クエリがfinderメソッドの近くに定義できる
- さらに、JPQLクエリはSpringのコンテキストが収集されると同時にコンパイルされるので、構文エラーが見つけやすい
まとめ
この記事では複雑な技術を概観し、相違点を明らかにしようとしました。Spring Dataプロジェクトについてさらに詳しく知りたい場合は、プロジェクトのホームページを参照するといいでしょう。
この記事のタイトルの答えはノーです。すべてのデータストアに対応する汎用APIは存在しません。根本的な違いがあるからです。しかし、Spring Dataプロジェクトはデータにアクセスするための共通のプログラミングモデルを提供します。
リポジトリを使うことで物理データ層を抽象化し、finderメソッドを書きやすくします。オブジェクトマッピングを使い、ドメインオブジェクトをデータストアの型に変換します。テンプレートを使うことで特定のデータストアに対する低レベルのアクセスが実現できます。
著者について
Tobias Trelle氏はcodecentric AGのシニアITコンサルタントであり、ITビジネスでの15年間の経験があります。ソフトウエアの設計、EAI、クラウドコンピューティングに興味を持っています。ブログを書き、トレーニングやカンファレンスでの講演も行います。
Twitter
ブログ
Linked In
G+