最終更新日:2020/03/21 原本2010-05-14

今からでも遅くない JPAを学ぼう!(後編)
オブジェクト間の関連を理解し、JPQLを使用する

ディレクション、カーディナリティは難しくない

ダウンロード サンプルコード (14.2 KB)

 Java Persistence API(JPA)を使ってオブジェクトの世界とリレーショナルの世界を結び付ける方法を一緒に学んでいきたいと考えています。前編では1つのテーブルに対してCRUD操作を行いました。後編となる今回は、複数のテーブル間の関連をEntityモデルで表現する方法と、それらを扱うためのJPQLについて説明します。

はじめに

 JPA(Java Persistence API)とは、オブジェクトの世界からリレーショナルの世界へ、あるいはその逆への変換を行うためのAPIです。

 前編では、JPAを使用した1テーブルに対するCRUD操作を行うための実装方法を説明しました。後編となる今回は、複数のテーブルに対するCRUD操作について解説していきます。

ディレクションとカーディナリティのトラウマ

 オブジェクトモデルの世界でEntity間の関連は、ディレクションとカーディナリティという2つの軸を組み合わせます。Entity間の関連の扱い方は非常に難しいと思われがちです。特にEJB2.1以前のCMPを使った設計やコーディングの経験のある方は、設定ファイルにしろコーディングにしろ、学習も困難・実装も困難ということでトラウマとなったと思います。

 筆者自身もEJB2.1で多くのCMPを記述し、その困難さを身をもって知った技術者の1人です。そこで、Javaの世界はEJBからPOJOの世界へと一気に流れが変わったように思えます。しかし、EJBにはそれなりの魅力があることも確かです。ライフサイクルの管理をはじめ、多くの面倒な処理をEJBコンテナが行ってくれるからです。

Session Bean+JPA

 上記のトラウマこそ、筆者がEJBを捨てPOJOに逃げ込んだ原因です。とは言いつつも、JPA自体はPOJOをベースに作られているということからすれば、EJBの世界にJPAというPOJOベースのO/Rマッピングの技術が持ち込まれたことになり、Session BeanとJPAの組み合わせはEJBとPOJOのいいとこ取りをしていることになります。前編ではJavaアプリケーションを使いJPAを説明しましたが、今回は、EJBを使用した場合、いかに便利になるかを実感していただければと思います。

ディレクションとカーディナリティ

ディレクション

 オブジェクトモデルのディレクションは参照のフィールドの保持により決定します。AクラスとBクラスが存在し、AクラスがBクラスの参照のフィールドは持つが、BクラスはAクラスの参照のフィールドを持たない場合、単方向となります。クラス図ではAからBへの矢印で表します。AクラスとBクラスが互いの参照のフィールドを持つ場合、双方向となります。クラス図ではAとBの間に両方向の矢印で表します。

カーディナリティ

 オブジェクトモデルのカーディナリティは、参照のフィールドがコレクションか否かで決定します。AクラスとBクラスが存在し、それぞれ次のように呼ばれます。

  • 1対1(One-to-one):AクラスがBクラスのコレクションではない参照のフィールドを持ち、BクラスはAクラスへの参照を持たない場合、またはその逆の場合、さらにはお互いコレクションではない参照のフィールドを持つ場合のAクラスとBクラス
  • 1対多(One-to-many):AクラスがBクラスの参照のフィールドをコレクションとして持つ場合(BクラスはAクラスのコレクションではない参照を持っても構わない)のAクラスとBクラス
  • 多対1(Many-to-one):BクラスがAクラスの参照のフィールドをコレクションとして持つ場合(AクラスはBクラスのコレクションではない参照を持っても構わない)のAクラスとBクラス
  • 多対多(Many-to-many):AクラスがBクラスの参照のフィールドをコレクションとして持ち、BクラスもAクラスの参照のフィールドのコレクションを持つ場合のAクラスとBクラス

関連はディレクションとカーディナリティの組み合わせ

 関連のパターンはディレクションで2つ(単方向、双方向)、カーディナリティで4つ(One-to-one、One-to-many、Many-to-one、Many-to-many)があり、その組み合わせは単純に計算すると8つとなります。

 関連のパターンをクラス図で示すと次のようになります。ディレクションとカーディナリティはAクラスを基準としています。Many-to-manyの場合、単方向はないので、実質的には7つのパターンが存在します。これらのパターンを整理したのは@OneToOne、@OneToMany、@ManyToOne、@ManyToManyなどのアノテーションをEntityクラスのカラムに付与する必要があるためです。以下ではコレクションとしてListを使用していますが、SetやMapも使用可能です。

One-to-one
単方向
単方向
双方向
双方向
One-to-many
単方向
単方向
双方向
双方向
Many-to-one
単方向
単方向
双方向
双方向
Many-to-many
  • 単方向:該当無し
双方向
双方向

主と従

 双方向の場合、という関係があります。これはオブジェクトモデルではなく、リレーショナルモデルを元に決定します。双方向でMany-to-manyの場合は、どちらも主であり従となることができます。それ以外の双方向は、RDBの外部キーを持っているテーブルを表現しているEntityオブジェクト側が主となり、外部キーを持っていないテーブルを表現しているEntityオブジェクト側が従となります。

