気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
私は現在、「Google App Engine」と「Silverlight」を組み合わせたWebアプリを開発しています。
「Google App Engine」側にはREST型のWebサービスを実装しているのですが、
やはり最初にぶちあたったのが、フレームワークの問題です。

実際、JavaでWebアプリを開発するとなると、ほとんどのケースにおいて、
フレームワークを使うことになると思いますが、「Google App Engine」の場合、
個人で小さなアプリを開発している人も多いのではと思います。

ほんの数画面ほどのものを作るだけなのですが、全く一から「JSP + Servlet」で
作るとコードが煩雑、または冗長になったりしてしまって、日頃きれいなコードを
心掛けている方は、少々不満に感じたりするかもしれません。

だからと言って、フレームワークを使うとなると、それを設定したり、
または使い方を覚えるのが大変で、かえって時間がかかってしまったり
することも有ります。

このジレンマを解決したく、私はGAE専用の超超軽量フレームワークを
作ってみたいと考えました。

私は、このフレームワークを考えるにあたり
「最低限JavaとServlet+JSPを知っていれば、すぐに使える!」
を最大の目標として設定しました。

私が今、ここでフレームワークと言っているのは、
フレームワークを構成するクラス群と
ある一つの考え方です。

■フレームワークを構成するクラス
フレームワークを構成する主なクラスは以下の3つです。

①Actionクラス
StrutsのActionクラスと同じで、実処理を記述したり、
遷移先をコントロールしたりするクラス。

②Dispatcherサーブレット
リクエストされたURLとルールに従って、対応するActionクラスの
対応するメソッドをコールするクラス。(設定ファイルは使いません。)

③HttpBeanManager
リクエストパラメータを、JDOエンティティのインスタンスに設定し、
検証を行うクラス。

①と②については最低限これが無いと、Servletが画面毎に存在する
原始のプログラムになってしまうのと考えたからです。

③についてはrequest.getParameter()をたくさん記述して、
それを一つ一つ各々で検証する部分に時間を割きたくなかったからです。

データまわりはGAEから提供されているので、これで
最低限の見栄えのと生産性が実現できると考えました。

■フレームワークを構成する考え方
次に先に述べた「ある一つの考え方」についてですが、
お気づきかと思いますが、このフレームワークではプレゼンテーション層の
モジュールを一切提供しません。

どうするかと言いますと、原点に立ち返り、JSPの「式」と「スクリプトレット」で
プレゼンテーション層を記述するのです。これが「ある一つの考え方」の内容です。

現在、ほとんどのフレームワークでは、独自のタグライブラリが提供されています。
今や、タグライブラリでJSPを記述するのは当たり前で、「スクリプトレット」と
言われても、言葉すら聞いたことが無いJavaのエンジニアの方もいると思います。

「スクリプトレット」はスパゲティーソースの原因としてバッシングを受け、
ほとんど使われることが無くなっていますが、私は、
「ビジネスロジックはJava側に記述し、スクリプトレットでは記述しない」
という前提の元に、「スクリプトレット」を使用することは、そこまで悪ではないと
考えています。

タグライブラリは確かにJSPをきれいにするのですが、その反面、覚えるのが大変で、
JavaによるWeb開発を難しくする要因の一つになっています。

そして、何よりも、タグライブラリを作ってしまったら、
「最低限JavaとServlet+JSPを知っていれば、すぐに使える!」と
矛盾することになります。

私は、このフレームワークを「Zinbei Framework」と名付けました!
理由は。。。ありません。思いつきです。

少しずつですが、作り上げてこのブログで発表していきたいと思います。

※私は、既存のフレームワークの有効性を否定しているわけではありません。
Javaの開発にもこのような選択肢があっても良いのではと考えているのです。
スポンサーサイト
少し空いてしまいましたが、本日はGAE開発日記の11回目です。台帳一覧からクリックされた台帳の詳細を表示する部分のコードを紹介します。
以下が台帳一覧画面と台帳詳細画面のイメージです。





まず台帳一覧側のJSPのコードを見てください。

[ledgerlist.jsp]
・・・・(省略)

