新トップページへ | Tip

JPAを使う

LastUpdate : 12/10/28

 JavaEE6になり、JPA2.0となりました。
DBアクセスには、JPAを使うのがスタンダードですよ?っていう感じらしいです。
JPQLという問い合わせ言語を使います。SQLに似ているので見てすぐわかると思います。もちろん、従来通りのSQLを使うことも可能です。

JPAはエンティティーの問い合わせを行うのに向いています。集計など、従来のSQLで行われていたことをそのまますべてJPQLでやろうとすると、無理があるケースがあるかと。
ケースバイケースでSQLとJPQLを使い分けたほうがよいハズ。(SQLを使う方法も本ページに書きました)。

もくじ

(0) このページで使用するソースの前提条件
(1) バッチプログラムなどコンテナ外で動くアプリケーションでJPAを使用する
(2) SELECT文のサンプル
(3) INSERT文のサンプル
(4) UPDATE文のサンプル
(5) DELETE文のサンプル
(6) コンストラクタ式
(7) 外部結合
(8) SQLを使用する


このページで使用するソースの前提条件

Eclipse4.2 + JBOSS7.1 + PostgreSQL9.12で作成・動作確認をしています。
(ただし、「バッチプログラムなどコンテナ外で動くアプリケーションでJPAを使用する」だけ、EclipseLink2.4で動作確認をしています)

以下のエンティティクラスを使用します(リンクをクリックすると、ソースを見れます)。

・Datadetail.java
・DatadetailPK.java
・Datamaster.java
・Itemdetailtable.java
・ItemdetailtablePK.java
・Itemtable.java

上記エンティティは、以下のSQLを使用しDBにテーブルを作成し、Eclipseにてエンティティーを自動生成しました(〜PKクラスは自動生成したとき、勝手に作られる)。自動生成の手順についてはこちらを参照。

Datadetailテーブルは、Datamasterテーブルへの外部参照を持っています。
ItemdetailtableテーブルはItemtableテーブルへの外部参照を持っています。

ItemTableテーブル
create table ItemTable(
    id serial primary key,
    msg text not null
);

ItemDetailTableテーブル
create table ItemDetailTable(
    id serial,
    price integer,
    date DATE,
    PRIMARY KEY(id, date),
    FOREIGN KEY (id) REFERENCES ItemTable(id)
);

DataMasterテーブル
create table DataMaster(
    id text primary key,
    name text
);

DataDetailテーブル
create table DataDetail(
    id text,
    sub text,
    msg text,
    primary key(id, sub),
    foreign key(id) references DataMaster(id)
);

Data〜系のテーブルと、Item〜系のテーブルで、エンティティを自動生成する際のオプションを一か所だけ変更しました。
(変更した点)

外部キー参照を持っていると、自動的に関連をもたせて生成するみたいです。
ここでは、DataDetailテーブルとDataMasterは設定はデフォルトのまま(関連有り)、ItemdetailtableとItemtableの関連は設定を変更し、関連を消しています(画面の「この関連づけを生成」のチェックボックスを消している)。


関連有り・無しで何が違ってくるかという具体例を以下にしめします(自動生成直後のソースより抜粋)。

Datamaster.java(抜粋)
@OneToMany(mappedBy="datamaster")
private List<Datadetail> datadetails;

Datadetail.java(抜粋)
@ManyToOne
@JoinColumn(name="id")
private Datamaster datamaster;

関連のあるテーブル双方に上記のようなフィールドが追加されます。DatamasterとDatadetailのSQL上の関係は「1」対「多」です。なので、「多」の方はList型になっています。外部結合なんかをしたときに、Datamasterテーブルのレコード情報と、それと対になるDatadetailのレコード情報が、Datamasterクラス内にすべて詰め込まれます。

本ページのサンプルでは、上記ソースを以下のように修正をして使用します(そうしないと、例外が発生する)。

Datamaster.java(抜粋/修正後)
@OneToMany(mappedBy="datamaster", fetch=FetchType.EAGER)
private List<Datadetail> datadetails;

Datadetail.java(抜粋/修正後)
@ManyToOne
@JoinColumn(name="id", insertable=false, updatable=false)
private Datamaster datamaster;

