気楽なソフト工房

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



mykonos2008

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

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
本日はLINQを使う上でおさえておきたい基本的な事柄についてお話をします。

LINQプロバイダ

前回の記事ではPersonという自作クラスの配列に対してシンプルなLINQクエリを実行するサンプルを
紹介しましたが、LINQでは通常のオブジェクトの他にデータベースやXML、Webサービスなど様々なデータソースに対して
クエリを実行することが出来ます。

標準ライブラリで提供されているものだけでも以下のものがあります。

・LINQ to Object
・LINQ to DataSet
・LINQ to SQL
・LINQ to Entities
・LINQ to XML

「LINQ to Object」は前回の記事で紹介したユーザ定義クラスのコレクションに対してクエリを実行するタイプのものです。
「LINQ to DataSet」、「LINQ to SQL」、「LINQ to Entities」はいずれもデータベースに対してクエリを実行するものです。
「LINQ to XML」はその名の通りXMLに対してクエリを実行するためのものです。

「LINQ to Object」はIEnumerable、IEnumerable<T>を直接使用して、ユーザ定義クラスのコレクションに対してクエリの
実行を行います。その他のものについてはLINQプロバイダと呼ばれる中間クラスを通して、目的のデータソースに対して、
クエリが実行されます。

LINQプロバイダはAmazon、Google、Twitter、Excelなど様々なデータソースに対するものが提供されています。
「Charlie Calvert's Community Blog」に詳細が紹介されています。

また、System.Linq名前空間の「IQueryable」または、「IQueryable<T>」と「IQueryProvider」を実装するクラスを作成し、
独自のLINQプロバイダを作成することも可能です。

LINQとC#3.0の言語仕様

LINQはC#3.0と.NET Framework3.5から導入された新機能ですが、その正体は.NET Framework3.5から追加されたAPIの1つなのです。
そして、C#3.0にはそれらLINQに関するAPIを使いやすくするための「シンタックスシュガー」が追加されているのです。

※シンタックスシュガー(糖衣構文)とはプログラムコードを読み書きしやすくするための構文のことを言います。

実は、前回の記事で扱った以下のクエリも新しく導入されたシンタックスシュガーの1つなのです。

  IEnumerable query = from person in persons
                        orderby person.Age
                        select person;

これをシンタックスシュガーを使わない形で記述すると以下にようになります。

   IEnumerable query = persons.OrderBy((x) => x.Age).Select((x) => x);

内容はまた後日説明いたしますが、後者の記述の仕方より、前者の方が明らかに見易いですよね。

このようにLINQを扱うためにはC#3.0から導入された新たな言語仕様を学ぶ必要が出てきます。

余談ですが、C#の普及が進まない理由の1つに技術革新の速度が早すぎるというものがあると記事で読んだことがあります。
これは技術者としては嬉しいことである反面、キャッチアップしていくことが大変であるという側面もあります。

そうこうしているうちにVisual Studioの次期バージョンと共にC#4.0もリリースされることになっています。
みなさまはC#の技術革新の速度についてどのようにお感じになられていますでしょうか?

さて、次回からLINQ to Objectを使ってLINQクエリの仕様を掘り下げていきたいと思います。

スポンサーサイト
今回の記事からC#3.0とNET Framework3.5から導入されたLINQの入門記事を連載していきたいと思います。

LINQとは「統合言語クエリ」と略されるもので、C#のコードの中にSQLに似た構文のクエリを記述し、それにより
配列など様々なデータの集合から検索や集計を行うことが出来るというものです。

今回はまずサンプルにより直感的にLINQに実感してもらえればと思います。

まず、人の氏名と年齢を管理するためのPersonクラスを定義します。

  class Person
  {
      public String LastName {  get;  set; }
      public String FirstName { get; set; }
      public int Age { get; set; }
  }

そして、Personクラスの配列を作成します。

  Person[] persons = new Person[]
  {
     new Person(){ LastName="Tanaka",FirstName = "ichiro", Age = 20},
     new Person(){ LastName = "Sato",FirstName = "Kazuhiro",Age = 25},
     new Person(){ LastName = "Wada",FirstName = "Takashi",Age = 30},
     new Person(){ LastName = "Asada",FirstName = "Kenji",Age = 40},
     new Person(){ LastName = "Tanaka",FirstName = "Tatsuta",Age = 26},
     new Person(){ LastName = "Yoshino",FirstName = "Taro",Age = 27}
  };