Entityクラスのアノテーション

 関連を持つフィールドの場合、ディレクションとカーディナリティの7つのパターン、および主と従という関係が理解できていれば適切にアノテーションを付与することが可能となります。カーディナリティはアノテーションを付与するのみで設定ファイルの変更が必要ないため、非常に分かりやすい記述が可能です。単方向でかつ相手の参照を持たない場合は、Columnアノテーションのみで済みます。

Entity間の関連を実装する

 Entity間の関連を実装するには、それなりのEntityが必要になります。1から構築するのは手間がかかるため、筆者の連載「GlassFishからアプローチするJava」の第6回を参考にします。参考記事はJavaのWebアプリケーションですが、今回はSession Beanを作成するため、プロジェクトは[エンタープライズアプリケーション]を指定する必要があります。

プロジェクトの作成

 NetBeansで[ファイル]-[新規プロジェクト]をクリックすると[新規プロジェクト]画面が開きます。[カテゴリ]で「Java EE」を選択すると、[プロジェクト]のリストに[エンタープライズアプリケーション]が表示されるので選択し、[次へ]ボタンをクリックします。[新規エンタープライズアプリケーション]の[名前と場所]画面が開きます。[プロジェクト名]は任意の名前で構いません(ここでは「JPASample02」と入力します)。

 他の項目はそのままで[次へ]ボタンをクリックすると、[サーバーと設定]画面が開きます。デフォルトのままでいいと思いますが、[EJBモジュールを作成]と[Webアプリケーションモジュールを作成]のチェックボックスにチェックが入っていることを確認してください。確認後、[完了]ボタンをクリックすると3つのプロジェクトが作成されます。「JPASample02」「JPASample02-ejb」「JPASample02-war」が作成されていることを、左ペインの[プロジェクト]タブで確認してください。

アプリケーションの要件

 アプリケーションの要件は「GlassFishからアプローチするJava」の第6回とほぼ同じです。「芋焼酎酒店」という架空のネットショップを作成します。

  • 画面上部に芋焼酎のカタログを表示する
  • 銘柄の左側にあるチェックボックスにチェックを入れ、カタログの下にある[買い物かごに追加する]ボタンをクリックすると買い物かごに銘柄が追加される。本数の初期値は1とする
  • 買い物かごの本数を更新し[買い物かごを再表示する]ボタンをクリックすると、銘柄ごとの金額が計算し表示する
  • 買い物かごの下部に明細合計(銘柄の金額の合計)、送料、総合計(明細合計に送料を加えたもの)を表示する
  • [レジへ進む]ボタンをクリックすると買い物かごが空になる

 買い物かごが空の状態の「芋焼酎酒店」のイメージは図1の通りです。

図1.「芋焼酎酒店」初期状態
図1.「芋焼酎酒店」初期状態

ER図

 参考アプリケーションのER図は図2になります。

図2.当アプリケーションのER図
図2.当アプリケーションのER図

クラス図

図3.当アプリケーションのEntityオブジェクトのクラス図
図3.当アプリケーションのEntityオブジェクトのクラス図

 BasketクラスとBasketItemクラスは1対多の双方向の関連、BasketItemクラスとMeigaraクラスは多対1の単方向の関連になります。BasketクラスとBasketItemクラスはコンポジットの関係のため、Basketオブジェクトが削除される場合、BasketItemオブジェクトが存在しても意味がなくなるため、BasketItemオブジェクトを全削除する必要があります。

Entityクラスの実装

 上記のクラス図を踏まえEntityクラスを実装します。以下の3つのクラスはNetBeansの左ペインで[プロジェクト]-[JPASample02-ejb]を選択し右クリックします。[新規]-[Javaクラス]を選択すると、クラス名とパッケージ名を入力する画面が開きます。クラス名は以下の「Basket」「BasketItem」「Meigara」をおのおの作成するときに入力します。パッケージ名は何を入力しても結構ですが、ここでは「jp.kawakubo」と入力します(これは筆者の持っているドメイン名を逆さにしたものです。ドメインを持っている方はそれを使ってください)。

Basketクラスの実装

 リスト1がBASKETテーブルに対応したBasketクラスです。