<table width="500px">
<tr>
    <td style="background-color: #ccccff;">
       &nbsp;
    </td>
    <td width="65%" style="background-color: #ccccff;">
       台帳名
    </td>
    <td style="background-color: #ccccff;">
       作成者</td>
    </tr>

<%
   List<Ledger> ledgerList = (List<Ledger>)request.getAttribute("ledger_list");

   for(Ledger ledger : ledgerList)
   {
%>
   <tr>
       <td>
          <input name="chk_id" value="<%=  ledger.getKey().getId() %>" type="checkbox"/>
       </td>
       <td>
          <a href="#" onClick="showDetail('<%= ledger.getKey().getId() %>')">
              <%= ledger.getLedgerName() %>
          </a>
       </td>
       <td>
              <%= ledger.getCreatedBy() %>
       </td>
   </tr>
<%
   }
%>
</table>

・・・・(省略)

<input type="hidden" name="ledger_id"/>


台帳名のリンクがクリックされたら、showDetail()というJavaScriptの関数が呼ばれるようにしています。
Scriptの内容はこんな感じです。

[ledgerlist.jsp]
function showDetail(ledgerId)
{
    document.fm.action = "/ctl/LedgerDetail";
    document.fm.ledger_id.value = ledgerId;
    document.fm.submit();
}

formの要素としてname属性にledger_idという値を持つhiddenタグをあらかじめ作成しておき、
スクリプトでそのタグの値にクリックされた台帳レコードのIDを設定してフォームをサブミットしています。

次にリクエストを処理するハンドラクラスのコードを見てください。

[LedgerDetail.java]

    public void execute(HttpServletRequest req, HttpServletResponse resp)
            throws Exception {
        
        //詳細を表示する台帳のIDを取得する
        String id = req.getParameter("ledger_id");
        
        //台帳を取得する
        LedgerDao dao = new LedgerDao();
        Ledger ledger = dao.getLedger(id);
        
        //リクエストに台帳を設定する
        req.setAttribute("ledger", ledger);
        
        //Forwards to JSP.
        RequestDispatcher dispatcher =  req.getRequestDispatcher("/pages/ledgerdetail.jsp");
        dispatcher.forward(req, resp);

    }

リクエストから台帳のコードを取得し、Daoクラスに渡して台帳クラスのインスタンスを取得しています。
そして、取得した台帳クラスのインスタンスをRequestオブジェクトに設定し、JSPにリクエストのフォワードしています。

次はDaoクラスのコードです。

[LedgerDao.java]
    public Ledger getLedger(String id)
    {
        //Initialize Persistent Manager.
        PersistenceManager pm = PMFHelper.get().getPersistenceManager();
        
        Ledger ledger = null;
        
        try
        {
            //String型のIDをlong型に変換し、Ledgerクラスのインスタンスを
            //データストアから取得する
            long longId = Long.parseLong(id);
            ledger = pm.getObjectById(Ledger.class,longId);

            //データストアからデータをロードするための処理
            ledger.getCreatedBy(); //台帳の作成ユーザ
            
            //台帳を使用可能なユーザ
            for(LedgerUser user : ledger.getLedgerUser())
            {
                user.getUser();
            }
        }
        finally
        {
            pm.close();
        }
    
        return ledger;
    }

IDからインスタンスを取得するためのメソッドはPersistenceManagerクラスのgetObjectById()メソッドです。

黄色時の部分はデータストアから各属性のデータをロードするための処理です。GAEのデータストアでは、
オブジェクトの属性の型が他のクラスの場合、接続が開かれているうちにその属性のアクセスしておかないと
ロードが行われません。

今回の例で言うと台帳クラスの台帳名(ledgerName)は自動的にロードされますが、作成ユーザ(createdBy)や
台帳ユーザ一覧(List<LedgerUser>)はアクセスしないとロードされません。

台帳の詳細画面では台帳を使用可能なユーザ等も表示するためにこの処理が必須になります。

最後に詳細画面のJSPです。

[ledgerdetail.jsp]
・・・・(省略)
<%
  Ledger ledger = (Ledger)request.getAttribute("ledger");
