JakartaEE(旧称:JavaEE)では、JSF(JavaServer Faces)というJavaベースのWebアプリケーションフレームワークに加え、JPA(Java Persistence API)という、データの永続的な保管や取り出しを容易にする機能も利用することができる。
今回は、Eclipse上のJSFプロジェクトでJPAを利用して、データベース(Oracle)にアクセスしてみたので、その手順とサンプルプログラムを共有する。
前提条件
以下の記事の実装が完了していること。

Oracle XEのインストールが完了し、以下のUSER_DATAテーブルが作成済であること。
desc USER_DATA

やってみたこと
JDBCドライバの配置
JPAでデータベースに接続するためには、OracleのJDBCドライバー(jar)を取得し、GlassFishサーバーとJSFプロジェクトに配置する必要がある。その手順は、以下の通り。
1) 以下のサイトにアクセスし、OracleのJDBCドライバー(jar)を取得する。なお、今回はJDK6~8で動作するバージョンのJDBCドライバーを取得している。
 https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc6/11.2.0.4

2) 取得したOracleのJDBCドライバー(jar)を、「(GlassFishサーバーのホームディレクトリ)\domains\domain1\lib」下に配置する。
 
3) JSFプロジェクトを作成済のEclipseを起動し、プロジェクトを選択し右クリックし、「ビルド・パス」から「外部アーカイブの追加」を選択する。
 
4) 2)で配置したOracleのJDBCドライバー(jar)を選択し、「開く」ボタンを押下する。
 
5) 以下のように、「参照ライブラリー」に指定したOracleのJDBCドライバー(jar)が追加されていることが確認できる。
 
JDBC Connection Poolsの設定
JPAによって、データベースにアクセスするためのJNDIを利用するには、あらかじめJDBC Connection Poolsを作成しておく必要がある。その手順は、以下の通り。
1) JSFプロジェクトを作成済のEclipse上で、GlassFishサーバーを起動する。
 
2) URL「http://localhost:4848/」にアクセスし、GlassFishサーバーの管理コンソール画面を起動する。
 
3)「JDBC Connection Pools」を開き、「New」ボタンを押下する。
 
4)「Pool Name」に任意の名前、「Resource Type」に「javax.sql.DataSource」、「Database Driver Vendor」に「Oracle」を指定し、「Next」ボタンを押下する。
 
5) 赤枠部分のプロパティ(上から順に、ユーザー名・データベース名・URL・パスワード)の値を設定後、「Finish」ボタンを押下する。
 
6) JDBC Connection Poolsの設定が完了し、以下のように、一覧に追加した「OraclePool」が表示されることが確認できる。ここで接続確認のため、「OraclePool」リンクを押下する。
 
7) 接続設定は「Ping」ボタンで確認できる。接続できていれば、以下のように、「Ping」ボタンを押下すると「Ping Successed」と表示される。
 
 
 
JNDIの設定
JPAによってデータベースにアクセスするためのJNDIは、以下の手順で作成する。
1) GlassFishサーバーの管理コンソール画面上で、「JDBC Resources」を選択後、「New」ボタンを押下する。
 
2)「JNDI Name」に任意の名前、「Pool Name」に先ほどのJDBC Connection Poolsを設定後、「OK」ボタンを押下する。なお、JNDI名(JNDI Name)は、慣習として「jdbc/」で始まる文字列を使用することが多いようだ。
 
3) JNDIの追加が完了し、以下のように、一覧に追加した「jdbc/Oracle」が表示されることが確認できる。
 
 
 
JPAプロジェクトへの変換
JSFプロジェクトでJPAによるデータベース接続が行えるようにするため、JPAプロジェクトに変換する。その手順は、以下の通り。
1) JSFプロジェクトを作成済のEclipse上で、JSFプロジェクトを選択し右クリックし、「構成」から「JPAプロジェクトへ変換」メニューを押下する。
 
2) 以下の画面が起動するため、そのまま「次へ」ボタンを押下する。
 
4) 以下のように、DB接続定義を行う「persistence.xml」が追加されたことが確認できる。
 
5) 上記「persistence.xml」を以下のように編集し、先ほど作成したJNDIによるデータベース接続設定を追加する。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="demoJsf">
        <jta-data-source>jdbc/Oracle</jta-data-source>
    </persistence-unit>
</persistence>サンプルプログラムの作成
作成したサンプルプログラムの構成は、以下の通り。
 
 なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。また、DB接続定義(persistence.xml)の内容は、前項「JPAプロジェクトへの変換」で記載した通りとなる。
USER_DATAテーブルのエンティティクラスの内容は以下の通りで、USER_DATAテーブルの各項目を定義している。
package jpa;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
/**
 * USER_DATAテーブルのエンティティ.
 */