リスト1.Basketクラス
001:@Entity
002:@Table(name = "BASKET")
003:public class Basket implements Serializable {
004:
005:    private int id = 0;
006:    private String userId = null;
007:    private List basketItems = null;
008:
009:    public Basket(){
010:    }
011:
012:    public Basket(int id,
013:                String userId,
014:                List basketItems) {
015:        this.id = id;
016:        this.userId = userId;
017:        this.basketItems = basketItems;
018:    }
019:
020:    @Id
021:    @GeneratedValue(strategy=GenerationType.IDENTITY)
022:    @Column(name = "ID")
023:    public int getId() {
024:        return id;
025:    }
026:
027:    public void setId(int id) {
028:        this.id = id;
029:    }
030:
031:    @Column(name = "USER_ID")
032:    public String getUserId() {
033:        return userId;
034:    }
035:
036:    public void setUserId(String userId) {
037:        this.userId = userId;
038:    }
039:
040:    @OneToMany(targetEntity = BasketItem.class,
041:        mappedBy = "basket")
042:    public List getBasketItems() {
043:        return basketItems;
044:    }
045:
046:    public void setBasketItems(List basketItems) {
047:        this.basketItems = basketItems;
048:    }
049:
050:}

 1行目のEntityアノテーションと2行目のTableアノテーションは前編と同じです。このクラスがEntityクラスであり、RDBのBASKETテーブルに対応していることを宣言しています。前編と比較して新しい箇所は、21、40、41行目です。

 BASKETテーブルのIDはAUTO_INCREMENTを指定しており、自動的に採番されています。そのようなカラムに対応するフィールド変数の場合、21行目のように、getterメソッドの前に以下を付けます。

@GeneratedValue(strategy=GenerationType.IDENTITY)

 40行目と41行目が今回のテーマである関連をどう表現するかに相当する箇所です。図2の通り、BasketクラスとBasketItemクラスはコンポジットの関連を持っています。黒いひし形がコンポジットを表しています。コンポジットとは集約よりもさらに強い結びつきを持っており、BasketItemオブジェクトはBasketオブジェクトの存在がなければ意味を持たないことを意味しています。実際に、買い物かごがなく、買い物かごの中身だけ存在していても意味はありません。40行目でBasketクラスとBasketItemクラスが1対多であることを指定しています。41行目でBasketItemクラスで宣言しているBasketのフィールド変数名(リスト2の6行目)であることを指定しています。

BasketItemクラスの実装

 リスト2がBASKET_ITEMテーブルに対応したBasketItemクラスです。

リスト2.BasketItemクラス
001:@Entity
002:@Table(name = "BASKET_ITEM")
003:public class BasketItem implements Serializable {
004:
005:    private int id = 0;
006:    private Basket basket = null;
007:    private Meigara meigara = null;
008:    private int quantity = 0;
009:
010:    public BasketItem() {
011:    }
012:    
013:    public BasketItem(int id,
014:            Basket basket,
015:            Meigara meigara,
016:            int quantity) {
017:        this.id = id;
018:        this.basket = basket;
019:        this.meigara = meigara;
020:        this.quantity = quantity;
021:    }
022:
023:    @Id
024:    @GeneratedValue(strategy=GenerationType.IDENTITY)
025:    @Column(name = "ID")
026:    public int getId() {
027:        return id;
028:    }
029:
030:    public void setId(int id) {
031:        this.id = id;
032:    }
033:
034:    @ManyToOne(targetEntity = Basket.class)
035:    @JoinColumn(name = "BASKET_ID",
036:        referencedColumnName = "ID")
037:    public Basket getBasket() {
038:        return basket;
039:    }
040:
041:    public void setBasket(Basket basket) {
042:        this.basket = basket;
043:    }
044:
045:    @ManyToOne(targetEntity = Meigara.class)
046:    @JoinColumn(name = "MEIGARA_ID",
047:        referencedColumnName = "ID")
048:    public Meigara getMeigara() {
049:        return meigara;
050:    }
051:
052:    public void setMeigara(Meigara meigara) {
053:        this.meigara = meigara;
054:    }
055:
056:    @Column(name = "QUANTITY")
057:    public int getQuantity() {
058:        return quantity;
059:    }
060:
061:    public void setQuantity(int quantity) {
062:        this.quantity = quantity;
063:    }
064:
065:}

 24行目はBasketクラスの説明と同様です。34行目から36行目がBasketItemクラスとBasketクラスの関連を表現しています。34行目でBasketItemクラスとBasketクラスが多対1であることを指定しています。35行目でBASKET_ITEMテーブルのカラム名を、36行目でBASKETテーブルのカラム名を指定します。

 45行目から47行目はMeigaraクラスとの関連を表現しています。45行目でBasketItemクラスとMeigaraクラスは多対1であることを指定しています。46行目ではBASKET_ITEMテーブルのカラム名を、47行目ではMEIGARAテーブルのカラム名を指定します。

 関連に直接関係のないフィールドは、前編同様Columnアノテーションを指定するだけです。

Meigaraクラスの実装

 リスト3がMEIGARAテーブルに対応したMeigaraクラスです。