※同じくC#3.0から登場したオブジェクト初期化を使用してPersonクラスのインスタンス化を行っています。

次にこのPersonクラスの配列からLINQクエリを使用して、年齢の若い順にインスタンスを抽出し、氏名と年齢をコンソールに表示します。

     //LINQクエリ
     IEnumerable<Person> query = from person in persons
                  orderby person.Age
                  select person;

     //クエリを実行し、結果を列挙する
     foreach (Person person in query)
     {
          Console.WriteLine("{0} {1} Age:{2}", person.LastName, person.FirstName, person.Age);
     }

結果は以下のように表示されます。

Tanaka ichiro Age:20
Sato Kazuhiro Age:25
Tanaka Tatsuta Age:26
Yoshino Taro Age:27
Wada Takashi Age:30
Asada Kenji Age:40

LINQ以前のコードだと自作クラスをソートするのは少し面倒くさい作業でしたが、LINQを使うと
こんなに簡単にソート処理を行うことができます。

サンプルを使ってLINQを少しだけ解説します。(詳細は次回以降で。)
LINQクエリとは指定されたデータの集合(今回のサンプルでは配列)を列挙する方法を指定するものなのです。従って
クエリを受ける変数の型をIEnumerable<Person>と宣言しています。

※IEnumerableはforeach可能なクラスが実装するインターフェースです。

LINQクエリはSQLに慣れた方であれば何となく推測できると思いますが、少し詳しくを見ていきます。

まず、from句はデータの取得元を指定する部分で、[from A in B]という形をとります。Bにはforeachできるタイプの
もの(IEnumerable、IEnumerable<T>を実装するクラス)なら何でも指定可能です。Bをデータソースと言います。
AはBから順番に取り出されたものを格納する範囲変数と呼ばれるものです。

この範囲変数を使用して抽出条件を指定したり、並び順を指定したりします。
上記のサンプルでも、order by person.Ageのように使用していますよね。

orderby句は並び順を指定するためのものです。
select句はクエリの結果の型を指定するためのものです。上記サンプルをselect person.Ageとすると、年齢だけを
クエリの結果として返すようになります。

最初にも説明しましたようにLINQクエリはデータソースから各要素を列挙する方法を指定するためのもので、
列挙されたデータそのものではありません。

データの列挙自体はforeachを実行することにより始めて実行されます。

今回のサンプルでは、紹介しませんでしたがLINQクエリではwhere句、group句、join句などを使用し
高度なクエリを記述することが出来ます。

LINQクエリは通常のオブジェクトの他にデータベースやXMLに対してもクエリを実行することが可能ですが、
私としては最大の魅力はやっぱり通常のオブジェクトに対してクエリを実行できることだと思います。

個人的に作るプログラムではデータベースを使用しないケースも多いので、LINQの機能はとても重宝すると思います。

次回はLINQの基礎を説明します。

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



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

削除部分に入る前に一覧の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を使っても出来るようですが、こちらの方が簡単ですね。

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

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

C#の標準APIには「DateTimePicker」などのようにコントロールとして、カレンダーを扱うクラスは有りますが、
データとしてカレンダーを扱うクラスはありません。(たぶん)

最近、Google Calendar APIを利用したカレンダーアプリケーションを作成しようとしているのですが、
そんなクラスが欲しいと思って作成しました。

このクラスで実行する処理は、与えられた日付を含む当月のカレンダーデータを作成するというものです。
通常のカレンダーでは、前月の最終週と当月の最初の週が同じ場合、その週に関しては前月の日付と当月の日付が
混ざった形になります。今回、作成したクラスでは、その場合の処理にも対応しています。

まず、カレンダーデータを管理するために、月を表す「CalendarMonth」、週を表す「CalendarWeek」、日を表す「CalendarDay」
という3つのクラスを作成しました。

