JDeveloperでのJPA開発 / 複合 主キーの扱い
OTN Japan 掲示板にあったこんな質問をもとに、JDeveloperでのJPA開発のハンズオンを紹介しましょう。
- OTN Japan 掲示板 > JDeveloper > USER_TAB_COLUMNSのエンティティ作成(表ベース)について
Oracle DatabaseのUSER_TAB_COLUMNSとは、現行のDBユーザが所有するテーブルやカラムの情報が格納されたビューです。
- Oracle Database リファレンス 10.2 > USER_TAB_COLUMNS
SCOTTユーザでは、USER_TAB_COLUMNS ビューの内容はこんな感じ。(ちなみに、この画面は、JDeveloperのSQLワークシートで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アプリケーション [JSF、EJB] を選択しておきます。
- S/N Ratio > JDeveloper 10.1.3.3 リリース (2007/06/30)
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では、ちょっと気が利いていなくて、内容のない永続化ユニットのエントリ (
このダイアログでは、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エンティティの方も修正しましょう。下記のアノテーションは、ウィザードによって定義済みです。
- JPAエンティティであることを示す @Entity アノテーション
- マッピング先のテーブル/ビュー名を指定する @Table アノテーション
- 全件検索を行うJPQLクエリ (select o from UserTabColumns o) を定義した @NamedQuery アノテーション
- マッピング先のカラム名を指定する @Column アノテーション
まず、クラス レベルのアノテーションとして @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モジュール) をデプロイするところまでを自動的に行ってくれます。(今回のシナリオでは出てきませんが、JSF、JSP、サーブレットを実行すると、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エンティティへのアクセスは、またの機会に…。