リスト3.Meigaraクラス
001:@Entity
002:@Table(name = "MEIGARA")
003:public class Meigara implements Serializable {
004:
005:    private int id = 0;
006:    private String name = "";
007:    private String nameKana = "";
008:    private int dosu = 0;
009:    private String koji = "";
010:    private String sweetPotatoName = "";
011:    private String manufacturer = "";
012:    private float volume = 0f;
013:    private int price = 0;
014:
015:    public Meigara() {
016:    }
017:
018:    public Meigara(
019:            int id,
020:            String name,
021:            String nameKana,
022:            int dosu,
023:            String koji,
024:            String sweetPotatoName,
025:            String manufacturer,
026:            float volume,
027:            int price) {
028:        this.id = id;
029:        this.name = name;
030:        this.nameKana = nameKana;
031:        this.dosu = dosu;
032:        this.koji = koji;
033:        this.sweetPotatoName = sweetPotatoName;
034:        this.manufacturer = manufacturer;
035:        this.volume = volume;
036:        this.price = price;
037:    }
038:
039:    @Id
040:    @GeneratedValue(strategy=GenerationType.IDENTITY)
041:    @Column(name = "ID")
042:    public int getId() {
043:        return id;
044:    }
045:
046:    public void setId(int id) {
047:        this.id = id;
048:    }
049:
050:    @Column(name = "NAME")
051:    public String getName() {
052:        return name;
053:    }
054:
055:    public void setName(String name) {
056:        this.name = name;
057:    }
058:
059:    @Column(name = "NAMEKANA")
060:    public String getNameKana() {
061:        return nameKana;
062:    }
063:
064:    public void setNameKana(String nameKana) {
065:        this.nameKana = nameKana;
066:    }
067:
068:    @Column(name = "DOSU")
069:    public int getDosu() {
070:        return dosu;
071:    }
072:
073:    public void setDosu(int dosu) {
074:        this.dosu = dosu;
075:    }
076:
077:    @Column(name = "KOJI")
078:    public String getKoji() {
079:        return koji;
080:    }
081:
082:    public void setKoji(String koji) {
083:        this.koji = koji;
084:    }
085:
086:    @Column(name = "SWEETPOTATONAME")
087:    public String getSweetPotatoName() {
088:        return sweetPotatoName;
089:    }
090:
091:    public void setSweetPotatoName(String sweetPotatoName) {
092:        this.sweetPotatoName = sweetPotatoName;
093:    }
094:
095:    @Column(name = "MANUFACTURER")
096:    public String getManufacturer() {
097:        return manufacturer;
098:    }
099:
100:    public void setManufacturer(String manufacturer) {
101:        this.manufacturer = manufacturer;
102:    }
103:
104:    @Column(name = "VOLUME")
105:    public float getVolume() {
106:        return volume;
107:    }
108:
109:    public void setVolume(float volume) {
110:        this.volume = volume;
111:    }
112:
113:    @Column(name = "PRICE")
114:    public int getPrice() {
115:        return price;
116:    }
117:
118:    public void setPrice(int price) {
119:        this.price = price;
120:    }
121:
122:}

 このクラスは前編で使用したクラスと同じです。唯一違う行が40行目ですが、TopLinkの場合、この指定を入れなくても自動採番されます。BasketItemクラスとMeigaraクラスは多対1の単方向の関連を持っています。単方向の矢印の先にあるクラス、この場合であればMeigaraクラスはBasketItemクラスから使用されていることさえ認識していません。したがって、関連を表現するアノテーションは一切必要ありません。

関連を持ったEntityクラス群を扱うクラスの実装

 前編ではMeigaraクラスというEntityクラスのCRUD操作の仕方を説明しました。今回はMeigaraクラスに加え、Basketクラス、BasketItemクラスも存在し、それらの間には関連が存在します。今回はこのようなEntity群のCRUD操作を説明します。

コンテナ管理のEntityマネージャとJTAトランザクションの組み合わせのメリット

 Session Bean、つまりEJBコンポーネントでは一般的にコンテナ管理のEntityマネージャを使用します。またEJBコンポーネントの場合、自動的にJTAトランザクションのコントロール下に置かれます。つまり、EJBコンポーネントでコンテナ管理のEntityマネージャを使用するということは、コンテナ管理のEntityマネージャもJTAトランザクションのコントロール下に置かれることになります。これはEntityマネージャのライフサイクルがJTAトランザクションと一致することを意味します。要するにJTAトランザクションをcommitするかrollbackすることでEntityマネージャはcloseされます。Entityマネージャがコンテナ管理されると、前編のJavaアプリケーションで行っていたEntityファクトリとEntityマネージャの取得処理は不要になります。ただし、取得処理こそ行いませんが、DI(Dependency Injection:依存性注入)により、EntityマネージャはEJBコンテナによりEJBコンポーネントに設定されます。

 Entityマネージャにはコンテナ管理以外にアプリケーション管理のEntityマネージャが存在します。コンテナ管理のEntityマネージャを使用する場合は必ずJTAトランザクションを使用しなければなりません。

JPQLとは何か

 前編では1つのテーブルに対しCRUD操作を行いました。今回のように関連のある複数のテーブルを操作するにはSQLのような言語が必要になります。そこで誕生したのが、Entityクラスとそのカラムを使用したSQLとほぼ同じ構文を持ったJPQLです。実際のコーディングの中で使い方を説明します。JPQLについての詳細はDownload Enterprise JavaBeans 3.0 Final Releaseのサイトに「ejb-3_0-fr-spec-persistence.pdf」というリンクがあるので、クリックしてダウンロードしてください。そのChapter 4がJPQLの仕様です。

