[Subsonic Airlines] 会員登録画面 – Java Persistence API

今回は、Java Persistence API(JPA)について説明します。

これまでの記事

http://subsonic.info/ja/category/subsonic-airlines/

ソースコード

https://github.com/subsonicsystems/subsonic-airlines-members

環境
MySQL

JPA
Java EE 7におけるJPAのバージョンは2.1です。JPAは、O/Rマッパーの機能を提供しています。O/Rマッパーによって、JavaオブジェクトとSQLの変換が自動的に行われ、開発者はロジックの作成に集中できます。

モデル
Userクラス(info.subsonic.subsonicairlines.members.model.User)は、ユーザに関するオブジェクトで、フィールドとゲッターとセッターを持つ単純なクラスです。

クラスの前に@Entityを付けると、そのクラスはJPAのエンティティとして扱われます。@Tableは、データベース側の対応テーブル名を指定します。ここでは、name = “user”としているので、JavaのUserクラスは、データベースのuserテーブルに対応しています。

@Entity
@Table(name = "user")
@NamedQueries({ @NamedQuery(name = User.EXISTS_USERNAME, query = "SELECT COUNT(u) FROM User u WHERE LOWER(u.username) = :username") })
public class User {

その次の行の@NamedQueriesは、今後の記事で説明します。

次にidフィールドを見てみます。

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Idは、そのフィールドが主キーであることを宣言します。

@GeneratedValueは、主キーの値の生成方法について定義します。MySQLの場合、strategy = GenerationType.IDENTITYと指定し、データベースの対応カラムにオートインクリメントを指定すると、連番が自動生成されます。

@Columnは、Javaクラスのフィールドとデータベースのカラム名を対応させます。ここでは、name = “id”としているので、Userクラスのidフィールドは、データベースのidカラムに対応しています。

型については、JavaクラスのLongは、データベースのBIGINT(20)に対応します。

次にusernameフィールドを見てみます。

@Column(name = "username", length = 255, unique = true)
@SuppressWarnings("checkstyle:magicnumber")
private String username;

@Columnは、length = 255としています。そして、フィールドの型はStringです。persistence.xmlで<property name=”hibernate.hbm2ddl.auto” value=”create”/>を指定していて、MySQLを使っている場合、デプロイ時にVARCHAR(255)のusernameカラムが生成されます。さらにunique = trueとしているので、データベースのusernameカラムはユニーク制約が付けられます。

ここで注意するのは、@Columnの属性はフォームの検証とは無関係ということです。例えば、フォームに入力された文字列が@Columnで指定したlengthを超えた場合、例外は発生しませんし、エラーメッセージもセットされません。

@SuppressWarnings(“checkstyle:magicnumber”)は、EclipseのCheckstyleのwarningが出ないようにしています。このアノテーションがなくても機能的には問題ありません。ここで抑制しているwarningは、@Columnのlength = 255がマジックナンバーであるという警告です。ここでは、lengthを定数として宣言するよりもフィールドに書いた方がわかりやすいのであえて定数にしないでwarningを抑制しています。

passwordフィールドは、usernameフィールドとほとんど同じです。

createdフィールドとupdateフィールドは、@Temporalが付いています。フィールドの型がjava.util.Date、またはjava.util.Calendarの場合、このアノテーションを付ける必要があります。

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created")
private Calendar created;

そして、TemporalType.TIMESTAMPとすると、このフィールドは、java.sql.Timestampにマッピングされます。TemporalTypeは、次の通りです。

TemporalType マッピングされるクラス
DATE java.sql.Date
TIME java.sql.Time
TIMESTAMP java.sql.Timestamp

createdフィールドとupdateフィールドに対応するデータベースのカラムの型は、DATETIMEです。

次にversionフィールドを見てみます。

versionフィールドは、楽観的ロックを使うために作成します。楽観的ロックとは複数ユーザが同時にアクセスした場合にデータを上書きしない仕組みです。

JPAでは、フィールドに@Versionを付けるとそのフィールドはバージョン用のフィールドとなり、楽観的ロックが有効となります。

@Version
@Column(name = "version")
private Long version;

まず、楽観的ロックがない場合を考えてみます。例えば、ユーザAとユーザBがいて、次のような動作を行います。

1 ユーザAがデータベースからデータを取得する
2 ユーザBがデータベースからユーザAと同じデータを取得する
3 ユーザAが取得したデータを編集して、データベースへ上書き保存する
4 ユーザBが取得したデータを編集して、データベースへ上書き保存する

この場合、4でユーザBがデータを上書き保存してしまうと、ユーザAが編集した内容が消えてしまいますので、そうならないための仕組みが必要です。

楽観的ロックを使うと次のようになります。

1 ユーザAがデータベースからデータ(バージョン1)を取得する
2 ユーザBがデータベースからユーザAと同じデータ(バージョン1)を取得する
3 ユーザAが取得したデータ(バージョン1)を編集して、データベースへ上書き保存を開始する
4 ユーザAの持っているデータのバージョン(バージョン1)とデータベースのデータのバージョン(バージョン1)を比較する
5 バージョンがどちらも同じなので、上書き保存を許可する
6 データベース上のデータのバージョンを2にする
7 ユーザBが取得したデータ(バージョン1)を編集して、データベースへ上書き保存を開始する
8 ユーザBの持っているデータのバージョン(バージョン1)とデータベースのデータのバージョン(バージョン2)を比較する
9 バージョンが異なるため、上書き保存を実行しない
10 ユーザBは、データベースから最新バージョンのデータ(バージョン2)を取得する
11 ユーザBが取得したデータ(バージョン2)を編集して、データベースへ上書き保存を開始する
12 ユーザBの持っているデータのバージョン(バージョン2)とデータベースのバージョン(バージョン2)を比較する
13 バージョンがどちらも同じなので、上書き保存を許可する
14 データベース上のデータのバージョンを3にする

このように楽観的ロックがあると、上書きによるデータの消失を防ぐことが可能になります。

データベースのバージョンのカラムは、オートインクリメントでないことに注意してください。バージョン番号の管理は、コンテナによって行われます。