[CalendarDay.cs]
    public class CalendarDate
    {
        public int Year
        {
            get;
            set;
        }

        public int Month
        {
            get;
            set;
        }

        public int Day
        {
            get;
            set;
        }
    }

[CalendarWeek.cs]
    public class CalendarWeek
    {
        private CalendarDay[] _days = null;

        public CalendarWeek(CalendarDay[] days)
        {
            _days = days;
        }

        public CalendarDay[] Days
        {
            get
            {
                return _days;
            }

            set
            {
                _days = value;
            }
        }
    }

[CalendarMonth.cs]
    public class CalendarMonth
    {
        public CalendarWeek[] Weeks
        {
            get;
            set;
        }
    }

CalendarMonthクラスがCalendarWeekクラスを、CalendarWeekクラスがCalendarDayクラスを所有する関係です。

そして、指定された日付を含むカレンダーデータを作成する処理が以下になります。

    public class Calendar
    {
        //引数はYYYY-MM-DD形式の文字列
        public CalendarMonth GetCalendarMonth(String date)
        {
            //月の週のリスト
            List weeks = new List();

            //日付を管理する変数
            int day = 1;

            //対象の日付オブジェクトを取得する
            DateTime target = DateTime.Parse(date);

            //当月の日数を求める
            int daysInMonth = DateTime.DaysInMonth(target.Year, target.Month);

            //当月1日の曜日を求める
            DateTime firstDay = target.AddDays(-(target.Day - 1));
            int dayOfWeek = (int)firstDay.DayOfWeek;

            //前月の最終日を取得する
            DateTime prevMonth = target.AddMonths(-1);
            int daysInPrevMonth = DateTime.DaysInMonth(prevMonth.Year, prevMonth.Month);

            //第一週のリストを作成する
            CalendarDay[] weekDays = new CalendarDay[7];

              //今月1日から週の終わりまでを配列にいれる
            for (int i = dayOfWeek; i < 7; i++,day++)
            {
                CalendarDay calDate = new CalendarDay();
                calDate.Year = target.Year;
                calDate.Month = target.Month;
                calDate.Day = day;
                weekDays[i] = calDate;
            }

            //前月月末から週の始めまでを配列に入れる
            for (int i = dayOfWeek - 1, x = daysInPrevMonth; i >= 0; i--, x--)
            {
                CalendarDay calDate = new CalendarDay();
                calDate.Year = prevMonth.Year;
                calDate.Month = prevMonth.Month;
                calDate.Day = x;
                weekDays[i] = calDate;
            }

            weeks.Add(new CalendarWeek(weekDays));

            //月末までの残りの日数を週毎にコレクションに入れる
            for (; day <= daysInMonth;)
            {
                weekDays = new CalendarDay[7];

                for (int i = 0; i < 7; i++)
                {
                    CalendarDay calDate = new CalendarDay();
                    calDate.Year = target.Year;
                    calDate.Month = target.Month;
                    calDate.Day = day;

                    weekDays[i] = calDate;
                    day++;

                    if (day > daysInMonth)
                    {
                        break;
                    }
                }

                weeks.Add(new CalendarWeek(weekDays));
            }

            //月に週を設定する
            CalendarMonth month = new CalendarMonth();
            month.Weeks = weeks.ToArray();

            return month;
        }
    }

前月と当月が混ざる週のロジックは少しややこしいですが、最初に当月1日の曜日を求めて、それより前の曜日を前月の日付で、
後の曜日を当月の日付で埋めるようにしています。

祝祭日に関しては扱っていませんが、Google Calendar APIから取得する方法が有るようですので、
また今度、トライしてみたいと思います。

今日は、ユーザが利用できる台帳の一覧を取得する部分を紹介します。
各ログインユーザが利用できる台帳は、「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のままで値を取得することが出来ません。
私はしばらくこれにはまってしまいました。

久しぶりのC#の記事を書かせていただきます。最近、WPFの透過ウィンドウを使った
アプリを企画しています。そこで本日は透過ウィンドウアプリを作るためのTipsを紹介します。

