JDeveloperでのJPA開発 / 複合 主キーの扱い

satonaoki2007-07-02


OTN Japan 掲示板にあったこんな質問をもとに、JDeveloperでのJPA開発のハンズオンを紹介しましょう。

Oracle DatabaseのUSER_TAB_COLUMNSとは、現行のDBユーザが所有するテーブルやカラムの情報が格納されたビューです。

SCOTTユーザでは、USER_TAB_COLUMNS ビューの内容はこんな感じ。(ちなみに、この画面は、JDeveloperSQLワークシートでSQLを発行した様子です。)

では、USER_TAB_COLUMNS ビューに対するJPAエンティティと、そのJPAエンティティにアクセスするクライアントを作ってみましょう。今回は、EJBコンテナ内でJPAエンティティにアクセスするEJBセッションBeanを作成し、そのEJBセッションBeanにアクセスするEJBクライアントのJavaアプリケーションを作成することにします。

JDeveloperには、Oracle Application ServerのJEEコンテナ OC4Jが同梱されているので、JEEコンテナ/アプリケーション サーバを別途インストールしてそこにデプロイする、というった面倒な手順を踏まなくても、簡単にEJBやWebアプリをテスト実行できます。今回のシナリオだと、JPAエンティティとEJBセッションBeanが、このOC4Jで動作します。

Oracle Database <--[JDBC]--> JPA エンティティ <--[ローカル呼び出し]--> EJBセッションBean <--[RMI]--> EJBクライアント

さて、JDeveloper (10.1.3.1以降、最新の10.1.3.3がオススメ) を起動して、新規アプリケーションを作成します。今回は、EJBを作成するので、アプリケーション テンプレートとしてWebアプリケーション [JSFEJB] を選択しておきます。

2つのプロジェクト (EJB向けのModelプロジェクトと、JSF向けのViewControllerプロジェクト) が自動生成されます。今回のシナリオでは、EJB向けのModelプロジェクトのみを使っていきます。

今回は既存のテーブル/ビューをもとに新規JPAエンティティを作成するので、Modelプロジェクトを右クリックして[新規]を選択し、新規ギャラリのEJBカテゴリから「表ベースのエンティティ (JPA/EJB 3.0)」を選択します。

「エンティティ作成 (表ベース)」ウィザードでは、Oracle DatabaseのSCOTTユーザへのDB接続情報を定義していなければ定義し、定義済みなら定義済みの接続情報を選択します。次に、JPAエンティティのベースとなるテーブル/ビューを選択します。USER_TAB_COLUMNS ビューの所有者はSYSユーザなので、スキーマをSYSに変更してから USER_TAB_COLUMNS ビューを選択します。その他いくつかのオプションも適宜設定して、ウィザードを終了します。

その他いくつかのオプションも適宜設定して、ウィザードを終了します。これにより、JPAエンティティである UserTabColumns クラスと、JPAの設定ファイル persistence.xml が自動生成されます。

persistence.xml には、永続性ユニット (Persistence Unit) という形でDB接続情報などを定義します。JDeveloper 10.1.3.xでは、ちょっと気が利いていなくて、内容のない永続化ユニットのエントリ (要素) が自動生成されてしまうので、まずは、persistence.xmlをオープンして、この 要素を削除します。それから、persistence.xmlを右クリックして「新規の永続性ユニット」を選択し、「永続性ユニットの作成」ダイアログを起動します。

このダイアログでは、DB接続情報や、実行環境 (EJBコンテナ内外) の選択、JPAエンティティをもとにしたテーブルの自動生成の設定などができます。今回は、EJBコンテナ内で動かしたいので、「Inside Java EE container」を選択します。この場合、JTAデータソースのJNDI名も自動的に生成されます。(JDeveloperのDB接続情報はJDeveloperから起動するOC4Jに自動的に引き継がれるので、テスト実行の場合にはデータソースのJNDI名は特に気にする必要はありません。別のJEEコンテナ/APサーバにデプロイする場合には、当然そのデプロイ先で適宜データソースを構成しておく必要があります。)


<persistence .....>
 <persistence-unit name="Model">
  <jta-data-source>jdbc/ScottDS</jta-data-source>
 </persistence-unit>
</persistence>

ちなみに、今回のシナリオとは違いますが、JEEコンテナ外やWebコンテナから直接JPAエンティティを使いたい場合には、「Outside Java EE container」を選択します。この場合、DB接続情報は、JPA実装 TopLink Essentials 固有のプロパティ群 toplink.jdbc.* で設定されます。

<persistence .....>
 <persistence-unit name="Model">
  <class>model.UserTabColumns</class>
  <properties>
   <property name="toplink.jdbc.driver", value="oracle.jdbc.OracleDriver"/>
   <property name="toplink.jdbc.url", value="jdbc:oracle:thin:@host:port:sid"/>
   <property name="toplink.jdbc.user", value="scott"/>
   <property name="toplink.jdbc.password", value="[暗号化されたパスワード]"/>
   <property name="toplink.target-database", value="Oracle"/>
   <property name="toplink.logging.level", value="FINER"/>
   <property name="toplink.ddl-generation", value="drop-and-create-tables"/>
  </properties>
 </persistence-unit>