@Entity
@Table(name="user_data")
@Data
public class UserData implements Serializable {
    // シリアルバージョンUID
    private static final long serialVersionUID = -5319751026833041042L;
    /** ID */
    @Id
    private Integer id;
    /** 名前 */
    private String name;
    /** 生年月日_年 */
    @Column(name = "birth_year")
    private Integer birthYear;
    /** 生年月日_月 */
    @Column(name = "birth_month")
    private Integer birthMonth;
    /** 生年月日_日 */
    @Column(name = "birth_day")
    private Integer birthDay;
    /** 性別 */
    private String sex;
    /** メモ */
    private String memo;
}また、USER_DATAテーブルにアクセスするクラスの内容は以下の通りで、USER_DATAテーブルにデータを1件追加する処理を記載している。
package jpa;
import javax.enterprise.context.RequestScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
/**
 * USER_DATAテーブルへアクセスするJPA.
 */
@RequestScoped
public class UserDataJpa {
    @PersistenceContext
    private EntityManager em;
    /**
     * USER_DATAテーブルに引数のデータを追加する
     * @param userData
     */
    @Transactional
    public void regist(UserData userData){
        // UserDataテーブルのid最大値を取得
        Integer maxId = em.createQuery("SELECT MAX(u.id) FROM UserData u"
               , Integer.class).getSingleResult();
        // id最大値がNULLの場合は、0に変換
        maxId = (maxId == null) ? 0 : maxId;
        // USER_DATAテーブルのIDを設定後、登録処理を実行
        userData.setId(maxId + 1);
        em.persist(userData);
        em.flush();
    }
}さらに、各画面のForm値と画面遷移を定義したクラスの内容は以下の通りで、完了画面に遷移するsendメソッド内で、USER_DATAテーブルにデータを1件追加する処理を呼び出している。
package faces;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.SessionScoped;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.event.ComponentSystemEvent;
import javax.faces.model.SelectItem;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
import common.CommonUtil;
import jpa.UserData;
import jpa.UserDataJpa;
import lombok.Data;
import lombok.ToString;
/**
 * 画面のフォーム値と画面遷移メソッドを定義.
 */