ウィンドウを透過する
WPFでウィンドウを透過するのはとても簡単に出来ます。
Windowクラスの「AllowsTransparency」をtrueに、「Background」を「Transparent」に
WindowStyleを「None」に設定するだけで完了です。

ちなみに「AllowsTransparency」をtrueにした場合、必ず、WindowStyleを「None」にしないと
Exceptionが発生します。

 <Window ・・・ AllowsTransparency="True" Background="Transparent" WindowStyle="None">

このままでは本当に何も表示されないので、ウィンドウに「Ellipse」オブジェクトを配置しました。

WindowStyleを「None」に設定しているため、ウィンドウのタイトルバーが表示されません。
そのため、ウィンドウを閉じたり、移動することが出来なくなります。

コンテキストメニューでアプリケーションを終了する
まず、アプリケーションをコンテキストメニューで終了できるようにします。

まず以下のようにして、XAMLでウィンドウにコンテキストメニューを設定します。

    <Window.ContextMenu>
        <ContextMenu Name="_menu" StaysOpen="true">
            <MenuItem Header="Exit" Click="Exit_Click" />
        </ContextMenu>
    </Window.ContextMenu>

そして、コンテキストメニューがクリックされるとウィンドウを閉じるようにします。

   //Exitメニューのハンドラ
   //Windowを閉じる
   private void Exit_Click(object sender, RoutedEventArgs e)
   {
       Close();
   }

マウスでウィンドウを移動させる
タイトルバーが無いので、このままではウィンドウを移動させることが出来ません。

以下のようにすることで、ウィンドウ内のオブジェクト、今回のアプリの場合、
「Ellipse」オブジェクト内でマウスの左ボタンをクリックし、マウスを動かすことで
ウィンドウを移動させることが出来るようになります。

  protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
  {
      base.OnMouseLeftButtonDown(e);

      try
      {
          DragMove();
      }
      catch { }
  }

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



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



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



台帳登録を処理する「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」クラスのインスタンスの登録が行われるため、
トランザクションを使用する必要があります。

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

物品管理システムのアーキテクチャをまだ決めていなかったのですが、
結局、今回は純粋なJavaのWebアプリケーションとして作成してみることにしました。

その後、WPFでWindowsアプリケーションのクライアントを作成してみたいなと計画しています。
(どんどん、ブログのタイトルにそぐわない内容になってきていてすみません。)

さて、JavaでWebアプリケーションを開発するとなると、どうしても「何のフレームワークを使うのか?」と
いう話になるのですが、今回はあえて既存のフレームワークを使わないことにします。

開発する物品管理システムは、多くても10画面ほどの小さなシステムですし、開発者も私1人です。
GAEを使って小規模のシステムを開発したいと思っている方もたくさんいると思うのですが、
私は、その小規模のシステムを開発するのに、難易度が高いJavaのフレームワークをどうしても使わないといけない
ということではないと思っています。

昨今、Javaは手間がかかって難しいというイメージが定着していますが、Javaだって極端な話、
例えばJSPだけで開発することも出来るわけです。

もちろん私はStrutsなどのフレームワークの有効性を否定しているわけではありません。
そこそこの規模のシステムにおいてはフレームワークが有効であると思います。

せっかく、Google App Engineという手軽なインフラを利用できるので、プログラムも手軽に作れればいいなと
思っているのです。

とはいえ、本当にJSPの中にビジネスロジックが混ざったり、たくさんサーブレットを作成したりという状況には
したくはありません。

そこで、今回、「HtmlControllerServlet」と「RequestHandler」というインターフェースにより構成される超シンプルな
自作フレークワークを作成しました。

「RequestHandler.java」
package com.appspot.myhomemgr.base;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface RequestHandler {

   public void execute(HttpServletRequest req, HttpServletResponse resp)
                                           throws Exception;
}


「HtmlControllerServlet.java」
package com.appspot.myhomemgr.base;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@SuppressWarnings("serial")
public class HtmlControllerServlet extends HttpServlet {

   public void doGet(HttpServletRequest req, HttpServletResponse resp)
   {
      processReguest(req, resp);
   }
   
   public void doPost(HttpServletRequest req, HttpServletResponse resp)
    { 
      processReguest(req, resp);
    }
   