</persistence>

さて、persistence.xml の準備はできたので、自動生成された JPAエンティティ (UserTabColumns クラス) に目を移しましょう。今回利用した「エンティティ作成 (表ベース)」ウィザードは、対象のテーブル/ビューに主キー (プライマリ キー) が設定されていると、JPAエンティティの対応するフィールド/プロパティに @Id アノテーションを付けてくれます。

が、今回対象としている USER_TAB_COLUMNS ビューには主キーが設定されていません。そのため、ウィザードが自動生成したJPAエンティティにも、@Idアノテーションがない状態になっており、問題があります。USER_TAB_COLUMNS ビューの構造を見てみると、TABLE_NAME カラムとCOLUMN_NAME カラムの組み合わせが主キーとして適当なので、これらを複合 主キーとするのがいいでしょう。

ビューの定義を修正可能であれば、SQLのALTER VIEW文を発行して、USER_TAB_COLUMNS ビューに主キーの定義を追加してから、再度「エンティティ作成 (表ベース)」ウィザードを実行すればOKです。ウィザードは、(後で紹介する) 主キー クラスや@IdClass アノテーションなどを自動生成してくれます。

が、今回対象としているUSER_TAB_COLUMNS ビューはOracle Database自体が提供するもので、その定義を修正することはできません。そこで、ちょっと面倒ですが、手動で追加の設定をしてみましょう。

まず、新規ギャラリから「Javaクラス」を選択し、JPAエンティティと同じパッケージに、複合 主キーに対応する複数の値を保持するためのクラス (主キー クラス) UserTabColumnsPK を作りましょう。

package model;

public class UserTabColumnsPK {

    public String columnName;
    public String tableName;

    public UserTabColumnsPK() {}

    public UserTabColumnsPK(String columnName, String tableName) {
        this.columnName = columnName;
        this.tableName = tableName;
    }

    public void setColumnName(String columnName) { this.columnName = columnName; }
    public String getColumnName() { return columnName; }
    public void setTableName(String tableName) { this.tableName = tableName; }
    public String getTableName() { return tableName; }

    public boolean equals(Object other) {
        if (other instanceof UserTabColumnsPK) {
            final UserTabColumnsPK otherUserTabColumnsPK = 
                (UserTabColumnsPK)other;
            final boolean areEqual = 
                (otherUserTabColumnsPK.tableName.equals(tableName) && 
                 otherUserTabColumnsPK.columnName.equals(columnName));
            return areEqual;
        }
        return false;
    }

    public int hashCode() {
        return tableName.hashCode() + columnName.hashCode();
    }
}

単に、複合 主キーに対応するpublicプロパティを定義して、そのgetter/setterも定義し、コンストラクタ、equals()、hashCode()メソッドを追加しただけです。詳しい要件は、JPA 1.0仕様の「2.1.4 Primary Keys and Entity Identity」をどうぞ。

次に、JPAエンティティの方も修正しましょう。下記のアノテーションは、ウィザードによって定義済みです。

まず、クラス レベルのアノテーションとして @IdClass を指定し、その値には主キークラスを指定します。そして、複合主キーに対応する2つのフィールドに @Id アノテーションを追加します。

// パッケージ宣言、インポート

@Entity
// @IdClass を追加
@IdClass(UserTabColumnsPK.class)
@NamedQuery(name = "UserTabColumns.findAll", 
            query = "select o from UserTabColumns o")