Entityクラス間の関連を扱うクラスの実装

 前編で説明した設定を行っていれば準備は終了です。前編を読まれていない方で設定を知りたい方は参照ください。

 まずはSession Beanの作成からです。NetBeansの左ペインで[プロジェクト]-[JPASample02-ejb]を開き右クリックします。[新規]-[セッション Bean]をクリックすると図4のように「新規 セッション Bean」画面が表示されます。EJB名に「JPASession01」、パッケージに「jp.kawakubo」と入力します。今回はStateless Session Beanを使用するため[セッションのタイプ]のラジオボタンは「ステートレス」を選択します。[インターフェースを作成]は後に説明するWeb層と接続する必要があるため、「リモート」にチェックを入れます。[完了]ボタンをクリックすると、[JAPSample02-ejb]-[ソースパッケージ]-[jp.kawakubo]の下に、「JPASession01Bean」と「JPASession01Remote」が作成されているはずです。このようにEJB名に「Bean」と「Remote」が自動的に付加されたクラス名になるため、EJB名に「Bean」を入れないことをおすすめします。

図4.「新規セッションBean」画面
図4.「新規セッションBean」画面

JPASample01Beanクラスの実装

 JPASample01Beanクラスの実装のみを説明しますが、JPASample01Remoteクラスにも同じシグネチャを持つメソッドを宣言してください。リスト4がJPASample01Beanの内容です。リスト5がJPASample01Remoteクラスです。

リスト4.JPASample01Beanクラス
001:package jp.kawakubo;
002:
003:import java.util.ArrayList;
004:import java.util.List;
005:import javax.ejb.Stateless;
006:import javax.persistence.EntityManager;
007:import javax.persistence.PersistenceContext;
008:import javax.persistence.Query;
009:
010:@Stateless(name = "JPASession01Remote")
011:public class JPASession01Bean implements JPASession01Remote {
012:    
013:    @PersistenceContext(unitName = "JPASample02-ejbPU")
014:    private EntityManager entityManager;
015:
016:    public JPASession01Bean() {
017:    }
018:
019:    public JPASession01Bean(EntityManager entityManager) {
020:        this.entityManager = entityManager;
021:    }
022:
023:    public List getMeigaras() {
024:        List meigaras = null;
025:        Query query = entityManager.createQuery(
026:                "SELECT m FROM Meigara AS m");
027:        meigaras = query.getResultList();
028:        return meigaras;
029:    }
030:
031:    public void insertBasketItems(int[] meigaraIds) {
032:        Basket basket = null;
033:        Query query = entityManager.createQuery(
034:                "SELECT b FROM Basket AS b " +
035:                "WHERE b.userId = :userId")
036:                .setParameter("userId", "tomoharu");
037:        List baskets = query.getResultList();
038:        if (baskets.size() == 0) {
039:            basket = new Basket();
040:            basket.setUserId("tomoharu");
041:            entityManager.persist(basket);
042:            entityManager.flush();
043:        } else {
044:            basket = baskets.get(0);
045:        }
046:
047:        BasketItem basketItem = null;
048:        Meigara meigara = null;
049:        for (int i = 0; i < meigaraIds.length; i++) {
050:            query = entityManager.createQuery(
051:                    "SELECT bi " +
052:                    "FROM BasketItem AS bi INNER JOIN bi.meigara AS m, " +
053:                    "Basket AS b " +
054:                    "WHERE m.id = :meigaraId " +
055:                    "AND b.userId = :userId")
056:                    .setParameter("meigaraId", meigaraIds[i])
057:                    .setParameter("userId", "tomoharu");
058:            List basketItems = query.getResultList();
059:            // 買い物かごに同じ銘柄がない場合のみ追加
060:            if (basketItems.size() == 0) {
061:                basketItem = new BasketItem();
062:                meigara = entityManager.find(Meigara.class, meigaraIds[i]);
063:                basketItem.setBasket(basket);
064:                basketItem.setMeigara(meigara);
065:                basketItem.setQuantity(1);
066:                entityManager.persist(basketItem);
067:            }
068:        }
069:    }
070:
071:    public List getBasketItems(String userId) {
072:        // userIdは固定で「tomoharu」を設定
073:        Query query = entityManager.createQuery(
074:                "SELECT NEW jp.kawakubo.BasketItemContent" +
075:                "(bi.id, m.name, m.price, bi.quantity) " +
076:                "FROM BasketItem AS bi INNER JOIN bi.meigara AS m, " +
077:                "Basket AS b " +
078:                "WHERE b.userId = :userId")
079:                .setParameter("userId", "tomoharu");
080:        List basketItemContents = query.getResultList();
081:        return basketItemContents;
082:    }
083:
084:
085:    public void updateBasketItems(int[] basketItemIds, int[] allQuantity) {
086:        for (int i = 0; i < basketItemIds.length; i++) {
087:            Query query = entityManager.createQuery(
088:                    "UPDATE BasketItem AS b " +
089:                    "SET b.quantity = :quantity " +
090:                    "WHERE b.id = :id")
091:                    .setParameter("quantity", allQuantity[i])
092:                    .setParameter("id", basketItemIds[i]);
093:            query.executeUpdate();
094:        }
095:    }
096:
097:    public List getTotalCharge(List basketItemContents) {
098:        List totalCharge = new ArrayList();
099:        int sumItems = 0;
100:        int shippingCost = 1500;
101:        int total = 0;
102:        BasketItemContent basketItemContent = null;
103:        // 本数変更後の金額の合計を求める
104:        // getBasketItemsの引数にはuserIdを設定すべきだが、内部で「tomoharu」
105:        // を固定で設定しているため、ここでは空文字を設定
106:        for (int i = 0; i < basketItemContents.size(); i++) {
107:            basketItemContent = basketItemContents.get(i);
108:            sumItems += (basketItemContent.getPrice() * basketItemContent.getQuantity());
109:        }
110:
111:        total = sumItems + shippingCost;
112:        totalCharge.add(sumItems);
113:        totalCharge.add(shippingCost);
114:        totalCharge.add(total);
115:        return totalCharge;
116:    }
117:
118:    public void removeBasket(String userId) {
119:        Query query = entityManager.createQuery(
120:                "SELECT b FROM Basket AS b " +
121:                "WHERE b.userId = 'tomoharu'");
122:        Basket basket = (Basket)query.getSingleResult();
123:        entityManager.remove(basket);
124:    }
125:
126:}

 10行目でこのセッションBeanがステートレスであることをアノテーションで示しています。13行目で前編で説明した持続性ユニットを指定しています。今回もコーディング中に電球マークが出てきて持続性ユニットを作成するように促されます。電球をクリックすると図5のように「持続性ユニットを作成」画面が表示されます。「持続性ユニット名」はデフォルトで表示されているものを使用します。

 問題はデータソースです。本稿では、データソースの作り方は説明していないので「GlassFishからアプローチするJava」の第4回を参考に作成してください。紹介した記事の通りデータソースの作成ができたら、図5のようにデータソースに「jdbc/imoshop」と入力してください。

 13行目から21行目がEntityマネージャのDIに関するコードです。

