本連載では、Java Platform, Enterprise Edition 6 (Java EE 6) を実際の開発に活用していただくための情報をお届けしています。今回も前回から引き続き Java Persistence API (JPA) を取り上げます。前回は、JPAの基礎に関してご紹介しました。今回は、アプリケーションの排他制御及びJPAで使用可能なクエリに関してご紹介します。 |
作成するアプリケーション
今回の記事も、前回に引き続きOracle Database Express Edition 11g Release 2を使用したJPAのアプリケーションを作成します。開発には Oracle Enterprise Pack for Eclipse (OEPE) を使用します。 OEPE や WebLogic Server 12c のインストールおよび設定が終わっていない方は、本連載の「[連載] WebLogic Server 12cでJava EE 6を動かしてみよう!(1) 概要」を参考にしてください。前回は、EMPLOYEES表に対する主キーによる参照、挿入、更新、削除を行うことのできるシンプルなアプリケーションを作成しました。アプリケーションの詳細やOracle Database Express Edition 11g Release 2のインストール及び設定に関しては前回の記事を参照してください。今回は、アプリケーションに下記の機能を追加します。
- 更新の同時実行時にデータの整合性を担保するためのロック機能
- JPQL等のクエリを用いた名前検索機能
同時実行と排他制御
一般的に複数のユーザが同時に同一のデータにアクセスし、更新を行うアプリケーションの場合、同時実行性を実現し、なお且つデータの整合性が損なわれないように考慮しなくてはなりません。JPAは、アプリケーションの上記要件を満たすため、処理対象のデータに対して排他制御(ロック)を行うことができます。
JPAが提供しているロックの種類は大きく2種類あり、楽観的ロックと悲観的ロックをサポートします。
楽観的ロックは、「同時に同一データにアクセスするユーザはいないだろう」と、楽観的な考えに基づきます。よって、複数のトランザクションが同一データを操作後に更新可能かどうかを最後に確認します。既に、他のトランザクションによって更新が行われた場合は、変更をロールバックします。
一方、悲観的ロックは、「同時に同一データにアクセスするユーザがいるだろう」という悲観的な考えに基づきます。トランザクションの最初で、更新対象のデータをロックし他のトランザクションからのデータアクセスを更新処理が終わるまでブロックします。
楽観的ロック
最初に、バージョニングを使用した楽観的ロックを下記の手順で実装します。
- EMPLOYEESテーブルに、レコード毎のバージョンを示すカラムの追加
- エンティティの再作成
- JSFの修正
EMPLOYEESテーブルのカラム追加
SQLコマンド・プロンプト・ウィンドウを表示し、下記のSQLを実行します。
Windowsでは、「スタート」、「プログラム」(または「すべてのプログラム」)、「Oracle Database 11g Express Edition」、「SQLコマンドラインの実行」の順にクリックします。
ALTER table Employees ADD vernum NUMBER(4) default 1 not null;
EMPLOYEES表に「vernum」カラムを追加します。本カラムは、レコードのバージョンを示し、更新を契機に値が増えます。初期値は1で、NULLは許容されません。
SQLの実行に成功すると、下記のようなカラム構成となります。
エンティティの再作成
前回作成したエンティティ Employeeを下記の手順で再作成します。Customize Default Entity Generation画面までは前回と手順は同様ですので、詳細は前回記事「[連載] WebLogic Server 12cでJava EE 6を動かしてみよう!(4) JPA 第1回」の「エンティティの作成」の章を参照してください。
- OEPEのJPAプロジェクトを選択し、右クリック「JPA Tools」―「Generate Entities from Tables…」を選択します。
- Select Table画面にて、XE11gとの接続設定を選択し、Schemaに「HR」を選択し、Tablesから「EMPLOYEES」にチェックをいれます。「Next」ボタンをクリックします。
- Table Associations画面では、デフォルトのまま「Next」ボタンをクリックします。
- Customize Default Entity Generation画面で、Packageに「model」を入力し、「Next」ボタンをクリックします。
- Customize Individual Entities画面で「Table and Columns」内のツリーから「vernum」カラムを選択し、以下の項目を設定し、「Finish」ボタンをクリックします。
- Mapping type:「int」
- Mapping kind:「version」
既存のEmployee.javaを上書きするか否かを問われますので、「Yes」をクリックします。
以下のファイルをダブルクリック等で開き、ファイルの内容を確認します。
package model; import java.io.Serializable; import javax.persistence.*; import java.math.BigDecimal; import java.util.Date; import java.util.Set; @Entity @Table(name="EMPLOYEES") public class Employee implements Serializable { // ...中略 @Version private int vernum; // ...中略 public int getVernum() { return this.vernum; } public void setVernum(int vernum) { this.vernum = vernum; } }
@Versionアノテーションが付与された属性は、更新処理が行われた際に、楽観的ロックのためのバージョン管理に使用されます。今回は、数値型の属性に@Versionアノテーションを付与しましたが、日付・時間型に付与することもできます。
@Versionアノテーションがサポートする型は、int, Integer, short, Short, long, Long, java.sql.Timestamp です。
JSFの修正
楽観的ロック機能を確認するために、以下のファイルを修正します。
一部省略<h:outputText value="JOB ID" /><h:inputText value="#{indexBean.dispEmp.jobId}" /><!-- 追加1ここから --><!-- 現在のエンティティのバージョンを表示します --><h:outputText value="VERSION" /><h:outputText value="#{indexBean.dispEmp.vernum}" /><!-- 追加1ここまで --></h:panelGrid><!-- 追加2ここから --><h:inputHidden value="#{indexBean.dispEmp.vernum}" /><h:messages style="color:red"/><!-- 追加 2ここまで--><h:message for="empid" />一部省略
public void actionUpdateEmp() { /* 追加1 ここから */ try{ /* 追加1 ここまで */ empLogic.updateEmp(this.dispEmp); /* 追加2 ここから */ } catch (EJBException e) { // 更新できなかった場合、エラーメッセージを表示 if (e.getCause() instanceof OptimisticLockException) { FacesContext fc = FacesContext.getCurrentInstance(); fc.addMessage("msg", new FacesMessage("Employee cannot be merged because it has changed or been deleted since it was last read.")); }else{ throw e; } } /* 追加2 ここまで */ }
動作確認
WebContent/index.xhtmlを選択し、右クリック「Run as」―「Run on Server」を選択します。
ブラウザを二つ起動し、同一レコードを両方のブラウザから「UPDATE」ボタンをクリックし更新すると、片方の更新は成功し、もう一方の更新は失敗することを確認することができます。
悲観的ロック
悲観的ロックの実装は、明示的にエンティティマネージャのlockメソッドを呼び出して実現します。
以下のファイルをダブルクリック等で開き、下記のメソッドを追加します。
追加するメソッドaddSalaryは、対象のエンティティの給与を、300昇給させるビジネスロジックです。
public Employee addSalary(long empId){ // 従業員情報の取得 Employee emp = em.find(Employee.class, empId); // 悲観的ロック em.lock(emp, LockModeType.PESSIMISTIC_READ); // 給与を増やす emp.setSalary(emp.getSalary().add(BigDecimal.valueOf(300L))); // 5秒スリープを入れる try {Thread.sleep(5000L); } catch (InterruptedException e) {e.printStackTrace();} return emp; }
メソッドが一瞬で終わってしまうと、ロックがすぐに解除されてしまいますので、メソッドの最後にスリープ処理を追加し、トランザクションの時間を長くします。上記は、lockメソッドで悲観的ロックをかけますが、findメソッド(参照処理)と同時に、ロックをかけることもできます。以下にコード例を記載します。
// find等で更新対象のエンティティを取得と同時にロックする Employee emp = em.find(Employee.class, empId,LockModeType.PESSIMISTIC_READ);
ロック対象のエンティティは、管理状態である必要があります。デタッチ状態のエンティティをロックした場合は例外が発生します。
JSFの修正
悲観的ロック機能を確認するために、以下のファイルを修正します。
一部省略<h:outputText value="JOB ID" /><h:inputText value="#{indexBean.dispEmp.jobId}" /><!--追加①ここから--><h:outputText value="SALARY" /><h:inputText value="#{indexBean.dispEmp.salary}" /><!--追加①ここまで--><h:outputText value="VERSION" /><h:outputText value="#{indexBean.dispEmp.vernum}" /></h:panelGrid><h:inputHidden value="#{indexBean.dispEmp.vernum}" /><h:messages style="color:red"/><h:message for="empid" /><br /><h:commandButton value="GET" action="#{indexBean.actionGetEmp}" /><h:commandButton value="GET(REFRESH)" action="#{indexBean.actionGetEmpRefresh}" /><br /><h:commandButton value="CREATE" action="#{indexBean.actionCreateEmp}" /><h:commandButton value="UPDATE" action="#{indexBean.actionUpdateEmp}" /><h:commandButton value="REMOVE" action="#{indexBean.actionRemoveEmp}" /><h:commandButton value="CLEAR" action="#{indexBean.actionClearEmp}" /><!-- 追加②ここから --><br /><h:commandButton value="ADD SALARY" action="#{indexBean.actionAddSalary}" /><!-- 追加②ここまで -->一部省略
下記のメソッドを追加します。
public void actionAddSalary() { dispEmp = empLogic.addSalary(dispEmp.getEmployeeId()); }
動作確認
WebContent/index.xhtmlを選択し、右クリック「Run as」―「Run on Server」を選択します。
ブラウザを二つ起動し、同一レコードを両方のブラウザから「ADD SALARY」ボタンをクリックし更新すると、どちらの給与加算処理も正常に行われ、SALARYの値が600増加することが確認できます。
クエリ
前回の記事では、エンティティマネージャの提供するAPIを使用し、エンティティの主キーの検索、挿入、更新、削除が少ないコードで簡単にできることを紹介しました。しかし、実際の開発では、主キー以外の様々な条件を指定した検索を行うこともありますし、エンティティを1つ1つ更新するのではなく、条件を指定して一括で更新(または、削除)するケースもあります。
このような場合、エンティティマネージャのfindメソッドや、merge、removeメソッドだけでは、実現することができません。
以上のような要件を満たすために、JPAではSQLライクなクエリ言語であるJPQLを提供します。
JPQLは、データベースのベンダに依存しないクエリ言語ですので、JPQLで構築したアプリケーションは移植性が高くなります。さらに、SQLと非常によく似ているため、開発者にとってなじみやすく習得までに時間がかかりません。
また、本日は取り上げませんが、ネイティブクエリを使用することができます。これはJPQLではなく、ベンダ依存のSQLクエリをそのまま使用することをサポートするものです。
JPQLクエリの追加
EmployeeエンティティにJPQLクエリを追加します。
追加するクエリは、指定した文字列が、LASTNAMEもしくはFIRSTNAMEに部分一致するエンティティを取得します。
package model; // 中略 @Entity @Table(name="EMPLOYEES") @NamedQueries({ @NamedQuery(name="Employee.selectByName", query="select e from Employee e where e.lastName like :word or e.firstName like :word") }) public class Employee implements Serializable { // 以下略
@NamedQueryのname属性で、このクエリの名前を指定します。query属性に、実際のJPQLを記述します。パラメータはコロン(:)を先頭に付与して定義します。上記のクエリを呼び出すためのメソッドを下記ファイルに追加します。
public ListgetEmpByName(String searchWord) { // エンティティマネージャから、定義済みの名前付きクエリを生成し // パラメータを設定し、クエリ結果をList型で受け取ります return em.createNamedQuery("Employee.selectByName") .setParameter("word", new StringBuilder("%").append(searchWord).append("%") .toString()).getResultList(); }
CriteriaAPI
CriteriaAPIは、JPQLクエリのような文字列ではなく、APIを使いプログラミングでクエリを作成します。 CriteriaAPIを使用するメリットは、文字列でクエリを表現する場合と比較してタイプミスを削減することができることです。例えば、文字列でクエリを表現した際に、「SELECT」を「SELCT」と記述ミスしても、Javaのコンパイラはこの間違いを検知することができず、当該処理を実行して初めて間違いに気付きます。CriteriaAPIを使用することで、クエリの初歩的な記述ミスを早期に解決することができます。
また、動的なクエリの作成をシンプルに行うことができます。ユーザの入力した検索条件に応じて、動的にWHERE句を構築する場合、文字列でクエリ文を組み立てると、とても複雑な処理となります。しかしCriteriaAPIを使用することで、処理の記述がシンプルになり、検索条件の追加などにも対応しやすくなります。
前述した、文字列表現のJPQLをCriteriaAPIに置き換えます。下記のファイルにメソッドを追加します。
public ListgetEmpByNameCriteriaAPI(String searchWord) { // 検索条件の文字列を作成 String param = new StringBuilder("%").append(searchWord).append("%") .toString(); CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(Employee.class); Root e = query.from(Employee.class); // 検索条件をCriteriaAPIを使用して構築 query.select(e).where( builder.or(builder.like(e. get("firstName"), param), builder.like(e. get("lastName"), param))); // クエリの実行と結果を返却 return em.createQuery(query).getResultList(); }
JSFの作成
上記の検索クエリを呼び出し、検索結果を表示するための画面を作成します。
以下のファイルを作成してください。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"><h:head><title>JPA</title></h:head><h:body><h:form><h:inputText value="#{searchBean.searchWord}" /><h:commandButton value="SEARCH" action="#{searchBean.actionGetEmpByName}" /><h:commandButton value="SEARCH(CriteriaAPI)" action="#{searchBean.actionGetEmpByNameCriteriaAPI}" /><h:dataTable value="#{searchBean.searchResult}" var="emp" border="1"><h:column><f:facet name="header"><h:outputText value="ID" /></f:facet><h:outputText value="#{emp.employeeId}" /></h:column><h:column><f:facet name="header"><h:outputText value="FIRST NAME" /></f:facet><h:outputText value="#{emp.firstName}" /></h:column><h:column><f:facet name="header"><h:outputText value="LAST NAME" /></f:facet><h:outputText value="#{emp.lastName}" /></h:column><h:column><f:facet name="header"><h:outputText value="JOB ID" /></f:facet><h:outputText value="#{emp.jobId}" /></h:column></h:dataTable></h:form></h:body></html>
package managed; import java.util.ArrayList; import java.util.List; import javax.ejb.EJB; import javax.faces.bean.ManagedBean; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import ejb.EmpLogic; import model.Employee; @ManagedBean public class SearchBean { @EJB private EmpLogic empLogic; /** 検索結果の従業員データ格納用 */ private ListsearchResult = new ArrayList (); /** 検索文字列 */ private String searchWord; public List getSearchResult() { return searchResult; } public void setSearchResult(List searchResult) { this.searchResult = searchResult; } public String getSearchWord() { return searchWord; } public void setSearchWord(String searchWord) { this.searchWord = searchWord; } public void actionGetEmpByName() { searchResult = empLogic.getEmpByName(searchWord); } public void actionGetEmpByNameCriteriaAPI() { searchResult = empLogic.getEmpByNameCriteriaAPI(searchWord); } }
WebContent/search.xhtmlを選択し、右クリック「Run as」―「Run on Server」を選択します。下記の画面がブラウザ上に表示されます。
テキストフィールドに、検索キーワードを入力し、「SEARCH」もしくは「SEARCH(CriteriaAPI)」ボタンをクリックします。FIRST NAMEもしくは、LAST NAMEに部分一致するエンティティが一覧表示されることが分かります。
SQLライクなクエリ文を作ることにより、JPAでも柔軟なクエリを作成できることが分かりました。JPQLでは、MAX、SUMなどの関数や、GROUP BY によるグループ化、ORDER BYによるソートを行うこともできます。
まとめ
本連載では2回にわたり、JPA の基本的な機能について説明しきました。JPAは今回紹介した機能以外にも、エンティティの関連やキャッシュAPI等の機能があります。本日取り上げなかった機能等に関して興味のある方は、JPAの仕様書「Java Persistence API, Version 2.0」を参照してみてください。
次回は、Java EE 6から新規に仲間に加わった新しいWebサービスのAPIであるJAX-RSをとりあげます。お楽しみに。