%>
・・・・(省略)
<div style="text-align: left;font-weight: bold;">台帳名</div>
<div style="text-align: left;"><input type="text" value="<%= ledger.getLedgerName() %>" size="50"></div>
・・・・(省略)
<table width="400px">
<tr id="acthdr" style="background-color: #ccccff;"><th width="5%"></th><td>アカウント</td></tr>

<!-- 使用可能ユーザ一覧 -->
<% 
    for(LedgerUser user : ledger.getLedgerUser())
    {
%>
<tr>
   <td>
<%
      //作成ユーザ以外の使用可能ユーザを削除できるようにチェックボックスの表示を制御する
      if(!user.getUser().getEmail().equals(ledger.getCreatedBy().getEmail()))
      {
%>
       <input type="checkbox"/>
<%
      }
      else
      {
%>
       &nbsp;
<%
      }
%>
   </td>
   <td>
       <%= user.getUser().getEmail() %>
   </td>
 </tr>
<%
    }
%>
</table>

少しづつですがまた進めていきます。

さて、本日は台帳を削除する部分のコードを紹介します。前回は、台帳の一覧を
取得するコードを紹介しましたが、現在のところ台帳の一覧画面はこんな感じになっております。



台帳の削除は一覧の各行の左端にあるチェックボックスにチェックを付けて「台帳を削除する」ボタンを
クリックすることで行えるようにします。

削除部分に入る前に一覧のJSPを少し紹介します。

[ledgerlist.jsp]
・・・・(省略)
<%
    @SuppressWarnings("unchecked")
    List<Ledger> ledgerList = (List<Ledger>)request.getAttribute("ledger_list");
%>
・・・・(省略)

<table width="500px">
<tr><td> </td><td>台帳名</td><td>作成者</td></tr>
<%
   for(Ledger ledger : ledgerList)
   {
%>

   <tr>
     <td><input name="chk_id" value="<%= ledger.getKey().getId() %>" type="checkbox"/></td>
     <td><a href="#" onClick="showDetail('<%= ledger.getKey().getId() %>')"><%= ledger.getLedgerName() %></a></td>
     <td><%= ledger.getCreatedBy() %></td>
   </tr>
<%
   }
%>
</table>

チェックボックスの「value」には、前回取得したLedgerクラスのKeyのIDを設定しています。削除する際には、削除する台帳を特定するために
このIDが必要になります。

次に削除のリクエストを処理するハンドラクラスのコードを紹介します。

[DeleteLedger.java]
public class DeleteLedger implements RequestHandler {

   @Override
   public void execute(HttpServletRequest req, HttpServletResponse resp)
         throws Exception {
      
      //削除対象の台帳IDを取得する
      String[] ids = req.getParameterValues("chk_id");
      
      //Daoを生成する
      LedgerDao dao = new LedgerDao();
      dao.DeleteLedger(ids);
      
      //一覧のハンドラにリクエストを転送する
      RequestDispatcher dispatcher =  req.getRequestDispatcher("/ctl/LedgerList");
      dispatcher.forward(req, resp);
   }
}

チェックが付いたチェックボックスの[value]を削除対象の台帳のIDとして、取得しそれをDAOクラスの台帳削除メソッドに
渡しています。

そして、肝心の台帳を削除する部分のコードです。

[LedgerDao.java]
public class LedgerDao {
   public void DeleteLedger(String[] ids)
   {
       //Initialize persistent manager.
       PersistenceManager pm = PMFHelper.get().getPersistenceManager();
      
       javax.jdo.Transaction tx = pm.currentTransaction();
       
       try
       {
          tx.begin();
          
          for(String id : ids)
          {
             //IDからLedgerクラスのインスタンスを取得する
             long longId = Long.parseLong(id);
             Ledger ledger = pm.getObjectById(Ledger.class,longId);

             //台帳を削除する
             pm.deletePersistent(ledger);
          }
          
          tx.commit();
       }
       finally
       {
          if(tx.isActive())
          {
             try
             {
                tx.rollback();
             }
             catch(Exception ex){}
          }
       
           pm.close();
       }
   }
}