   private void processReguest(HttpServletRequest req, HttpServletResponse resp)
   {
      try
      {
         String pathInfo = req.getPathInfo();
         pathInfo = pathInfo.substring(1);
         
         @SuppressWarnings("unchecked")
         Class cls = (Class)Class.forName("com.appspot.myhomemgr.html.handler." + pathInfo);
         RequestHandler handler =  (RequestHandler)cls.newInstance();
         handler.execute(req, resp);
      }
      catch(Exception ex)
      {
         try
         {
            resp.setContentType("text/html");
            resp.getWriter().println("<html><head><title>エラー</title></head><body>");
            resp.getWriter().println(ex.toString());
            resp.getWriter().println("</body>");
         }
         catch(Exception iex){}
      }
   }
   
}

「HtmlControllerServlet」は全てのリクエストの受け手となるサーブレットクラスです。そして、「HtmlControllerServlet」は
クライアントが指定したURLに応じて、「RequestHandler」インタフェースを実装するクラスに処理を振り分けます。



「RequestHandler」の実装クラスはパッケージ「com.appspot.myhomemgr.html.handler」に作成することをルールにしています。

URLと実装クラスのマッピングはPathInfoを利用して行います。PathInfoに直接「RequestHandler」のクラス名(パッケージ部分は省略)を記述し、
Javaのリフレクションの機構を利用して、該当クラスをインスタンス化しています。

例) URLが「http://xxxx.xx.xx/ctl/LedgerList」の場合、PathInfoは「LedgerList」になります。
(正確には/LedgerListがPathInfoになるので、substring()して先頭の/を除外しています。)
この場合、「RequestHandler」を実装する「com.appspot.myhomemgr.html.handler.LedgerList」にリクエストが振り分けられます。

後は、「RequestHandler」の実装クラスにビジネスロジックを記述し、「RequestHandler」とJSP、「RequestHandler」同士の処理の受け渡しは
普通に「RequestDispatcher」の使用して行います。

これだけでも、それぞれが密結合ですが、一応コントローラ部分は、「HtmlControllerServlet」、モデルは「RequestHandler」、ビューは「JSP」
というMVCモデルになっています。

モデルのクラス設計と生産性の向上
ここからは少し余談ですが、私はオブジェクト指向型の言語を用いてシステムを開発する場合には、
MVCのモデル部分のクラス設計をしっかりと行うことがとても重要と考えています。

多くのプロジェクトにおいて、「MVCをどのようにして実現するのか?」ということについては、良く議論され、考えられていると思いますが、
この「モデル部分をどのように作るか?」という点はあまり考えられていなかったりします。
しかし、開発の生産性という点からいうと私はここが一番大事だと思っています。なぜなら、業務ロジックの再利用とルール化はプロジェクトの生産性を
高める上では欠かせない要素であるからです。

折角、苦労してStrutsなどのフレームワークを導入しても、ビジネスロジックが開発者個人により好き勝手に作られると
結局スパゲッティソースになってしまうのです。

私が担当するプロジェクトにおいては、「モデル部分のクラス設計」を「外部設計」と並行して行います。場合によっては、この段階で
モデル部分のフレームワークを設計したりします。モデル部分のフレームワークはStrutsのような画一的なものでなく、
業務フローの分析により、明らかになる複数の業務間で共通したビジネスフローを具現化するもので、プロジェクト毎に固有のものになります。

これについてはまた、別途詳しく記事にさせていただきたいと思います。

今回は非所有関係にある複数のエンティティのデータクラスを作成します。

非所有関係にあるエンティティの作成

まず、物品管理システムのエンティティ間の関係をおさらいします。



エンティティ間のリレーションの中で非所有の関係になっているのは、
「物品」と「物品種別」、「物品」と「管理場所」の関係です。
それぞれ、「物品」エンティティがその他のエンティティを参照する関係です。

非所有関係の場合、参照される側のクラスに特別な属性の定義は必要ありません。
参照する側に参照先のエンティティのキーを属性として定義します。

以下は、「物品」を表す「Goods」クラスのコードの一部です。

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

   @PrimaryKey
   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
   private Key key;