上記修正は、アプリケーションの動作の機能的な面では影響はないハズ。
エンティティは自動生成が基本なので、困ったら、その都度自動生成し直し、手直ししていけばよいかと。。。(面倒だがw


バッチプログラムなどコンテナ外で動くアプリケーションでJPAを使用する

バッチプログラムなどでもJPAを使用したい場合(Webアプリのロジック部分をそのまま流用するケースなどで発生するかと)の、サンプルを示します。

作成する上で、必要なものは、JPAの実装をしたライブラリと、DBのドライバです。
今回は、JPA実装には、EclipseLink2.4を使用しました(EclipseLinkについて → http://www.eclipse.org/eclipselink/)。
そして、使用してるDBはPostgreSQLを使用したので、それのドライバ(postgresql-9.1-902.jdbc4.jar)をダウンロードしてきます。
実行するには、これらのjarにパスを通す必要があります。

そして、DBアクセスの設定ファイルであるpersistence.xml(ソースディレクトリに、「META-INF」ディレクトリを作成し、その中にpersistence.xmlという名前のファイルを作成すれば、自動的に読み込まれる)に、DBへの接続情報を記述します。

以下のサンプルは、レコードをINSERTし、そして、全件SELECTをしています。

JavaMain.java(エントリポイントのあるクラス)
package alctail;

public class JavaMain {
        public static void main(String[] args) {
                new JPAMain().sample();
        }
}

JPAMain.java
package alctail;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import alctail.entity.Itemtable;

/**
 * サーブレットやWebサービスなどはコンテナ上で動作する。
 * なので、@PersistenceContextを使ってEntityManagerをインジェクションしてもらえば良い。
 * (トランザクションがコンテナ管理となる)
 * バッチなどの場合は、自分でEntityManagerを生成し、自分でトランザクションを管理することとなる。
 */
public class JPAMain {
        
        public void sample() {
                EntityManagerFactory factory = Persistence.createEntityManagerFactory("JPAStudy2");
                EntityManager entityManager = factory.createEntityManager();
                EntityTransaction tx = entityManager.getTransaction();
                
                Itemtable record = new Itemtable();
                //record.setId("XXXXX"); idは自動採番されるので値を設定しない
                record.setMsg("サンプルだよ!");
                
                // トランザクションは自分で制御する必要がある
                try {
                        tx.begin(); 
                        entityManager.persist(record);
                        tx.commit();                    
                }catch (Exception e) {
                        tx.rollback();
                }
                
                // Itemtableのレコードをすべて取得する
                String jpql = "SELECT u FROM Itemtable u ORDER BY u.id DESC";
                List<Itemtable> resultList = entityManager.createQuery(jpql).getResultList();
                
                entityManager.close();
                factory.close();
                
                // 取得した一覧を標準出力へ表示
                showItemTable(resultList);
        }
        
        private void showItemTable(List<Itemtable> recordList) {
                for (Itemtable record : recordList) {
                        System.out.println("id : "+record.getId()+" / msg : "+record.getMsg());
                }
        }

}

persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
        <persistence-unit name="JPAStudy2" transaction-type="RESOURCE_LOCAL">
                <class>alctail.entity.Itemtable</class>
                <properties>
                        <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://192.168.11.40:5432/testdb"/>
                        <property name="javax.persistence.jdbc.user" value="postgres"/>
                        <property name="javax.persistence.jdbc.password" value="postgres"/>
                        <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
                </properties>
        </persistence-unit>
</persistence>


SELECT文のサンプル

最初に、最も基本的な全件取得のSELECT文の紹介と、サンプルアプリの呼び出し方について説明します。
そして、後半には、条件句(WHERE句)をつける方法について扱います。

最も基本的なSELECT文

1つのテーブルから、レコードを全件取得するサンプルを以下に示します。
EntityManagerはCDIを利用してコンテナからインジェクションしてもらいます。なので、DBAccessクラスはコンテナ上で動作しているアプリでDIされなければ動きません。
DBAccess.java(抜粋)
@PersistenceContext(unitName = "JPAStudy1")
private EntityManager entityManager;

〜〜〜〜〜 省略 〜〜〜〜〜

/**
 * JPQLのサンプル(全件検索)
 */
public List<Itemdetailtable> getItemDetailAll() {
        try {
                // 全件検索
                String jpql = "SELECT u FROM Itemdetailtable u";
                return entityManager.createQuery(jpql, Itemdetailtable.class).getResultList();
        } catch(Exception e) {
                e.printStackTrace();
                throw new RuntimeException("ワロタwwwwwww");
        }
}
上記クラスは以下のように使用します。今回は、コンテナ上で動くサンプルの一回目なので、サンプルを載せますが、二回目以降は同じようなソースなので、省略します。
以下は、RESTをWebサービスを作成しています(DBAccess.javaを動かすためには別に、RESTである必要はありません。SOAPでも、サーブレットでも良いです)。
JPAStudy.java
package alctail.rest;

import java.util.List;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

import alctail.dao.DBAccess;
import alctail.entity.Datadetail;
import alctail.entity.Datamaster;
import alctail.entity.Itemdetailtable;
import alctail.entity.Itemtable;
import alctail.expression.ItemDataArrange;

@Path("/JPAStudy")
public class JPAStudy {
        
        @Inject
        DBAccess dbAccess;
        
        /** JPAの単純なサンプル1(1テーブルの全件取得) */
        @GET
        @Path("/accessDB1")
        @Produces("text/plain")
        public String accessDB1() {
                StringBuilder builder = new StringBuilder();
                List<Itemdetailtable> resultList = dbAccess.getItemDetailAll();
                for (Itemdetailtable record : resultList) {
                        builder.append("--------------------").append("\n").
                                append("id : ").append(record.getId().getId()).append("\n").
                                append("price : ").append(record.getPrice()).append("\n").
                                append("date : ").append(record.getId().getDate()).append("\n").
                                append("\n");
                }
                return builder.toString();
        }

〜〜〜〜〜〜〜 以下省略 〜〜〜〜〜〜〜

条件句を追加した例

WHERE句のつけ方についてです。固定値も、プログラム上から設定できる変数にすることもできます。おまけで、ソート(DESC)も指定しています。
固定値を設定した例
DBAccess.java
/**
 * JPQLのサンプル(WHERE句/Orderby有り)
 */
public List<Itemdetailtable> getItemDetailWhere() {
        try {
                // where句とorderby有り
                String jpql = "SELECT u FROM Itemdetailtable u WHERE u.id.id='000001' ORDER BY u.id.id DESC";
                return entityManager.createQuery(jpql, Itemdetailtable.class).getResultList();
        } catch(Exception e) {
                e.printStackTrace();
                throw new RuntimeException("ワロタwwwwwww");
        }
}
プログラム上から変数に値を設定する方法
DBAccess.java
/**
 * JPQLのサンプル(JPQL内に可変値を指定する)
 */
public List<Itemdetailtable> getItemDetailQuery(String key) {
        try {
                // 可変の部分(例では「:id」が該当)をホスト変数みたいに、あとから指定ができる
                String jpql = "SELECT u FROM Itemdetailtable u WHERE u.id.id=:id ORDER BY u.id.id DESC";
                TypedQuery<Itemdetailtable> query = entityManager.createQuery(jpql, Itemdetailtable.class);
                // 「:id」の部分の値を指定する(戻り値は、自分自身が返ってくるので.で続けざまにsetParameterを記述していくことも可能)
                query = query.setParameter("id", key);
                
                // 1個しかレコードが返ってこないなら以下のメソッドを使い結果を取得する
                // query.getSingleResult()
                
                // 1個以上返ってくる場合(不定の場合)以下のメソッドを使用して結果を取得する
                List<Itemdetailtable> reusltList = query.getResultList();
                return reusltList;
        } catch(Exception e) {
                e.printStackTrace();
                throw new RuntimeException("ワロタwwwwwww");
        }
}


INSERT文のサンプル

レコードのINSERTを行います。
Itemtableテーブルのプライマリーキーはidというカラムです。しかし、ここでは値を設定していません。自動でインクリメントされた値が挿入されます(idフィールドに「@GeneratedValue(strategy=GenerationType.IDENTITY)」というアノテーションをつけています。さらに、PostgreSQLの場合、serialという型にすると、自動でインクリメントした値が挿入されます)。

DBAccess.java
/**
 * JPQLのサンプル(レコードの挿入)
 */
public List<Itemtable> insertItem(String msg) {
        
        // レコードの挿入
        try {
                utx.begin();
                Itemtable record = new Itemtable();
                record.setMsg(msg);
                entityManager.persist(record);
                utx.commit();
        } catch(Exception e) {
                e.printStackTrace();
                // ロールバック
                try {   utx.rollback(); } catch(SystemException ee) { ee.printStackTrace(); }
                throw new RuntimeException("ワロタwwwwwww1");
        }
        
        // itemtableテーブルのレコードを全件取得する
        try {
                String jpql = "SELECT u FROM Itemtable u ORDER BY u.id";
                TypedQuery<Itemtable> query = entityManager.createQuery(jpql, Itemtable.class);
                return query.getResultList();
        } catch(Exception e) {
                e.printStackTrace();
                throw new RuntimeException("ワロタwwwwwww2");
        }
}


UPDATE文のサンプル

レコードのUPDATEを行います。

DBAccess.java
/**
 * JPQLのサンプル(レコードの更新)
 */
public List<Itemtable> updateItem(String msg) {
        
        // レコードの更新
        try {
                utx.begin();
                Itemtable record = new Itemtable();
                record.setId(1);        // id=1のレコードを更新する(今回のサンプルではここは固定)
                record.setMsg(msg);
                
                // 指定したrecordのPKをみてDB上にある場合はupdate、ない場合はinsertになる
                entityManager.merge(record);
                utx.commit();
        } catch(Exception e) {
                e.printStackTrace();
                // ロールバック
                try {   utx.rollback(); } catch(SystemException ee) { ee.printStackTrace(); }
                throw new RuntimeException("ワロタwwwwwww1");
        }
        
        // itemtableテーブルのレコードを全件取得する
        try {
                String jpql = "SELECT u FROM Itemtable u ORDER BY u.id";
                TypedQuery<Itemtable> query = entityManager.createQuery(jpql, Itemtable.class);
                return query.getResultList();
        } catch(Exception e) {
                e.printStackTrace();
                throw new RuntimeException("ワロタwwwwwww2");
        }
}


DELETE文のサンプル

レコードの削除を行います。

DBAccess.java
/**
 * JPQLのサンプル(レコードの削除)
 */
public List<Itemtable> deleteItem(int id) {
        
        // レコードの更新
        try {
                utx.begin();
                // 引数で指定されたidを削除するため、idのレコードを取得する
                Itemtable record = entityManager.find(Itemtable.class, id);
                entityManager.remove(record);
                utx.commit();
        } catch(Exception e) {
                e.printStackTrace();
                // ロールバック
                try {   utx.rollback(); } catch(SystemException ee) { ee.printStackTrace(); }
                throw new RuntimeException("ワロタwwwwwww1");
        }
        
        // itemtableテーブルのレコードを全件取得する
        try {
                String jpql = "SELECT u FROM Itemtable u ORDER BY u.id";
                TypedQuery<Itemtable> query = entityManager.createQuery(jpql, Itemtable.class);
                return query.getResultList();
        } catch(Exception e) {
                e.printStackTrace();
                throw new RuntimeException("ワロタwwwwwww2");
        }
}


コンストラクタ式

エンティティークラス以外にも、SELECT結果を自由に格納することが出来ます。

DBAccess.java
/**
 * コンストラクタ式のサンプル
 * SELECTの次にNEW クラス名・・・と記述することで、Entityクラス以外の任意のクラスに検索結果を格納できます
 */
public List<ItemDataArrange> constractorExpression1() {
        try {
                String jpql = "SELECT new alctail.expression.ItemDataArrange(d.id, d.name) FROM Datamaster d";
                List<ItemDataArrange> resultList = entityManager.createQuery(jpql, ItemDataArrange.class).getResultList();
                
                return resultList;
        } catch(Exception e) {
                throw new RuntimeException(e);
        }
}

上記のJPQL内で使用しているクラスのソースを以下に示します。

ItemDataArrange.java
package alctail.expression;

import java.io.Serializable;

/**
 * JPQLのコンストラクタ式のサンプルに使用するクラス
 */
public class ItemDataArrange implements Serializable {
        private static final long serialVersionUID = 1L;
        
        private String id;
        private String name;
        
        /** コンストラクタ */
        public ItemDataArrange(String id, String name){
                this.id = id;
                this.name = name;
        }

        public String getId() {
                return id;
        }

        public void setId(String id) {
                this.id = id;
        }

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }
        
}


外部結合

JPQLで外部結合も可能です。そのサンプルを示します。

DBAccess.java
/**
 * 外部結合のサンプル
 * Datamasterテーブルとdatadetailsテーブルを結合しています。
 * JOINの後ろに、Datamasterクラスが持ってるDatadetailsテーブルのデータを格納するフィールドを指定している
 */
public List<Datamaster> outerJoin() {
        try {
                String jpql = "SELECT a FROM Datamaster a LEFT OUTER JOIN a.datadetails b";
                List<Datamaster> resultList = entityManager.createQuery(jpql, Datamaster.class).getResultList();
                
                return resultList;
        } catch(Exception e) {
                throw new RuntimeException(e);
        }
}

このページで使用するソースの前提条件で説明したとおり、使用するエンティティークラスには、関連を持たせてソースを自動生成しています。
SELECTした結果のDatamaster内に、Datadetailのリストを保持しているので、Datadetailのレコードは、このフィールドに格納されます。


SQLを使用する

SQLも使用することができます。そのサンプルを以下に示します。

DBAccess.java
/**
 * SQLを使用するサンプル
 */
public List<Datamaster> nativeQuery() {
        try {
                String jpql = "SELECT id, name FROM Datamaster";
                List<Datamaster> resultList = entityManager.createNativeQuery(jpql, Datamaster.class).getResultList();
                
                return resultList;
        } catch(Exception e) {
                throw new RuntimeException(e);
        }
}

s