エンティティの削除はPersistenceManagerのdeletePersistent()メソッドにエンティティのインスタンスを渡して行います。
従って、削除する前にまずクライアントから受け取ったIDで該当する台帳クラスのインスタンスを取得を行う必要があります。

キーのIDが有る場合は、PersistenceManagerクラスのgetObjectById()メソッドでキーのIDに該当するエンティティを取得することが
出来ます。ドキュメントによるとQueryを使っても出来るようですが、こちらの方が簡単ですね。

他のエンティティを所有するエンティティが削除されると、そのエンティティによって所有されているエンティティも同時に
削除されます。今回のアプリの場合では台帳クラスは物品を始めてとして、ほとんどのデータクラスのエンティティを所有しています。

従って、エンティティの削除を行う場合は、トランザクションを使用した方が良いです。

今日は、ユーザが利用できる台帳の一覧を取得する部分を紹介します。
各ログインユーザが利用できる台帳は、「LedgerUser」クラスで管理しています。

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class LedgerUser {

   @PrimaryKey
   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
   private Key key;
   
   @Persistent
   private Ledger ledger;
   
   @Persistent
   private User user;

・・・・・・
}

前回の記事でも紹介しましたように「LedgerUser」クラスのエンティティは台帳を
表す「Ledger」クラスのインスタンスを登録する際に、同時にデータストアに登録しています。

ここまで作成した機能では台帳の作成ユーザを、台帳を使用可能なユーザとして登録するように
していますが、後から作成する機能で他のユーザも使用可能なユーザとして登録できるようにします。

JDOのクエリ
本題の台帳一覧の取得ですが、JDOのクエリを使って行います。JDOのクエリは、SQLのクエリと何となく似ているので
そう難しくはありません。上記で紹介した「LedgerUser」クラスのエンティティのうち、「User」プロパティがログインユーザと
一致するインスタンスを取得します。

以下はログインユーザが使用できる台帳の一覧を取得するDaoクラスのメソッドです。

   public List<Ledger> getLedgerList(User user)
   {
      //Initialize PersistenceManager.
      PersistenceManager pm = PMFHelper.get().getPersistenceManager();
      
      List<Ledger> ledgerList = new ArrayList<Ledger>();
      
      //クエリを生成する
      Query query = pm.newQuery(LedgerUser.class,"user == userparam");
      query.declareParameters("com.google.appengine.api.users.User userparam");
        
      try
      {
            @SuppressWarnings("unchecked")
            List<LedgerUser> results = (List<LedgerUser>) query.execute(user);
            
            for(LedgerUser ledgerUser : results)
            {
               Ledger ledger = ledgerUser.getLedger();
               
               //プロパティをロードする
               //これはとても重要。やらないと台帳名がとれません!!
               ledger.getLedgerName();
               ledger.getCreatedBy();
               
               ledgerList.add(ledgerUser.getLedger());
            }
      }
      finally
      {
         query.closeAll();
         pm.close();
      }
      
      return ledgerList;
   }

メソッドの引数はログインユーザの情報を持つUserクラスのインスタンスです。LedgerUserクラスのuserプロパティに対して、それを
指定し、取得するレコードを絞り込んでいます。

クエリの結果は、LedgerUserクラスのリストとして返されます。そのリストからLedgerUserクラスのインスタンスを1つずつ取り出しています。
そして、LedgerUserクラスのインスタンスから目的の「Ledger」クラスのインスタンスを取得し、リストにつめています。

以下の2行は、それぞれ、台帳名と、作成ユーザの情報を取得するために必要な処理です。

ledger.getLedgerName();
ledger.getCreatedBy();

LedgerUserクラスのプロパティである「ledger」はLedgerUserのエンティティがインスタンス化された時点では、
プロパティへの値のロードは行われていません。値が必要になった時点で始めてロードされるのです。

そのため、クエリが開いている間に必要なプロパティを参照しておかないと
Ledgerクラスの該当プロパティはnullのままで値を取得することが出来ません。
私はしばらくこれにはまってしまいました。

今回は、まず台帳登録の機能を作成してみます。その前に物品管理システムの
画面構成を整理してみます。以下が画面遷移図です。