// ...

   @Persistent
   private Key goodsKind;

   @Persistent
   private Key location;

// ...
}

これで完了ですが、一点注意しないといけない点があります。Key型のフィールドにセットされるインスタンスが、
意図するエンティティのキー(この場合、物品種別のキー)であるかどうかをチェックしてくれる仕組みはGAEからは
提供されていません。

従って、それを保障するのは、アプリケーションコードの責任になります。

ドキュメントによると、「GAEはJDOの[unowned relationships(非所有関係)]を実装していないので、
その代わりにこの方法を使いなさい」と書かれています。

実装されるとどう変わるのか分かりませんが、今のところこれが唯一の方法のようです。

1つ気ががりな点があります。所有関係と異なり、非所有関係の場合、参照する側のクラスに
参照されるクラスが直接定義されていません。しかも、JDOのクエリでは「結合」を使用できません。

そうなると例えば、物品種別の名称を項目の1つとする物品の一覧を表示したいとなると、
物品と物品種別を別々に取得して、それを結合する必要が有ります。

今のところ、別々に取得して、Mapとかを使って自分で結合するしかないのかなと思っていますが、
それは、次回以降で。

データクラスのソース

さて、本日は実際にデータクラスの作成を行ってみますが、その前に
もう1つおさえておきたい点があります。それはEntity Groupとトランザクションについてです。
Entity Groupもエンティティ間の関係と同じく、データクラスの定義の仕方に影響を与えます。

「Entity Group」について
GAEのデータストアではトランザクションを扱うことが出来ます。複数のエンティティに対する
変更を1つのトランザクション内で行い、全て成功したらコミット、1つでも失敗したらロールバックしたり
することが出来るのです。

しかし、これには1つの条件があります。1つのトランザクション内で行われる複数のアクションの対象エンティティが
同一のEntity Groupに属している必要があるということです。

Entity Groupはルートエンティティをトップにその子供のエンティティ、子供の子供(孫)のエンティティという様に
木構造を形をとるエンティティの集合です。

1つのEntity Groupに属するエンティティは大規模な分散システムによって成り立っているGAEのインフラの中で、
任意の単一のノード内で管理されるようになります。

前回の記事で、紹介した所有関係にある複数のエンティティでは、子のエンティティは、自動的に親のエンティティと
同じEntity Groupに属するようになります。

親を持たないエンティティは自動的にそれ自体で1つのEntity Groupを構成するルートエンティティとなります。
ルートエンティティが子孫を持つか持たないかは強制される問題ではありません。

同じく前回の記事で扱った所有関係でない複数のエンティティは、自動的に同じEntity Groupに属するということは
ありません。しかし、処理を施すことによって同一のEntity Groupに属するようにすることも可能です。

このあたりを踏まえて、データクラスの作成に入ります。

所有関係にあるエンティティの作成
まず、所有関係にある「台帳」と「物品種別」を作成してみます。「台帳」は「Ledger」というクラス名で、
「物品種別」は「GoodsKind」というクラス名で作成します。

下記は「Ledger」クラスのコードです。

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

   @PrimaryKey
   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
   private Long ledgerID;

   @Persistent
   private String ledgerName;

   @Persistent
   private Date createdOn;

   @Persistent
   private User createdBy;

   //セッター、ゲッター省略
}

前回の記事でお話しましたように、JDOではデータクラスとなるクラスが特別なインターフェースを実装したり
する必要はありません。その代わりにエンハンサが解釈するためのアノテーションを記述します。

まず、クラスのアノテーションとして、@PersistenceCapable(identityType = IdentityType.APPLICATION)を記述します。

データクラスは必ず1つ、主キーとなるフィールドを持つ必要があります。上記のコードではLong型の「ledgerID」に、
@PrimaryKeyを追加し、主キーとなるようにしています。その下の@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)アノテーションは
このフィールドが永続化の対象でありかつ、永続化される時に自動的に一意の値が割り当てられるようにするためのものです。

その他のフィールドにも@Persistentを付加し、永続化の対象になるようにしています。
逆に永続化しないフィールドについては、@NotPersistentを記述します。

