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

GlassFishからアプローチするJava~入門編~
第6回「Webアプリケーションの作成 ON DELETE CASCADE」

買い物かごへの削除、更新機能の実装

2010/03/10 14:00

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

 この連載では、「GlassFish」という製品を利用して、Java言語に親しんでもらうことを目的としています。第5回ではカタログで選択した銘柄を買い物かごに追加する機能を実装しました。第6回の今回は、買い物かごへの削除・更新機能を実装します。

はじめに

 この連載では、「GlassFish」という製品を利用して、Java言語に親しんでもらうことを目的としています。第6回目の今回は、前回に引き続き買い物かご機能部分の実装を解説していきます。

対象読者

  • Javaでなにかしらのアプリケーションを作成したことのある方
  • Javaの変数の宣言や、if文・for文・while文の制御文など簡単な文法を知っており、アプリケーションを作成したことのある方

 オブジェクト指向プログラミングができなくとも構いません。徐々に学んでいければと考えています。また、学びやすいWebアプリケーションをサンプルとするので、Webアプリケーションとは違った分野を勉強したい方には当連載は向いていません。

本稿で想定する主要技術のバージョン

 Java EE 5を使用するため、主な技術要素のバージョンは以下の通りです。EJBに関しては応用編で扱う予定です。

  • Servlet:2.5
  • JSP:2.1
  • JSTL:1.2
  • JTA:1.1
  • EJB:3.0(応用編)

買い物への削除・更新機能を実装

 第5回ではimoshopデータベースにテーブルを追加・変更する方法の説明と、カタログで選択した銘柄を買い物かごへ追加する機能を実装しました。今回は、買い物かごへの削除・更新機能の実装を説明します。

 「オープンソースApache Tuscanyで楽しむSOA」という連載ではAjaxを含めJavaScriptを多用したため説明が分からないという指摘を受けました。ページの半分以上がAjaxを含むJavaScriptで埋め尽くされてしまい、非常に分かりづらいコードとなってしまいました。

 今回は説明をシンプルにするため、JavaScriptは一切使わないことにします。Ajaxに詳しい方は当連載のサンプルのコードにAjaxを適用し、よりユーザビリティの高いプログラムを作成してください。したがって、当連載ではクライアントとサーバはボタンのクリックで発生するアクションによりはじめて通信が開始されるよう作成します。

仕様の確認

 第5回で買い物かごの仕様を定義しました。もう一度おさらいということで再掲します。

 買い物かごで実装する仕様は以下の通りです。

  1. カタログの芋焼酎の銘柄を選択し[買い物かごに追加]ボタンをクリックすると、買い物かごに芋焼酎の銘柄が追加される。追加した場合の本数の初期値は1本とする。ただし、既に買い物かごに存在する場合は、本数は増加させない。
  2. 本数を増減させたい場合、直接、本数のテキストフィールドに本数を入力する。本数が0本になった場合、買い物かごから削除する。[買い物かごを再表示する]をクリックすると0本にした銘柄が買い物かごからなくなっている
  3. 買い物かごの銘柄の明細には単価と本数をかけた金額が表示される
  4. 買い物かごには明細の金額の合計と送料、合計と送料を足した総合計が表示される
  5. [レジへ進む]ボタンをクリックすると買い物かごは空になる

買い物かごの銘柄ごとの金額の計算

 1の機能は第5回で実装しました。残りの機能を実装するのも難しくはありません。本数を増減させてもその場で金額は変わりません。その代わり[買い物かごを再表示する]ボタンをクリックすると金額が計算されて再表示されます。

BasketItemクラスの変更

 まずは金額を表示させるためにBasketItemクラスに銘柄の金額(単価と本数をかけたもの)をサーバ側で再計算させ表示させます。そのためにはBasketItemに金額フィールド(itemTotal)を追加します。

 いつもはエディタ内で右クリックし[リファクタリング] -[フィールドをカプセル化]と選択することでgetter/setterメソッドを用意するところですが、金額は単価と本数で導出できる項目であるため、getterメソッドは手作りします。getItemTotalはその場で単価と本数をかけたものを呼び出し元に返します。