@Table(name = "USER_TAB_COLUMNS")
public class UserTabColumns implements Serializable {
    @Column(name = "AVG_COL_LEN")
    private Long avgColLen;
    @Column(name = "CHARACTER_SET_NAME")
    private String characterSetName;
    @Column(name = "CHAR_COL_DECL_LENGTH")
    private Long charColDeclLength;
    @Column(name = "CHAR_LENGTH")
    private Long charLength;
    @Column(name = "CHAR_USED")
    private String charUsed;
    @Column(name = "COLUMN_ID")
    private Long columnId;
    // @Id を追加
    @Id
    @Column(name = "COLUMN_NAME", nullable = false)
    private String columnName;
    @Column(name = "DATA_DEFAULT")
    private String dataDefault;
    @Column(name = "DATA_LENGTH", nullable = false)
    private Long dataLength;
    @Column(name = "DATA_PRECISION")
    private Long dataPrecision;
    @Column(name = "DATA_SCALE")
    private Long dataScale;
    @Column(name = "DATA_TYPE")
    private String dataType;
    @Column(name = "DATA_TYPE_MOD")
    private String dataTypeMod;
    @Column(name = "DATA_TYPE_OWNER")
    private String dataTypeOwner;
    @Column(name = "DATA_UPGRADED")
    private String dataUpgraded;
    @Column(name = "DEFAULT_LENGTH")
    private Long defaultLength;
    private Long density;
    @Column(name = "GLOBAL_STATS")
    private String globalStats;
    @Column(name = "HIGH_VALUE")
    private String highValue;
    private String histogram;
    @Column(name = "LAST_ANALYZED")
    private Timestamp lastAnalyzed;
    @Column(name = "LOW_VALUE")
    private String lowValue;
    private String nullable;
    @Column(name = "NUM_BUCKETS")
    private Long numBuckets;
    @Column(name = "NUM_DISTINCT")
    private Long numDistinct;
    @Column(name = "NUM_NULLS")
    private Long numNulls;
    @Column(name = "SAMPLE_SIZE")
    private Long sampleSize;
    // @Id を追加
    @Id
    @Column(name = "TABLE_NAME", nullable = false)
    private String tableName;
    @Column(name = "USER_STATS")
    private String userStats;
    @Column(name = "V80_FMT_IMAGE")
    private String v80FmtImage;

// 以下、コンストラクタとgetter/setterが続く

これで、JPAエンティティのできあがりです!!!

今回は、JPAエンティティにアクセスするEJBセッションBeanを作って、JEEコンテナ OC4JでそのEJBを動作させ、さらにEJBクライアントも作っていきます。まず、JPAエンティティにアクセスするファサードを作りましょう。persistence.xmlを右クリックし、「新規セッション ファサード」を選択します。これは、EJBコンテナ内でのJPAアクセスのためのもので、EJBセッションBeanを生成します。(「新規のJavaサービス ファサード」を選択すると、EJBコンテナ外でのJPAアクセスのためのPOJOファサードが生成されます。)

「新規セッション ファサード」が生成したEJBセッションBeanは、こんな感じです。@Stateless アノテーションや、(unitName属性値に永続性ユニット名を指定した) @PersistenceContext アノテーションに注意しましょう。@PersistenceContext アノテーションでインジェクトされたEntityManagerを使って、JPAエンティティの新規作成時の永続化、削除、デタッチ後のマージ、検索などのメソッドが実装されていますね。

// パッケージ宣言、インポート

@Stateless(name="SessionEJB")
public class SessionEJBBean implements SessionEJB, SessionEJBLocal {
    @PersistenceContext(unitName="Model")
    private EntityManager em;

    public SessionEJBBean() {}

    public Object mergeEntity(Object entity) {
        return em.merge(entity);
    }

    public Object persistEntity(Object entity) {
        em.persist(entity);
        return entity;
    }

    /** <code>select o from UserTabColumns o</code> */
    public List<UserTabColumns> queryUserTabColumnsFindAll() {
        return em.createNamedQuery("UserTabColumns.findAll").getResultList();
    }

    public void removeUserTabColumns(UserTabColumns userTabColumns) {
        userTabColumns = em.find(UserTabColumns.class, new UserTabColumnsPK(
            userTabColumns.getColumnName(), userTabColumns.getTableName()));
        em.remove(userTabColumns);
    }
}

自動生成されたEJBセッションBean (SessionEJBBean) を右クリックして「実行」を選択すると、OC4Jが起動してJEEアプリケーション (EJBモジュール) をデプロイするところまでを自動的に行ってくれます。(今回のシナリオでは出てきませんが、JSFJSPサーブレットを実行すると、Webブラウザで適切なURLをオープンするところまでやってれくれます。)

さて、最後に、このEJBセッションBeanにアクセスしてUSER_TAB_COLUMNS ビューの全件検索を行うEJBクライアントを作ってみましょう。EJBセッションBean (SessionEJBBean) を右クリックし、「新規のサンプルJavaクライアント」を選択します。自動生成されたJavaアプリケーションのmainメソッドの中に、EJBセッションBeanのqueryUserTabColumnsFindAll()メソッドを介して、全件検索を行うJPQLクエリを実行し、forループでその結果のListをイテレートし、標準出力に、テーブル名とカラム名のプロパティ値を出力するようなコードを追加します。


// パッケージ宣言、インポート

public class SessionEJBClient {
    public static void main(String [] args) {
        try {
            final Context context = getInitialContext();
            SessionEJB sessionEJB = (SessionEJB)context.lookup("SessionEJB");
            // EJBにアクセスするには次のRemoteメソッドのいずれかを呼び出してください
            // sessionEJB.mergeEntity(  entity );
            // sessionEJB.persistEntity(  entity );
            // System.out.println( sessionEJB.queryUserTabColumnsFindAll(  ) );
            // sessionEJB.removeUserTabColumns(  userTabColumns );

            // 次の2行を追加
            for(UserTabColumns u : sessionEJB.queryUserTabColumnsFindAll()) {
                System.out.println(u.getTableName() + ":" + u.getColumnName());
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private static Context getInitialContext() throws NamingException {
        // Get InitialContext for Embedded OC4J
        // The embedded server must be running for lookups to succeed.
        return new InitialContext();
    }
}

自動生成されたクライアントを右クリックして「実行」を選択すると、Javaアプrケーション (EJBクライアント) が実行され、OC4J上のEJBを介してJPQLクエリを発行し、JPAエンティティのリストをクライアントに返します。ログ ウィンドウに期待した結果が出力されていますね。

長くなってきたので、WebアプリケーションからのJPAエンティティへのアクセスは、またの機会に…。