まずは台帳を登録し、登録した台帳に対して物品を登録していく流れです。
なので、まず台帳を登録する画面から作っていきたいと思います。
画面イメージはこんな感じです。



クラス設計
まずは、簡単にクラス設計を行ってみます。



台帳登録を処理する「RequestHandler」の実装クラスは、「RegistLedger」クラスです。
しかし、データストアへの登録処理自体はさらにDAOの役割をする「LedgerDao」クラスで
行います。

Web版の他にWindowsアプリ版を計画していて、その際には、出力がHTMLでは無く、
XMLになります。

「RequestHandler」の実装クラスのメソッドには、JSPのファイル名を直接記述するので、
Windowsアプリ版を開発する際に、これらを使いまわすことは無いと考えています。

一方、データストアに対する操作は、Web版とWindowsアプリ版で共通の処理になるので、
データストアを操作する部分を外だししておくことで、後で使いまわすことが出来ると
考えました。

データストアへの登録処理
台帳を登録する際に、台帳を作成したログインユーザを台帳を使用可能なユーザとして登録します。
「台帳」は「Ledger」クラス、「台帳のユーザ」は「LedgerUser」クラスで管理します。

それでは、まず「RegistLedger」のコードを紹介します。

「RegistLedger.java」
public class RegistLedger implements RequestHandler {

   @Override
   public void execute(HttpServletRequest req, HttpServletResponse resp)
      throws Exception {
      
      //台帳名をパラメータから取得する
      String ledgerName = req.getParameter("ledger_name");
      
      //ログインユーザを取得する
      UserService service = UserServiceFactory.getUserService();
      User user = service.getCurrentUser();
      
      //台帳クラスの属性を設定する
      Ledger ledger = new Ledger();
      ledger.setLedgerName(ledgerName);
      ledger.setCreatedOn(Calendar.getInstance().getTime());
      ledger.setCreatedBy(user);
      
      //台帳ユーザクラスの属性を設定する
      LedgerUser ledgerUser = new LedgerUser();
      ledgerUser.setLedger(ledger);
      ledgerUser.setUser(user);
      
      //台帳に台帳ユーザを設定する
      ledger.getLedgerUser().add(ledgerUser);
      
      //データストアへの登録処理
      LedgerDao dao = new LedgerDao();
      dao.RegistLedger(ledger);
      
      //Forwards to JSP.
      RequestDispatcher dispatcher =  req.getRequestDispatcher("/ctl/LedgerList");
      dispatcher.forward(req, resp);
   }
}

「Ledger」クラスと「LedgerUser」クラスのインスタンスを生成し、それをDAOクラスに渡しています。
「Ledger」クラスが「LedgerUser」クラスを所有する関係になっているので、「Ledger」のインスタンスを登録すれば、
自動的に「LedgerUser」のインスタンスも登録されることになります。

上記のコードでは、ユーザが入力した台帳のサイズチェックを行っていません。
これは、GAEのデータストアのString型はバイト数ではなく文字数によって、最大サイズが決められているため、
クライアント側のJavaScriptでもチェック可能なためです。

GAEのドキュメントのこの記述はちょっと、混乱させられてしまいますね。。
「A value longer than 500 bytes throws a JDOFatalUserException.」

そして、以下が、「LedgerDao」のコードです。

「LedgerDao.java」
public class LedgerDao {

   public void RegistLedger(Ledger ledger)
   {
      //Initialize PersistenceManager.
      PersistenceManager pm = PMFHelper.get().getPersistenceManager();
      
      javax.jdo.Transaction tx = pm.currentTransaction();
      
      try
      {
         tx.begin();
         
         pm.makePersistent(ledger);
         
         tx.commit();
      }
      finally
      {
         if (tx.isActive())
         {
            try
            {
               tx.rollback();
            }
            catch(Exception ex){}
         }
         
         pm.close();
      }
   }
}

PersistenceManagerのmakePersistent()の呼び出しは1回ですが、内部的には、
「Ledger」のインスタンスと「LedgerUser」クラスのインスタンスの登録が行われるため、
トランザクションを使用する必要があります。

これで台帳の登録は完了です。

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。