図5.「持続性ユニットを作成」画面
図5.「持続性ユニットを作成」画面
リスト5.JPASample01Remoteクラス
001:package jp.kawakubo;
002:
003:import java.util.List;
004:import javax.ejb.Remote;
005:
006:@Remote
007:public interface JPASession01Remote {
008:
009:    public List getMeigaras();
010:    public void insertBasketItems(int[] meigaraIds);
011:    public List getBasketItems(String userId);
012:    public void updateBasketItems(int[] basketItemIds, int[] allQuantity);
013:    public List getTotalCharge(List basketItemContents);
014:    public void removeBasket(String userId);
015:    
016:}

JPASample01Beanクラスのメソッドの説明

getMeigarasメソッド

 JPASesion01RemoteはServletから呼び出されます。ServletからEJBの呼び出し方は同じであるため、当メソッドのみ呼び出し方を説明します。他のメソッドについても同様に呼び出してください。リスト6のようにしてImoshochuCatalogServletクラスからJPASession01RemoteインターフェースのgetMeigarasを呼び出します。JNDIを直接記述せずアノテーションで呼び出せるようになりました。

リスト6.ImoshochuCatalogServletの抜粋
001:public class ImoshochuCatalogServlet extends HttpServlet {
002:
003:    @EJB
004:    private JPASession01Remote jpaBean;

 3行目のEJBアノテーションで実装クラスのインターフェースを指定するだけでServletからEJBを呼び出すことができます。したがって、ServletではjpaBean.getMeigaras()で簡単にEJBのメソッドを呼び出すことができます。Servletについては他に特筆すべきことはありません。サンプルコードで確認してください。

 少し脇道にそれましたが、JPASession01BeanクラスのgetMeigarasメソッドは24行目で返り値のMeigaraオブジェクトのListを宣言しています。25、26行目はEntityManagerのcreateQueryメソッドを使ってQueryオブジェクトを生成しています。引数はJPQL文となります。当メソッドはMeigaraクラスを通してMEIGARAテーブルより明細を取得し、24行目で宣言したmeigarasに設定し戻しています。

 関心はJPQLです。SELECT m FROM Meigara AS mがJPQLそのものですが、「SQLでテーブルを指定するべきところにそのテーブルに相当するEntityクラスを使用する点」、また当JPQLには出てきませんが「カラムを指定すべきところにEntityクラスのフィールドを使用している点」が大きく異なるところです。したがって、27行目のquery.getResultList()によりMeigaraオブジェクトのListを取得しています。28行目でそのリストを返しています。

insertBasketItemsメソッド

 カタログのチェックボックスにチェックを入れ[買い物かごに追加する]ボタンをクリックすると、買い物かごに新しく銘柄を追加するためのメソッドです。カタログでは複数の銘柄にチェックを入れることができます。また、チェックを入れたものが既に買い物かごにある場合は、まったく更新せずそのままにします。

リスト7.Basketの存在を判定するコード
031:public void insertBasketItems(int[] meigaraIds) {
032:    Basket basket = null;
033:    Query query = entityManager.createQuery(
034:            "SELECT b FROM Basket AS b " +
035:            "WHERE b.userId = :userId")
036:            .setParameter("userId", "tomoharu");
037:    List baskets = query.getResultList();
038:    if (baskets.size() == 0) {
039:        basket = new Basket();
040:        basket.setUserId("tomoharu");
041:        entityManager.persist(basket);
042:        entityManager.flush();
043:    } else {
044:        basket = baskets.get(0);
045:    }

 32行目から45行目までのコードは、買い物かごがない場合は買い物かごを作成し、ある場合は既存の買い物かごを取得する処理です。31行目のメソッドの引数がintの配列になっているのは、複数銘柄の選択に対応するためです。

 34行目と35行目がJPQL文です。先述したようにJPQLではテーブルやカラムの代りにEntityクラスとそのフィールド変数を使います。このJPQL文は直感的に理解可能です。35行目の:userIdはプレースホルダーです。36行目でそのプレースホルダーに値を設定しています。本来であれば、プレースホルダーに設定する値も変数でなければあまり意味がありませんが、今回は説明を簡単にするために直接値を設定しています。

 37行目と38行目は「tomoharu」というuserIdを持ったBasketが存在するかを判断しています。37行目でgetSingleResultメソッドを使えばいいように思えますが、存在しない場合、Entityが存在しない旨の例外(javax.persistence.NoResultException:getSingleResult() did not retrieve any entities)が発生します。この例外が発生した場合に38行目から45行目に相当する処理を行うことも可能です。しかし、例外を使って通常の処理を記述するのは本来の例外の意味から外れてしまいます。getSingleResultメソッドはあくまでも1件のみ存在することが分かっている場合のみ使用するメソッドです。存在を確認する場合は、getResultListメソッドの戻り値のListのサイズを判定する方が例外を使用しない作りになります。

 39行目から42行目で新しくBasketオブジェクトを作成しています。44行目で既存のBasketオブジェクトを取り出しています。

リスト8.BasketItemの存在を判定するコード
047:BasketItem basketItem = null;
048:Meigara meigara = null;
049:for (int i = 0; i < meigaraIds.length; i++) {
050:    query = entityManager.createQuery(
051:            "SELECT bi " +
052:            "FROM BasketItem AS bi INNER JOIN bi.meigara AS m, " +
053:            "Basket AS b " +
054:            "WHERE m.id = :meigaraId " +
055:            "AND b.userId = :userId")
056:            .setParameter("meigaraId", meigaraIds[i])
057:            .setParameter("userId", "tomoharu");
058:    List basketItems = query.getResultList();
059:    // 買い物かごに同じ銘柄がない場合のみ追加
060:    if (basketItems.size() == 0) {
061:        basketItem = new BasketItem();
062:        meigara = entityManager.find(Meigara.class, meigaraIds[i]);
063:        basketItem.setBasket(basket);
064:        basketItem.setMeigara(meigara);
065:        basketItem.setQuantity(1);
066:        entityManager.persist(basketItem);
067:    }
068:}

 47行目から68行目までのコードは、取得したBasketオブジェクトに銘柄が存在しない場合のみ、BasketItemオブジェクトを作成する処理です。

 51行目から55行目がJPQL文です。52行目はBasketItemクラスに直接meigaraIdを持っていないため、Meigaraクラスと内部結合しています。この内部結合により、54行目でinsertBasketItemsの引数として渡されたMeigaraのidと一致するかの判定が可能になります。53行目でBasketクラスが出てくるのは唐突に思えますが、BasketクラスとBasketItemクラスでその関連を定義しているため、52行目のBasketItemクラスが53行目のBasketクラスの中に存在していることが分かる仕組みになっています。

getBasketItemsメソッド
リスト9.買い物かごの内容を保持するオブジェクトを返すコード
071:public List getBasketItems(String userId) {
072:    // userIdは固定で「tomoharu」を設定
073:    Query query = entityManager.createQuery(
074:            "SELECT NEW jp.kawakubo.BasketItemContent" +
075:            "(bi.id, m.name, m.price, bi.quantity) " +
076:            "FROM BasketItem AS bi INNER JOIN bi.meigara AS m, " +
077:            "Basket AS b " +
078:            "WHERE b.userId = :userId")
079:            .setParameter("userId", "tomoharu");
080:    List basketItemContents = query.getResultList();
081:    return basketItemContents;
082:}

 BasketItemクラスを見て、買い物かごの明細と一致していないのではと思われた方も多いと思います。BasketItemクラスであえてMeigaraクラスを参照しているのは、Meigaraクラスの内容が変化した場合、柔軟に買い物かごの明細を変化させたいがためです。したがって、getBasketItemsメソッドは画面に表示するBasketItemクラスからidとquantityを、Meigaraクラスからnameとpriceを取得し、Servletに返すという処理を行っています。しかし、これら4つのフィールドを保持するEntityクラスは存在しません。このような場合、リスト10のようにこれら4つのフィールドを持ったクラスを作成し、JPQLのSELECTの直後にNEWでそのクラスを指定します(74行目)。75行目は検索結果です。NEWで指定したクラスのフィールドと75行目は一致する必要があります。

リスト10.BasketItemContentクラスの抜粋
001:package jp.kawakubo;
002:
003:import java.io.Serializable;
004:
005:public class BasketItemContent implements Serializable {
006:
007:    private int id = 0;
008:    private String name = null;
009:    private int price = 0;
010:    private int quantity = 0;
011:    private int itemTotal = 0;
012:
013:    public BasketItemContent() {
014:    }
015:
016:    public BasketItemContent(
017:            int id,
018:            String name,
019:            int price,
020:            int quantity) {
021:        this.id = id;
022:        this.name = name;
023:        this.price = price;
024:        this.quantity = quantity;
025:        this.itemTotal = price * quantity;
026:    }
027:
028:    public int getId() {
029:        return id;
030:    }
updateBasketItemsメソッド

 当メソッドは[買い物かごを再表示する]ボタンをクリックした場合呼び出されるメソッドです。つまり買い物かごの本数の変更をBASKET_ITEMテーブルに反映させるためのメソッドです。

リスト11.BasketItemのquantityを更新するためのコード
085:public void updateBasketItems(int[] basketItemIds, int[] allQuantity) {
086:    for (int i = 0; i < basketItemIds.length; i++) {
087:        Query query = entityManager.createQuery(
088:                "UPDATE BasketItem AS b " +
089:                "SET b.quantity = :quantity " +
090:                "WHERE b.id = :id")
091:                .setParameter("quantity", allQuantity[i])
092:                .setParameter("id", basketItemIds[i]);
093:        query.executeUpdate();
094:    }
095:}

 これまで説明してきたメソッドはSELECT文だけでしたが、今回はUPDATE文です。JPQL文はほぼSQL文と同じなので説明を割愛します。更新の場合、93行目のようにexecuteUpdateメソッドを使用します。

getTotalChargeメソッド

 買い物かごの下の「明細合計」「送料」「総合計」を計算するメソッドです。簡単な処理のためコードは割愛します。

removeBasketメソッド

 userIdでBasketオブジェクトを検索し、EntityマネージャのremoveメソッドでBasketオブジェクトを削除するためのメソッドです。BASKET_ITEMテーブルの定義でCASCADE削除を指定しているため、検索したBasketオブジェクトに関連するBasketItemオブジェクトもすべて削除されます。簡単な処理のためコードは割愛します。

Servletの実装

 ServletからEJBのメソッドを呼び出すには、EJBアノテーションでJPASession01RemoteがEJBであることを宣言します。その変数であるjpaBeanを使って、JPASession01Beanのメソッドを呼び出します。処理自体は簡単なため、サンプルコードで確認していただければと思います。

知っていることと知っていないことの差は大きい

 以前、複数のEntityクラスのフィールドを集めるJPQL文を書くのに苦労しました。今回のgetBasketItemsメソッドがまさにそのような例です。当メソッドで説明したようにNEWが使えると知る前は、次のようなコーディングをしていました。NEWを知っていたならこんな苦労はしていなかったでしょう。知っていればなんてことはない処理も、知らなければ苦痛でしかありません。読者の皆さまからJPQLはこのようにすればもっと簡単に書けるというようなコメントをいただき、よりよいサンプルコードに成長させることができればと思っています。

001:List basketItemContents = new ArrayList();
002:// userIdは固定で「tomoharu」を設定
003:Query query = entityManager.createQuery(
004:        "SELECT  bi.id, m.name, m.price, bi.quantity " +
005:        "FROM BasketItem AS bi INNER JOIN bi.meigara AS m, " +
006:        "Basket AS b " +
007:        "WHERE b.userId = :userId")
008:        .setParameter("userId", "tomoharu");
009:List resultList = query.getResultList();
010:BasketItemContent basketItemContent = null;
011:for (int i = 0; i < resultList.size(); i++) {
012:    Object[] oList = resultList.get(i);
013:    for (int k = 0; k < oList.length; k++) {
014:        System.out.println("oList[" + k +"] = " + oList[k]);
015:    }
016:    basketItemContent = new BasketItemContent();
017:    basketItemContent.setId(((Integer)oList[0]).intValue());
018:    basketItemContent.setName((String)oList[1]);
019:    basketItemContent.setPrice(((Integer)oList[2]).intValue());
020:    basketItemContent.setQuantity(((Integer)oList[3]).intValue());
021:    basketItemContents.add(basketItemContent);
022:}
023:return basketItemContents;

おさらい

 今回説明したことをおさらいします。

  • Entityクラス間のディレクションとカーディナリティを整理しました
  • Entityクラス間の関連の指定の仕方を説明しました
  • JPQLを使って複数のEntityから値を取得する方法や、更新する方法を説明しました

 今回のJava EEプロジェクトのフォルダ構成は図6の通りです。説明したクラスやServlet、JSPはサンプルコードとしてページトップに置いているので、ダウンロードしてご使用ください。また、MySQLのリストア用SQLファイルも添付しています。1から構築するのが面倒な場合はMySQL Workbenchでリストアしてご使用ください。

図6.今回のプロジェクトのフォルダ構成
図6.今回のプロジェクトのフォルダ構成