ImochochuShop.jspの変更

 次にImoshochuShop.jspを変更します。まずは今まで表示していなかった金額を表示するため式言語を使用します。BasketItemのgetItemTotalを定義したことにより、${basket_item.itemTotal}だけで値が習得できるようになります。さらにtype属性値がsubmitのinputタグを用意します。カタログと買い物かごを同じformタグに入れてしまうと非常にサーバ側のプログラムが煩雑になるため、カタログ部分のformはcatalogというid、買い物かごのformはrefresh_basketというidを付け分離します。

 最後にbasket_itemのidを表示させ、Servletでその値を受け取れるよう変更します。図1がImoshochuShop.jspのrefresh_basketというidを持つformタグの部分です。basket_itemのidをhiddenで持たせているのはパラメータに含めたいからですが、このままだと表示できないため、テンプレート・テクストに改めて式言語を加えることで表示できるようにしています。

 ここでtype属性をhiddenではなくtextにし、disabledを指定すればいいようにも思えますが、この場合パラメータに含めることができず、ServletでString[] basketIds = request.getParameterValues("id");で受け取ろうとしても内容はnullになるため使用できません。

図1.ImoshochuShop.jspのrefresh_basketをidとして持つformタグの内容
図1.ImoshochuShop.jspのrefresh_basketをidとして持つformタグの内容

買い物かごを再表示するためのRefreshBasketServletクラスを作成

 Servletの作成の仕方は既に説明しているため割愛します。作成の仕方が分からない方は当連載第2回目の3ページ以降で説明しています。

 買い物かごのへの削除・追加のためのServletを作成します。まずは本数を増減させた場合に金額が正確に表示されるよう実装します。図2のように内容は第5回で作成したBasketServletクラスをほぼ同じです。

図2.RefreshBasketServletクラスのprocessRequestメソッド
001:    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
002:    throws ServletException, IOException {
003:
004:        ArrayList basketItems = new ArrayList();
005:
006:        String[] basketItemIds = request.getParameterValues("id");
007:        String[] allQuantity = request.getParameterValues("quantity");
008:
009:        int basketId = 0;
010:
011:        BasketHelper basketHelper = new BasketHelper();
012:        // ログインしたユーザidをもとにデータベースからbasket情報を
013:        // 取得する。第5回は固定で「tomoharu」とする
014:        basketId = basketHelper.getBasketId("tomoharu");
015:
016:        basketHelper.updateBasketItems(basketItemIds, allQuantity);
017:
018:        basketItems = basketHelper.getBasketItems(basketId);
019:        request.getSession().setAttribute("basket", basketItems);
020:
021:        // ImoshochuShop.jspに遷移する
022:        RequestDispatcher rd = request.getRequestDispatcher("/ImoshochuShop.jsp");
023:        rd.forward(request, response);
024:
025:    } 

 BasketServletクラスと違うところは、JSPから受け取るパラメタの違い(6、7行目)と銘柄の挿入ではなく更新を行っている箇所(16行目)です。ただし、本数が0本の銘柄を削除することが仕様であるため、厳密にはupdateBasketItemsという名称ですが、削除処理も行っています。BasketHelperクラスのupdateBasketItemsはそのままDAOクラスのupdateItemsメソッドを呼び出しているだけであるため割愛します。サンプルコードでご確認ください。

BasketDAOImplクラスの変更

 まずは図3のように定数として定義しているSQLに追加、変更します。

図3.SQLの追加・変更
001:    private static final String SQL_INSERT_BASKET
		= "insert into basket values(0, ?)";
002:    private static final String SQL_GET_BASKETID
		= "select * from basket where user_id = ?";
003:    private static final String SQL_INSERT_BASKET_ITEM
		= "insert into basket_item values(0, ?, ?, 1)";
004:    private static final String SQL_UPDATE_BASKET_ITEM
		= "update basket_item set quantity = ? where id = ?"; // 第6回で追加
005:    private static final String SQL_DELETE_BASKET_ITEM
		= "delete from basket_item where id = ?"; // 第6回で追加
006:    // 第6回でbasket_itemのidを返すよう変更
007:    private static final String SQL_GET_ALL_BASKET_ITEMS
		= "select b.id as basket_item_id, m.id, m.name, m.price, b.meigara_id, b.quantity"
008:            + " from (basket_item as b inner join meigara as m on b.meigara_id = m.id)"
009:            + " where basket_id = ?";

 4行目と5行目が追加したSQL文です。7行目がbasket_itemのidを取得するよう変更しています。説明が遅れましたが、SQL文の中にある「?」がプレースフォルダと呼ばれるもので、後で値を設定できるように用意されたものです。プレースフォルダを使用する場合はStatementではなくPreparedStatementを使用します。具体的にはupdateItemsメソッドで説明します。

 次にupdateItemsメソッドを追加します。図4がupdateItemsメソッドの抜粋です。