永続化対象フィールドはprivate、またはprotectedである必要があります。そのため、他のクラスからそれらを参照するための
セッターとゲッターを定義します。

「Ledger」クラスは親を持たないルートエンティティです。それに対し、次に紹介する「GoodsKind」は「Ledger」クラスを
親とするエンティティです。この違いによって主キーの定義の仕方が変わります。

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

   @PrimaryKey
   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
   private Key key;

   @Persistent
   private String name;

   @Persistent
   private String additionalDesc;

   //セッター、ゲッター省略
}

「GoodsKind」クラスでは、主キーに「key」という型を使用しています。エンティティはEntityGroup内の先祖のキーと自身のキーに
よって構成されるパスにより一意に識別されます。ファイルシステム内のファイルがディレクトリとファイルの名前によって構成される
パスによって識別されることと似ています。「GoodsKind」クラスで使用しているKeyクラスは、このパスの情報を管理するためのクラスなのです。

「Ledger」クラスはルートエンティティであり、パス情報を持つ必要が無かったので、long型のキーを使用しました。
ルートエンティティのキーとして使用できる型として他にString型があります。

※本来は「Ledger」クラスのキーはLong型で良いはずですが、現状ではクエリを使って、「GoodsKind」クラスのインスタンスを
抽出し、結果を取得する際に「java.lang.ClassCastException: com.google.appengine.api.datastore.Key cannot be cast to java.lang.Long」
が発生します。これは「Ledger」クラスのキーもKey型で定義することで回避できます。


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

   @PrimaryKey
   @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
   private Key key;
// ...
}

ルートエンティティ以外のキーとして使用できる型としては、他に「Key as Encoded String」というキー情報を文字列と
して表現するタイプの型があります。

さて、この段階ではまだ、「Ledger」クラスと「GoodsKind」クラスの親子関係を定義できていません。
まず、「Ledger」クラスに以下を追加します。

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

// ...

   @Persistent(mappedBy = "ledger")
   private List<GoodsKind> goodsKinds;
}

そして、「GoodsKind」クラスに以下を追加します。

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

   @Persistent
   private Ledger ledger;
}

親クラスに子クラスのコレクションのフィールドを定義するだけでも、
親子関係は成立します。しかし、子クラスから親クラスを参照したいケースも多いはずなので、
子クラスにも、親クラスの型のフィールドを追加し、親クラスの @Persistentアノテーションの
mappedByに子クラスのフィールド名を指定しています。

親子関係が便利な理由
親子関係が便利な理由はいくつかあるのですが、まず1つは、
親が削除されるタイミングで、同時に子供も削除されるという点です。

物品管理アプリでは台帳が無くなった物品はゴミになってしまうので、
台帳が無くなるタイミングで物品も自動的に消去されるので、とても便利です。

また、所有関係にあるEntity同士は自動的に同じEntity Group内に属することになります。
これにより、親と子の更新処理を1つのトランザクションで扱うことが可能になります。

台帳に物件を追加するという工程は、実際には、「物品のインスタンスを登録する」という
アクションと「台帳の物品リストに物品を登録する」という2つのアクションに分割されます。
これらは、1つのトランザクション内で行われるべきです。

他には、例えば今回のアプリだと、物品種別を検索する際、必ず物品種別が登録されている台帳が
条件として必要です。そうでないと他の家庭の物品種別まで、見えてしまいます。

そのような場合、以下のようにして台帳で物品種別を絞り込むことが出来ます。

   Query query = pm.newQuery(GoodsKind.class,"ledger == ledgerparam");
   query.declareParameters("String ledgerparam");
   List<GoodsKind> results = (List<GoodsKind>) query.execute(台帳のキーの文字列);

次回は、所有関係でないエンティティ間のリレーションを持つデータクラスを作成してみます。

前回の記事で紹介した物品管理システムのエンティティ分析の結果に基づいて、
それに対応するデータクラスを定義してみます。とにかく分からないことだらけで大変でした。。

Google App Engineはデータストアを利用する方法として「JDO」、「JPA」、あと「low level API」の
3つを提供しています。