// @Namedアノテーションは、JSFのXHTMLファイルから#{inputFormAction}で
// Javaクラスを参照できるようにしている(→バッキングビーン)
// @SessionScopedアノテーションは、このバッキングビーンの生存期間を
// セッションに設定している
@Named(value="inputFormAction")
@SessionScoped
// 以下はLombokのアノテーション
//「@Data」アノテーションを付与すると、このクラス内の全フィールドに対する
// Getterメソッド・Setterメソッドにアクセスができる
@Data
//「@ToString」アノテーションを用いて、exclude属性で指定した項目以外の値を、
// toStringメソッド呼出時に出力することができる
@ToString(exclude={"birthMonthItems","birthDayItems","sexItems"})
public class InputFormAction implements Serializable {
    // シリアルバージョンUID
    private static final long serialVersionUID = 7283339629129432007L;
    /** 名前 */
    @NotEmpty
    @Size(min=1, max=10)
    private String name;
    /** 生年月日_年 */
    private String birthYear;
    /** 生年月日_月 */
    private String birthMonth;
    /** 生年月日_日 */
    private String birthDay;
    /** 性別 */
    @NotEmpty(message="{sex.NotEmpty.message}")
    private String sex;
    /** 性別(ラベル) */
    private String sexLabel;
    /** メモ */
    private String memo;
    /** 確認チェック */
    @AssertTrue
    private Boolean checked;
    /** 生年月日_月(選択リスト) */
    private List<SelectItem> birthMonthItems;
    /** 生年月日_日(選択リスト) */
    private List<SelectItem> birthDayItems;
    /** 性別(選択リスト) */
    private List<SelectItem> sexItems;
    /** UserDataテーブルへアクセスするJPA */
    @Inject
    private UserDataJpa userDataJpa;
    /**
     * コンストラクタ生成時に選択リストの値を設定.
     */
    public InputFormAction(){
        // 生年月日_月(選択リスト)
        birthMonthItems = new ArrayList<SelectItem>();
        birthMonthItems.add(new SelectItem("", ""));
        for(Integer i = 1; i <= 12; i++){
            birthMonthItems.add(new SelectItem(String.valueOf(i), String.valueOf(i)));
        }
        // 生年月日_日(選択リスト)
        birthDayItems = new ArrayList<SelectItem>();
        birthDayItems.add(new SelectItem("", ""));
        for(Integer i = 1; i <= 31; i++){
            birthDayItems.add(new SelectItem(String.valueOf(i), String.valueOf(i)));
        }
        // 性別(選択リスト)
        sexItems = new ArrayList<SelectItem>();
        sexItems.add(new SelectItem(String.valueOf(1),"男"));
        sexItems.add(new SelectItem(String.valueOf(2),"女"));
    }
    /**
     * 確認画面への遷移.
     * @return 確認画面へのパス
     */
    public String confirm(){
        // 性別(ラベル)を設定
        if(!CommonUtil.isBlank(sex)){
            this.setSexLabel(this.getSexItems().get(
                Integer.parseInt(this.getSex())-1).getLabel());
        }
        // Formに設定された値を出力
        System.out.println(this.toString());
        // 確認画面に遷移
        return "confirm";
    }
    /**
     * 入力画面に戻る.
     * @return 入力画面へのパス
     */
    public String back(){
    	// 入力画面に戻る
    	return "back";
    }
    /**
     * 完了画面への遷移.
     * @return 完了画面へのパス
     */
    public String send(){
    	// 確認画面に表示された値を出力
        System.out.println(this.toString());
        // 画面の入力内容を登録
        userDataJpa.regist(getUserData());
        // セッション情報の破棄
        FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
        // 完了画面への遷移
        return "send";
    }
    /**
     * 相関チェックを実施し、エラーの場合はエラーメッセージを表示.
     * @param compSysEvent JSFシステムイベント
     */
    public void validate(ComponentSystemEvent compSysEvent) {
        UIComponent component = compSysEvent.getComponent();
        // 生年月日の年・月・日を取得する
        UIInput birthYearUI = (UIInput)component.findComponent("birthYear");
        UIInput birthMonthUI = (UIInput)component.findComponent("birthMonth");
        UIInput birthDayUI = (UIInput)component.findComponent("birthDay");
        String birthYearSt = (String)birthYearUI.getLocalValue();
        String birthMonthSt = (String)birthMonthUI.getLocalValue();
        String birthDaySt = (String)birthDayUI.getLocalValue();
        // 年・月・日がすべて空白値の場合はエラーメッセージを返す
        if(CommonUtil.isBlank(birthYearSt) && CommonUtil.isBlank(birthMonthSt)
            && CommonUtil.isBlank(birthDaySt)){
        	addErrorMessage("org.hibernate.validator.constraints.NotEmpty.message"
                    , component);
        	return;
        }
        // 生年月日が存在しない日付の場合はエラーメッセージを返す
        String dateStr = birthYearSt + CommonUtil.addZero(birthMonthSt) 
            + CommonUtil.addZero(birthDaySt);
        if(!CommonUtil.isCorrectDate(dateStr, "uuuuMMdd")){
        	addErrorMessage("date.Invalid.message", component);
        }
    }
    /**
     * 引数のメッセージKeyをもつエラーメッセージを追加.
     * @param messageKey メッセージKey
     * @param component JSFコンポーネント
     */
    private void addErrorMessage(String messageKey, UIComponent component){
    	FacesContext context = FacesContext.getCurrentInstance();
        String message = CommonUtil.getMessage(messageKey);
        FacesMessage facesMessage = new FacesMessage(message, message);
        facesMessage.setSeverity(FacesMessage.SEVERITY_ERROR);
        context.addMessage(component.getClientId(), facesMessage);
        context.renderResponse();
    }
    /**
     * 登録時に利用するユーザー情報を生成.
     * @return ユーザー情報
     */
    private UserData getUserData(){
    	UserData userData = new UserData();
        try{
            userData.setName(this.getName());
            userData.setSex(this.getSex());
            userData.setMemo(this.getMemo());
            userData.setBirthYear(Integer.parseInt(this.getBirthYear()));
            userData.setBirthMonth(Integer.parseInt(this.getBirthMonth()));
            userData.setBirthDay(Integer.parseInt(this.getBirthDay()));
        }catch(Exception ex){
            System.err.println(ex);
        }
    	return userData;
    }
}その他のソースコード内容は、以下のサイトを参照のこと。
 https://github.com/purin-it/java/tree/master/javaee-add-jpa/demoJsf
 
 
サンプルプログラムの実行結果
サンプルプログラムの実行結果は以下の通りで、確認画面で「送信」ボタンを押下した際に、USER_DATAテーブルにデータ登録が正常に行えることが確認できる。
1) GlassFishサーバーを起動後、Webブラウザ上で「http:// (ホスト名):(ポート番号)/(Webアプリケーションのプロジェクト名)/」とアクセスする。
 

3) 確認画面で「送信」ボタンを押下する前のUSER_DATAテーブルの内容は、以下の通り。
 
4) 確認画面で「送信」ボタンを押下し、完了画面に遷移する。
 

5) 確認画面で「送信」ボタンを押下した後のUSER_DATAテーブルの内容は以下の通りで、ID=3のデータが追加されていることが確認できる。
 
要点まとめ
- JakartaEE(旧称:JavaEE)では、JSFに加え、JPA(Java Persistence API)という、データの永続的な保管や取り出しを容易にする機能も利用することができる。
- JPAでデータベースにアクセスするためのJDNIは、GlassFishサーバーの管理コンソール画面上で作成できる。
- JSFプロジェクトでJPAによるデータベース接続が行えるようにするには、JPAプロジェクトに変換する。
- JPAで使用するJNDI定義は、設定ファイル(persistence.xml)で定義する。