図4.BasketDAOImplクラスのupdateItemsメソッド(抜粋)
001:        Connection conn = null;
002:        PreparedStatement updatePreparedStatement = null;
003:        PreparedStatement deletePreparedStatement = null;
004:        ArrayList updatedBasketItemIds = new ArrayList(Arrays.asList(basketItemIds));
005:        ArrayList updatedAllQuantity = new ArrayList(Arrays.asList(allQuantity));
006:        try {
007:            conn = getConnection();
008:            updatePreparedStatement = conn.prepareStatement(SQL_UPDATE_BASKET_ITEM);
009:            deletePreparedStatement = conn.prepareStatement(SQL_DELETE_BASKET_ITEM);
010:            for (int i = 0; i < updatedBasketItemIds.size(); i++) {
011:                int basketItemId = Integer.parseInt(updatedBasketItemIds.get(i));
012:                int quantity = Integer.parseInt(updatedAllQuantity.get(i));
013:                if (quantity == 0) {
014:                    deletePreparedStatement.setInt(1, basketItemId);
015:                    deletePreparedStatement.executeUpdate();
016:                } else {
017:                    updatePreparedStatement.setInt(1, quantity);
018:                    updatePreparedStatement.setInt(2, basketItemId);
019:
020:                    updatePreparedStatement.executeUpdate();
021:                }
022:
023:            }
024:        } catch(SQLException se) {

 少しトリッキーなコーディングになっていますが、可読性を高めるということでご勘弁ください。3、4行目で2つのPreparedStatementを定義しています。3行目が更新用、4行目が削除用となっています。

 このように2つ定義したのは本数(quantity)が0の場合、削除。0以外の場合、更新する必要があるためです。13行目から21行目が削除と更新処理のコードです。13行目で先述のSQL_DELETE_BASKET_ITEMの?に画面から取得したbasket_itemのidを設定しています。このようにプレースフォルダは異なった値を入れるためのもので、プログラミング言語の変数に相当します。ただし、SQLに出現する?の順番に従って1から番号を指定する必要があります。

 14行目で不完全であったSQL文が実行できるようになりました。15行目でSQL文を実行しています。これで本数が0の銘柄は買い物かごから削除されます。17、18、20行目が更新の場合の処理です。一点だけ注意しないといけないのは、データベースに接続し、処理を行い、終了したらリソースを解放するような処理は、ほとんどの場合一から作ることはなく、別のプロジェクトのプログラムからコピーして作成するが故にリソースの解放を忘れてしまいがちになることです。この例で言うと、PreparedStatementを2つ生成しているので、2つともfinallyブロックでcloseメソッドを使ってリソースを解放しなくてはなりません。

 ここまでで、冒頭で説明した仕様の2と3を実装したことになります。

合計、送料、総合計の計算

 ここまでできてしまえば、いろんな機能を盛り込むことが簡単になります。仕様の4は明細の金額の合計、送料(一律1,500円)、総合計を表示することでした。合計を求めるのは至って簡単です。これはJavaScriptを使えばクライアント側でも可能ですが、サーバ側で行う方法を示します。今回は新しいServletを作ることはありません。RefreshBasketServletに処理を追加します。

ImoshochuShop.jspの変更

 図5のように取り立てて目新しいことは行っていません。若干違うところは、今まではsessionScopeから取り出したのが総称型のArrayListでしたが、今回はraw型のArrayListを使用しています。var属性値をtotalとし、${total}として値を取り出していますが、このArrayListをaとするとa.get(0)に明細合計、a.get(1)に送料、a.get(2)に総合計が入っているのと同義です。非常にシンプルな記法です。ただし、この例のように総称型かraw型かにより記法が異なるので注意が必要です。

図5.ImoshochuShop.jspの明細合計、送料、総合計の表示
図5.ImoshochuShop.jspの明細合計、送料、総合計の表示

RefreshBasketServletの変更

 図6が明細合計、送料、総合計を求めるために変更したprocessRequestメソッドです。

図6.RefreshBasketServletクラスのprocessRequestメソッド
001:    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
002:    throws ServletException, IOException {
003:
004:        ArrayList basketItems = new ArrayList();
005:
006:        String[] basketItemIds = request.getParameterValues("id");
007:        String[] allQuantity = request.getParameterValues("quantity");
008:
009:        int basketId = 0;
010:
011:        BasketHelper basketHelper = new BasketHelper();
012:        // ログインしたユーザidをもとにデータベースからbasket情報を
013:        // 取得する。第5回は固定で「tomoharu」とする
014:        basketId = basketHelper.getBasketId("tomoharu");
015:
016:        basketHelper.updateBasketItems(basketItemIds, allQuantity);
017:
018:        basketItems = basketHelper.getBasketItems(basketId);
019:        request.getSession().setAttribute("basket", basketItems);
020:        ArrayList totalCharge = basketHelper.getTotalCharge(basketItems);
021:        request.getSession().setAttribute("totalCharge", totalCharge);
022:
023:        // ImoshochuShop.jspに遷移する
024:        RequestDispatcher rd = request.getRequestDispatcher("/ImoshochuShop.jsp");
025:        rd.forward(request, response);
026:
027:    }

 明細合計、送料、総合計を求めるために必要なのは、20、21行目のみです。BasketHelperクラスのgetTotalChargeメソッドにbasketItemsを渡し、返ってきたArrayListをHttpSessionに格納しているだけです。

ImoshochuCatalogServletクラス、BasketServletクラスの変更

 この2つのクラスも明細合計、送料、総合計を求めなければならないため、図6の20、21行目をそのまま、HttpSessionにbasketItemsを格納している箇所の下に追加してください。

 ここまでで、冒頭で説明した仕様の4を実装したことになります。画面は図7のようになります。

図7.明細合計、送料、総合計の表示実装後の画面
図7.明細合計、送料、総合計の表示実装後の画面

[レジへ進む]ボタンで一括削除

 本来であれば、レジへ進んだ後、送付先・請求先、支払方法の選択などを行い、購入が完了した時点で買い物かごを空にしなければならないのですが、そこまで実装すると本当にネットショップを立ち上げることができるくらいになります。しかし、その実装も今の数十倍になります。現実的ではないため[レジへ進む]ボタンをクリックした時点で購入まで行ったものと仮定します。

一括削除の方法

 basketテーブルから該当するユーザidを持つレコードを削除すれば、それに結び付くbasket_itemも全て削除されそうに思いますが、実際には削除されません。今のbasket_itemテーブルの作りでは、basketテーブルのidを持っているbasket_itemテーブルのレコードを全て削除し、そのあと該当するbasketテーブルのレコードを削除しなければなりません。この場合であればbasketテーブルとbasket_itemの特定のユーザに関連するレコードを全て削除しても大きな負担にはなりませんが、より深い階層になると削除ロジックが複雑になります。

 このような場合、ほとんどのデータベース製品はカスケード削除ができるようになっています。カスケード削除とは大元を削除すれば、それにぶら下がったものも削除されることを指します。今回は意地悪くbasket_itemテーブルを作成するときに、カスケード削除ができるようには指定しませんでした。とは言っても、大がかりな修正が必要なわけではなく、MySQL Workbenchでbasket_itemの定義を変更するだけで済みます。

 MySQL Workbenchで[SQL Development] → [Open Connection to start Querying]と選択肢、その下にあるコネクションをダブルクリックします。まだコネクションを作成していない場合、[New Connection]でコネクションを作成してください。難しい設定ではないことと、今まで添付していたサンプルコードに含まれていた設定ファイルを見れば分かるはずです。コネクションをダブルクリックすると[SQL Editor]タブが開き、その中ほどに[Overview]があります。一から作成した方は4つのスキーマが表示されており、その中に[imoshop]というタブがあるはずです。そのタブをクリックすると、basket、basket_item、meigara、userテーブルが表示されているはずです。basket_itemを右クリックし[Alter Table]を選択します。図8のように、左上に[basket_item]と表示された画面が開きます。下の方にたくさんのタブがありますが、[Foreign Keys]をクリックします。

図8.カスケード削除するための設定変更画面
図8.カスケード削除するための設定変更画面

 少し分かりづらいと思いますが、左側が参照される側、つまりbasketテーブルを、中央が参照する側、つまりbasket_itemテーブルを示しています。図9がカスケード削除を行うための設定です。

図9.カスケード削除するための設定内容
図9.カスケード削除するための設定内容

 図9のBがbasket_itemとbasketテーブルを結び付けるための設定です。つまりbasket_idがbasketテーブルのid項目であることを定義しています。Bのbasket_idの隣にidとありますが、その上に「Referenced Column」となっており、この項目(つまりbasket_id)がidという項目と結び付くことを定義しています。左側の「Foreign Key」にidとあり、まさしく中央でidと指定した項目です。idという項目がどのテーブルに属するものであるかを示すのが「Referenced Table」です。basketテーブルを指定(図9のA)して、basket_idがbasketテーブルのidと結び付くことを定義しているわけです。

 ただこれだけでは、単にbasketテーブルとbasket_itemテーブルの関連を定義したに過ぎません。右側にある「On Delete:」というセレクトボックスから「CASCADE」(図9のC)を選んではじめて、一括削除が可能となるのです。

ImoshochuShop.jspの変更

 カタログの表示や買い物かごの制御とは違うため、図10のように新しくformタグを作成しRemoveBasketServletを呼び出すようコードを追加します。

図10.ImoshochuShop.jspの変更
図10.ImoshochuShop.jspの変更

RemoveBasketServletの作成

 次はImoshochuShop.jspのformタグのaction属性で指定したRemoveBasketServletを作成します。Servletの作成の仕方はいつもの通りでなので割愛します。図11がRemoveBasketServletのprocessRequestメソッドです。これまで説明してきたServletも至って簡単な作りでしたが、一段と簡単になっています。他に修正すべきクラスとしてBasketHelper、BasketDAO、BasketDAOImplクラスがありますが、それぞれremoveBasketメソッドを追加しているのみです。メソッドの作り自体も簡単ですので、説明は割愛します。サンプルコードをご参照ください。

図11.RemoveBasketServletのprocessRequestメソッド
001:    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
002:    throws ServletException, IOException {
003:
004:        int basketId = 0;
005:
006:        BasketHelper basketHelper = new BasketHelper();
007:        // ログインしたユーザidをもとにデータベースからbasket情報を
008:        // 取得する。第5回は固定で「tomoharu」とする
009:        basketId = basketHelper.getBasketId("tomoharu");
010:
011:        basketHelper.removeBasket(basketId);
012:        request.getSession().setAttribute("basket", null);
013:        request.getSession().setAttribute("totalCharge", null);
014:
015:        // ImoshochuShop.jspに遷移する
016:        RequestDispatcher rd = request.getRequestDispatcher("/ImoshochuShop.jsp");
017:        rd.forward(request, response);
018:    } 

 カスケード削除のおかげで、basketテーブルから特定のユーザidを持つテーブルを削除することで、その買い物かごに入っていたbasket_itemテーブルのレコードも削除されます。削除前のbasketテーブルとbasket_itemテーブルの内容が図12と図13です。[レジへ進む]ボタンをクリックした後の買い物かごが図14で、その時のbasketテーブルとbasket_itemテーブルの内容が図15と図16です。両方とも削除されていることが分かります。

図12.削除前のbasketテーブル
図12.削除前のbasketテーブル
図13.削除前のbasket_itemテーブル
図13.削除前のbasket_itemテーブル
図14.削除後の買い物かご
図14.削除後の買い物かご
図15.削除後のbasketテーブル
図15.削除後のbasketテーブル
図16.削除後のbasket_itemテーブル
図16.削除後のbasket_itemテーブル

 ここまでで仕様の5「[レジへ進む]ボタンをクリックすると買い物かごは空になる」の実装が完了したことになります。サンプルコードに含まれるbasketテーブルとbasket_itemテーブルは空になっています。まずはカタログから[買い物かごに追加する]ボタンをクリックし、買い物かごに追加し、仕様通り動作するか確認してください。

サンプルコード

 サンプルコードはページトップにあります。ダウンロードしてご利用ください。ソースコード、設定ファイル、データベースのリストア用ファイルが納められています。プロジェクトは図17のような構造になっています。記事通りに作成していくのが一番ですが、一部割愛しているコードもあります。そういう場合サンプルコードを参考にしていただければと思っています。もちろん忙しい方はまずはものを動かしてみたいという場合もあります。そういう場合は機械的にファイルを貼り付けて試していただいても構いません。

図17.第6回までのプロジェクト構造
図17.第6回までのプロジェクト構造

 次回は複数のユーザが同時に芋焼酎酒店で買い物できるよう認証機能を追加します。