私はどれに関しても、ほとんど知識が無いので、3つの中では最もドキュメントが充実している「JDO」と
使うことにしました。

JDOについて
「JDOについて」と言っても私にもたくさん語れるほどの知識はありません。必要最低限の内容を少しだけ、書かせて頂きます。

JDOとはウィキペディアによると「Javaオブジェクトの永続性に関する仕様」と書かれています。
少し簡単に言い直すと、JavaのオブジェクトをRDBなどの永続ストレージに保存したり、
そこから復元したりするための仕様ということになると思います。

JDO自体にはインタフェースしか定義されていなくて、その実装は各ベンダーから提供されています。
GAEの場合は、オープンソースの「DataNucleus Access Platform」という実装にGAE用のアダプタを追加したものを使っています。

GAEはJDOを採用することにより、基盤を構成するクラスタ化されたデータベースサーバへの接続を隠蔽しているものと考えられます。

開発者からの視点でみると、GAEにおいてデータを保存するということは、データに対応するクラスのインスタンスを保存することになり、
データクラスを定義することが、データベースのスキーマを定義することになります。

JDOではJavaの「Plain Old Java Objects(POJOs)」、つまり普通のJavaクラスのインスタンスを永続化してくれます。
特別なインターフェースを実装したり、クラスを継承したりする必要はありません。

これは、POJOsにアノテーションを付加し、それをエンハンサにより永続化可能なオブジェクトに変換することで実現されています。
エンハンサとは、付加されたアノテーションに従って、コンパイルされたクラスを永続化可能なクラスに変換する作業を行ってくれるツールです。

Eclipeのプラグインを使用して開発を行う場合は、コンパイルの直後にエンハンサが自動的に実行されるので、
特別な作業の必要はありません。

※アノテーションはJava5から導入された機能で、javacやその他のツールがそれを利用してクラスに対してなんらかの加工処理を施すためのものです。
独自のアノテーションを定義して、それを利用するツールを開発することも出来ます。

エンティティ間の関係
それでは、RDBのテーブルに該当するデータクラスの定義から始めたいと思います。

まず、データクラスの定義に入る前に、エンティティ間の関係を整理して置くことが必要です。
それによって、クラスのフィールドの定義の仕方が変わってくるからです。

最初に整理したいのは、エンティティ間の関係が所有関係かどうかという点です。
所有関係というのは一方のエンティティが、もう一方のエンティティの存在なしには存在し得ない
関係を言います。

例を挙げると、伝票とその伝票の明細の関係は所有関係になります。伝票の存在なしに、明細が
存在することは有りません。一方、所有関係にない関係の例としては、明細と勘定科目の関係があります。
勘定科目は複数の明細で使用されることもあるので、明細の存在が無くても存在しうるものです。

前回、抽出した物品管理システムのエンティティ間の関係について、所有関係を整理してみました。
抽出したエンティティは「台帳」、「台帳ユーザ管理」、「物品」、「物品種別」、「管理場所」の5つでした。

「台帳」エンティティは、通常、1つの家庭と一対一になるものです。ユーザはシステムに物品を登録する前に
我が家専用の台帳を作成し、それに対して物品を登録していく想定です。「物品種別」、「管理場所」に関しても、
台帳に対して登録をしていく仕様を想定しています。

この想定において、「台帳」エンティティが「物品」、「物品種別」、「管理場所」エンティティを所有している
関係になります。「台帳」なしに「物品」だけが存在していることがないからです。

一方、「物品」と「物品種別」、「物品」と「管理場所」の関係を考察してみます。
「物品種別」、「管理場所」は「物品」の属性の1つになります。例えば、「物品」ドライヤーの「物品種別」は
電化製品という感じです。しかし、電化製品というくくりで言うと、テレビや冷蔵庫も電化製品になります。

従って「物品種別」電化製品はドライヤーの存在なしにも存在することが出来ます。
このことから、「物品」と「物品種別」の関係は所有関係ではないと言えます。「物品」と「管理場所」についても
同じです。

このような作業を行って、エンティティ間の関係を整理し、クラス図を更新しました。



次回はこのクラス図に従って実際にデータクラスを作成してみます。

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