1. 動機
会社でstruts2フレームワークを使ったWebアプリケーション開発を行っている。このアプリケーションではDBアクセスでJDBCではなくHibernateを使っているが、何も知識がなく困っていた。そんな中、以下の良書に巡り合えたので内容を理解するとともに要点をまとめた。
- Seasar2とHibernateで学ぶデータベースアクセス JPA入門
中村年宏 著、毎日コミュニケーションズ発行
2. 登場するキーワード
- JPA
- JPQL
- O/Rマッピング
- N+1問題
- 遅延ローディング
3. 用語説明
a. O/Rマッピング
Javaオブジェクト(Object)とリレーショナルデータベース(Relational)上のデータを関連付けること。
JPAはO/Rマッピング機能を持つことが大きな特徴。一方、JDBC(※)はオブジェクトとDBの関連付けをプログラミングする必要がある。(プログラムが煩雑になる。)
※JDBC:JavaプログラムからSQLを発行してデータベースへアクセスするためのAPI
b. JPA(Java Persistence API)
Java EE(※)に含まれるAPI。
JPAは仕様でしかなく、JPAを実装したプロダクトが複数存在あり、代表格はHibernate(ハイバネート)、TopLink Essentials、Open JPAの3つである。
環境面:JavaEE, Java SEで使用可能。JDKは5.0以上が必要。
※Java EE :Java SEにサーバーサイド機能を追加したもの。詳細は以下を参照。
- 【Java入門】JavaSEとEEの違いやJRE・JDKを世界一分かりやすく解説
https://www.sejuku.net/blog/12902 - EclipseのJavaとJavaEEの違い
https://qiita.com/kojocho/items/b5d3646012b7a6411c3d
c. Hibernate
米Red Hat社によって提供されるオープンソースのJPA実装。
HibernateをJPA実装として利用するにはHibernate Core、Hibernate Annotations、Hibernate EntityManagerの3プロダクトが必要。
JDBCと比較すると以下の4処理が不要になる。(詳細は後述)
- SQL文
- ResultSetからのデータ詰替え
- リソースのクローズ
- 例外ハンドリング
4. データの取得・更新・追加・削除の実装方法
JPAではO/RマッピングによってテーブルとJavaクラスを対応付け、テーブルの1レコードをJavaクラスの1インスタンスとして扱う。この時。Javaクラスをエンティティクラスと呼び、そのインスタンスをエンティティ と呼び、エンティティはEntityManagerを通して扱う。
EntityManagerはアプリケーションからエンティティの取得要求があると、まず永続コンテキストにキャッシュされているか確認し、キャッシュが存在する場合はキャッシュされたエンティティを返す。
イメージはこんな感じ。
a. データの取得
エンティティの取得方法は主キーによる取得(1)とJPQL(2)による取得の2種類ある。
(1) エンティティによる取得
主キーでエンティティを取得するにはEntityManagerのfindメソッドを使用する。
項目 | 説明 |
---|---|
引数 | 第1引数はエンティティクラス、第2引数は主キーの値を渡す |
戻り値 | エンティティが存在する場合はそのエンティティ。 存在しない場合はnull |
public void findById() throws Exception{
// EntityManager(em)をDIでインジェクトしておく
// Empクラス(テーブル)の主キーが「1」のレコード(エンティティ)を取得
Emp emp = em.find(Emp.class, 1L);
}
(2) JPQLによる取得
主キー指定でエンティティを取得しないケースでは、EntityManagerのfindメソッドが使用できず、JPQLでの実装が必要になる。JPQLはJPA独自の問い合わせ言語であり、SQLとは完全に異なる。
JPQLではEntityManagerのcreateQueryメソッドに引数を渡すことでQueryオブジェクトを作成できる。Queryにはバインド変数のパラメータやページング情報を渡すことができる。
エンティティはQueryのgetResultListメソッド、getSingleResultメソッドを実行することで取得できる。
項目 | 説明 |
---|---|
引数 | jpql(問い合わせ文) |
戻り値 | 条件に合致するレコードがある場合は合致したエンティティリスト 存在しない場合は空のリスト(サイズ=0) |
b. データの更新
更新したいエンティティをEntityManagerで取得し、そのエンティティの状態を変更することでデータを更新できる。EntityManagerが変更前のオリジナルデータを管理しており、適切なタイミングでオリジナルデータと現在のデータを比較し更新を自動で検出する。エンティティが更新されていることを検出すると自動的にUpdate文が発行される。
(この際発行されるUpdate文には自動的に楽観的排他制御で使用しているバージョンカラム(後述)の条件が追加される。)
※JPAではデータ更新を担うメソッドは存在しない
public void updateEntity() throws Exception{
// EntityManager(em)をDIでインジェクトしておく
// Empクラス(テーブル)の主キーが「1」のレコード(エンティティ)を取得
Emp emp = em.find(Emp.class, 1L);
emp.setEmpName("NEWNAME");
// 自動的にUpdate文が発行される
}
c. データの追加
エンティティを追加するには新規に生成したエンティティをEntityManagerのpersistメソッドに渡す。
操作対象のエンティティクラス定義にて主キーレコードに@GeneratedValueアノテーションを指定すると、主キー要素に値を設定せずとも自動設定される。
public void persistEntity() throws Exception{
// EntityManager(em)をDIでインジェクトしておく
// 次のEmpエンティティを生成
Emp emp = new Emp();
emp.setEmpNo (123);
emp.setEnpName("123Name");
em.persist(emp);
// 自動的にInsert文が発行される
}
d. データの削除
エンティティを削除するにはEntityManagerのremoveメソッドに削除したいエンティティを渡す。
EntityManagerは適切なタイミングでSQLのDelete文を発行する。
(この際発行されるDelete文には自動的に楽観的排他制御で使用しているバージョンカラム(後述)の条件が追加される。)
public void removeEntity() throws Exception{
// EntityManager(em)をDIでインジェクトしておく
// 次のエンティティを削除する
em.remove(emp);
// 自動的にDelete文が発行される
}
5. O/Rマッピング
a. アノテーション
O/Rマッピングに必要なアノテーションは以下のとおり。
これらのアノテーションはすべて「javax.persistence」パッケージに属するものである。
@ManyToOneや@OneToMany、@OneToOneのfetch属性は基本的に「fetchType.LAZY」(後述)を指定すること。
No | アノテーション | 指定場所 | 表す意味 |
---|---|---|---|
1 | @Entity | クラス | エンティティであること エンティティ名とはJavaの世界でエンティティを特定するための名称で、JPQL内で使用される 例:@Entity(name = "Hoge")
|
2 | @Id | フィールド | 主キーであること |
3 | @Transient | フィールド | 非永続的であること データベースのカラムと対応付けしたくない場合に付与する |
4 | @GeneratedValue | フィールド | 主キー値を自動生成すること |
5 | @ManyToOne | フィールド | 関連が多対一であること |
6 | @OneToMany | フィールド | 関連が一対多であること |
7 | @OneToOne | フィールド | 関連が一対一であること |
8 | @Table | クラス | マッピング対象のテーブルを明示する |
9 | @Column | フィールド | マッピング対象カラムであることを明示する NOT NULL制約のないカラムはNULLを表現するためにラッパー型を使用しなければならない |
10 | @JoinColumn | フィールド | 結合元のカラムと結合先のカラムを明示する |
11 | @SequenceGenerator | フィールド | 主キーを生成するシーケンスの定義 |
12 | @TableGenerator | フィールド | 主キーを生成するテーブルの定義 |
13 | @Version | フィールド | 楽観的排他制御(後述)を利用するためのバージョン番号のフィールドであること フィールド型はInteger型かLong型にすべき |
b. 遅延ローディング
あるテーブルが親子構成になっている場合、子テーブルを親テーブル取得時に取得せず、必要になったときに(裏側で)取得する仕組み。
@ManyToOneや@OneToMany、@OneToOneのfetch属性はデフォルトが「fetchType.EAGER」になっているため、特段意図がない場合は遅延ローディング「fetchType.LAZY」を使用する。
C. N+1問題
任意のテーブルからN個のレコード(親エンティティ)を取得するためにSELECT文を1回発行、
N個のレコードが関連するデータ(子エンティティ)を取得するためにSELECT文を1回ずつ、計N回発行してしまうこと。意図せず大量のSQLが発行されることになるため注意が必要。
解決方法は以下の2通りある。
- JPQLでJOIN FETCHを使用する
- @Fetchアノテーションを使用する
参考
6. JPQL
JPQLはJPA実装によって解釈され、使用するデータベースに適したSQLに変換されてから実行される。SQLはデータベース毎に方言を持つが、JPQLを使用することで方言による差異を多少吸収できる。
a. 文法と用語
(1) エンティティ名と識別変数
JPQLのFROM区はエンティティ名(≠テーブル名)を指定する。
識別変数(以下SQLでは”e”)はエンティティ名の後ろに宣言し、それを使用して抽出項目等を指定する。
※エンティティ名は大文字小文字が区別される
select e from Employee e;
(2) フェッチ結合
遅延ローディングが適用されると、子エンティティは必要になったときにしか取得されない。一方、最初から子エンティティも必要と分かっている場合には1つのSQLで同時に取得する方が効率的である。そのような場合にフェッチ結合と使用する。
フェッチ結合を使用したSQLは以下のとおり。
select distinct d d from Department d inner join fetch d.employees where d.name = "ABC";
7. エンティティのライフサイクル
a. エンティティの4つの状態
エンティティは取得されると永続コンテキストで管理される。永続コンテキスト内では以下の4つの状態に分かれて管理されている。これらの状態はEntityManagerのメソッド実行や永続コンテキストの終了などによって変化し、エンティティのライフサイクルと呼ばれる。図示すると以下のとおり。
-
新規の(new)
新規のエンティティはEntityManagerのpersistメソッドやmergeメソッドを仕様しない限り、新規のエンティティのまま。 -
管理された(managed)
エンティティをEntityManagerのfindメソッドやJPQLを使って取得した場合、エンティティをEntityManagerのpersistメソッドがmergeメソッドに渡した場合、エンティティは永続コンテキストに管理される。管理されたエンティティに対する永続フィールドの変更は自動で検出される。 -
分離された(detached)
分離されたエンティティに対して変更処理をしても、EntityManagerのmergeメソッドでエンティティを再び管理された状態にしない限り、変更は永続コンテキストに反映されない。 -
削除された(removed)
管理されたエンティティをEntityManagerのremoveメソッドに渡すと、永続コンテキストから削除される。
b. データベースの同期化
EntityManagerは管理されたエンティティと削除されたエンティティをデータベースに反映させる。この処理をデータベースの同期化という。同期化により、永続コンテキストに蓄積されたエンティティの変更情報がデータベースに書き込まれる。
データベースの同期化が行われるデフォルトのタイミングは以下のとおり。
- EntityManagerのflushメソッドが呼ばれた時
- トランザクションがコミットされる直前
- 問い合わせを実行する直前
8. JPAの楽観的排他制御
排他制御には特定のデータをロックし、他のトランザクションからのCRUDを禁止する悲観的排他制御と、データをロックせず、更新が衝突した場合に他方の処理を失敗させる楽観的排他制御がある。
JPAではデータベースにバージョン番号(@Version付与)を管理する整数型カラムを用意することで、このバージョン番号をもとに楽観的排他制御を実現できる。
※楽観的排他制御は同一のデータが同時に更新される頻度が少ないことを前提に使用される方法
*番外編*知っていると便利なこと
-
Hibernateで発行されたSQLをログで確認したい
→persistence.xml(※)のhibernate.show_sqlプロパティに"true"を設定する。<property name = "hibernate.show_sql" value = "true" />
-
Hibernateで発行されたログ内のSQLを整形して表示したい
→persistence.xml(※)のhibernate.format_sqlプロパティに"true"を設定する。<property name = "hibernate.format_sql" value = "true" />
※データソース名やトランザクションのタイプなどJPA共通の設定やJPA実装独自の設定を定義