CGIKit、TapKit を利用した Web アプリケーションの作成

著者:喜多川 豪 編集:かずひこ

CGIKit、TapKit を利用した Web アプリケーションの作成

1. はじめに

Ruby で CGI を作成した事がある方は判るかと思いますが、Ruby には cgi.rb というライブラリが標準で利用する事が出来ます。 cgi.rb でコードの再利用やテンプレートとロジックの分離等を行うためには自前でライブラリを書いたりする必要があります。 また、バックエンドにデータベースを利用する場合、なるべくロジックからはデータベースを意識しないようなコードを書きたくなります。

これらの問題を解消する手段として、本記事では Web アプリケーションフレームワークの CGIKit、O/R マッピングツールの TapKit を利用した Web アプリケーションの作成方法を説明します。

2. CGIKit とは?

CGIKit は Ruby で書かれた Web アプリケーションフレームワークです。

CGIKit による Web アプリケーションは、

  • テンプレート (.html)
  • バインディング (.ckd)
  • コード (.rb)

というファイルで構成されるコンポーネントを組み合わせて作成します。 これらのファイルで記述されたエレメント (インスタンス変数やメソッドを HTML として表示する仕組み) をコードがバインディングし、HTML に変換して出力することで Web ページが表示されるようになっています。

Ruby 標準の cgi.rb で CGI を作成していると、コードやテンプレートの再利用が難しいのですが、CGIKit では Web ページのヘッダ部分 (サイトのタイトル等) やフッタ部分、またはどのページにも表示させたいパーツ等再利用したいものは全てコンポーネントとして分ける事で、コードの再利用を可能にし、開発効率を上げる事ができます。

また、CGIKit のコンポーネントは、デザインとロジックが分離されているので、ロジックに影響しないデザインだけであればコードを書き換える事無く変更する事ができます。

3. TapKit とは?

TapKit はオブジェクト-リレーショナルデータベース間のマッピングを行うフレームワークです。 細かい内容はるびま 4 号の記事「Ruby Library Report --- [第3回] O/R マッピング」を参考にしてください。

4. Webアプリケーションを作ってみる

では実際に CGIKit と TapKit を使って Web アプリケーションを作成してみましょう。 今回は簡易 BBS (掲示板) を作成してみます。 手順としては以下のようになります。

  • コンポーネントの作成
  • モデルファイルの作成
  • TapKit オブジェクト用クラスの作成

まず掲示板 (以下 BBS と呼ぶ) の操作の流れは今回以下のようにしてみます。

[スレッド一覧] ----> [スレッド新規登録]
      |
      +------------> [コメント一覧]

「スレッド一覧」画面ではスレッドの一覧を表示し、「スレッド新規登録」ではスレッドを新しく登録出来るようにします。「コメント一覧」ではスレッドに対してのコメントの一覧を表示し、またコメントを入力出来るようにします。

5. 準備

BBS の仕様は [4.Webアプリケーションを作ってみる] の通りですが、まずは CGIKit、TapKit のインストールから行いましょう。 今回 CGIKit は CVS から取得します。取得するものは安定版であるバージョン 1 系のものになります。

$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/cgikit login
  パスワードを聞かれたら、Enter を押す。
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/cgikit co cgikit-1
$ cd cgikit-1
$ ruby setup.rb config
$ ruby setup.rb setup
# ruby setup.rb install

うまくインストール出来たでしょうか? 次に TapKit をインストールします。 今回 bbs のデータを扱うためにデータベースを使用しますが、データベースは RDBMS の PostgreSQL を利用します。 TapKit では PostgreSQL を利用するために RAA:ruby-dbiRAA:postgres を使いますのでこれらを先にインストールしておきましょう。 ruby-dbi、ruby-postgres がインストール出来ましたら TapKit も CVS から取得します。 インストール方法は CGIKit と同じように、

$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/tapkit login
  パスワードを聞かれたら、Enter を押す。
$ cvs -d:pserver:anonymous@cvs.sourceforge.jp:/cvsroot/tapkit co tapkit
$ cd tapkit
$ ruby setup.rb config
$ ruby setup.rb setup
# ruby setup.rb install

となります。 これで準備は完了です。

6. コンポーネントの作成

CGIKit による Web アプリケーションは、[1. CGIKitとは] で説明したとおり、*.html、*.ckd、*.rb というファイル群で構成されます。これら三つのファイルを合わせてコンポーネントと呼び、画面毎にこのコンポーネントを作成していきます。 今回作成する BBS では、[4. Webアプリケーションを作ってみる] での操作の流れから、以下の三つのコンポーネントを作成します。

  • MainPage (スレッド一覧)
  • EditPage (スレッド新規登録)
  • ThreadPage (コメント一覧)

まず MainPage コンポーネントのテンプレート、バインディングから作成します。 CGIKit のテンプレートはバインディングファイルと対となっています。 エレメントは、

<cgikit name="*****">.....</cgikit>

や、</cgikit>を省略した形、

<cgikit name="*****" />

と記述します。 では MainPage のテンプレートを見てみましょう。

テンプレートファイル (MainPage/MainPage.html)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>bbs</title>
  </head>
  <body>
    <h1>BBS</h1>
    [<cgikit name="edit">スレッドの追加</cgikit>]
    <hr>
    <table>
      <tr style="background-color: silver;">
        <td>
          日付
        </td>
        <td>
          タイトル
        </td>
        <td>
          名前
        </td>
      </tr>
      <cgikit name="thread_list">
        <cgikit name="tr">
          <td><cgikit name="date" /></td>
          <td><cgikit name="title" /></td>
          <td><cgikit name="register" /></td>
        </cgikit>
      </cgikit>
    <table>
  </body>
</html>
MainPage.html

"edit" というエレメントがまず出て来ます。これはご想像のとおりスレッドを追加するページへのリンクになります。以下のバインディングファイルでエレメントの内容を見てみましょう。

バインディングファイルは、

定義名 : 種類 {
  属性 = 値
  属性 = 値
  ...
}

という書式で記述します。定義名はテンプレートで指定した名前が入り、種類は CGIKit で提供されているエレメントや他のコンポーネントを指定します。 { と } の間はそのエレメントの属性を定義します。 例えば、edit にあたるバインディングは以下のようになります。

edit : CKHyperlink {
        page = 'EditPage'
}

これは、EditPage というコンポーネントへのリンクを生成するエレメントになります。

次に thread_list というエレメントが登場します。これにあたるバインディングは以下のようになります。

thread_list : CKRepetition {
        list  = thread_list
        item  = thread
        index = index
}

これは、<cgikit> タグで囲んだ内容を繰り返すエレメントになります。 つまり、<cgikit name="thread_list"> から </cgikit> までを繰り返すという意味になります。ここでは HTML での

<table>
  <tr>
    <td>2004-01-01</td>
    <td>けいじばん</td>
    <td>喜多川</td>
  </tr>
   .
   .
   .
</table>

のように表を 1 行ずつ繰り返す役割をしています。 thread_list の属性は、list、item、index が指定されてますが、

list
繰り返すデータ。この配列から要素を順に取り出し、item 属性に入れる。
item
list 属性から取り出した要素。イテレータのブロックパラメータにあたる。
index
item 属性で繰り返される要素数。

となっています。 つまりコード (*.rb) で thread_list へ配列で値を渡すと item 属性の thread へ 1 要素ずつ展開されるようになります。ここでの配列は、

[{'date' => '2004-01-01',
  'title' => 'けいじばん',
  'register' => '喜多川'
  'query' => {'thread_no' => 1}
 },
....]

のようにコードで要素にはハッシュで値を渡すようにしています。 なのでバインディングでは、以下のように date や title といった値として thread['date'] や thread['title'] などのように指定します。

また、以下のバインディングファイルで、CKString というエレメントが記述されていますが、CGIKit が提供している各エレメントには escape という属性がありますのでこれを利用する事で自前でエスケープ等をしなくて良くなります (escape 属性はデフォルトで有効)。

エレメントの一覧は CGIKit のドキュメント を参考にすると良いでしょう。

バインディングファイル (MainPage/MainPage.ckd)

edit : CKHyperlink {
  page = 'EditPage'
}

thread_list : CKRepetition {
  list  = thread_list
  item  = thread
  index = index
}

tr : CKGenericElement {
  tag = 'tr'
  style = tr_style
}

date : CKString {
  value = thread['date']
}

title : CKHyperlink {
  page = 'ThreadPage'
  query = thread_query
  string = thread['title']
}

register : CKString {
  value = thread['register']
}
MainPage.ckd

コード (MainPage/MainPage.rb)

class BBS
  class MainPage < CKComponent
    def init
      @thread_list = Thread.list
    end

    def thread_query
      {'thread_no' => @thread.thread_no}
    end

    def tr_style
      if (@index % 2) == 1 then
        "background-color: #dddddd;"
      else
        "background-color: #eeeeee;"
      end
    end
  end
end
MainPage.rb

7. モデルファイルの作成

次に TapKit で利用するモデルファイルを作成しましょう。 TapKit ではマッピングの設定を記述したファイルをモデルファイルと呼び、このファイルには他にデータベースと接続するアダプタの情報も記述します。

まず今回作成する BBS のデータベースのテーブル構造は以下のようにします。

/* bbs */
/* base table */
create  table	thread	(
        thread_no	integer,	/* 0. スレッド No */
        title		text,		/* 1. タイトル */
        date		text,		/* 2. 登録日 */
        register	text,		/* 3. 名前 */
        e_mail		text,		/* 4. メールアドレス */
        contents	text,		/* 5. 内容 */
        primary key (
          thread_no
        )
);
/* comment table */
create	table	comment (
        thread_no	integer,	/* 0. スレッド No */
        comment_no	integer,	/* 1. コメント No */
        date		text,		/* 2. 登録日 */
        register	text,		/* 3. 名前 */
        e_mail		text,		/* 4. メールアドレス */
        contents	text,		/* 5. 内容 */
        primary key (
          thread_no,
          comment_no
        )
);

これを PostgreSQL に bbs という名前の DB として作成します。 次にデータベーススキーマからモデルファイルを自動生成します。 TapKit をインストールすると modeler というコマンドもインストールされますが、これが自動生成用のスクリプトになります。 モデルファイルの自動生成は以下のようにします。 (モデルファイルのファイル名は model.conf とします)

$ modeler PostgreSQL model.conf

とすると、以下のように対話的に設定を行う事が出来ます。

kida@sairen:~/src/bbs$ modeler PostgreSQL model.conf
Login database with DBI
URL: dbi:Pg:bbs
Username: postgres
Password:
Selectable tables - comment, pg_aggregate, pg_am, pg_amop,
pg_amproc, pg_attrdef, pg_attribute, pg_cast, pg_class,
pg_constraint, pg_conversion, pg_database, pg_depend,
pg_description, pg_group, pg_index, pg_inherits, pg_language,
pg_largeobject, pg_listener, pg_namespace, pg_opclass,
pg_operator, pg_proc, pg_rewrite, pg_shadow, pg_statistic,
pg_trigger, pg_type, sql_features, sql_implementation_info,
sql_languages, sql_packages, sql_sizing, sql_sizing_profiles, thread
(If you want to select the all tables, input 'all')
Select tables (separate table names with comma): thread,comment
Create model.conf

これでカレントディレクトリに model.conf というファイル名のモデルファイルが作成されます。 以下のモデルファイルは自動生成されたファイルを若干修正しています。 具体的には text 型のフィールドで width が-1 と設定されているのを削除したり、primary key を class_properties に追加したりしています。 また、次の章で説明しますが、class_name を GenerciRecord から BBS::Thread や BBS::Comment に変更しています。

---
adapter_name: PostgreSQL
connection:
  password: ''
  url: dbi:Pg:bbs
  user: postgres
entities:
-
  attributes:
    -
      allow_null: true
      class_name: Integer
      column_name: thread_no
      external_type: integer
      name: thread_no
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: contents
      external_type: text
      name: contents
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: date
      external_type: text
      name: date
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: e_mail
      external_type: text
      name: e_mail
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: register
      external_type: text
      name: register
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: title
      external_type: text
      name: title
      read_only: false
  class_name: BBS::Thread
  class_properties:
    - contents
    - e_mail
    - register
    - title
    - comments
    - date
    - thread_no
  external_name: thread
  name: Thread
  primary_key_attributes:
    - thread_no
  relationships:
    -
      delete_rule: nullify
      destination: Comment
      join_semantic: inner
      joins:
        -
          destination: thread_no
          source: thread_no
      mandatory: false
      name: comments
      to_many: true
-
  attributes:
    -
      allow_null: false
      class_name: Integer
      column_name: comment_no
      external_type: integer
      name: comment_no
      read_only: false
    -
      allow_null: false
      class_name: Integer
      column_name: thread_no
      external_type: integer
      name: thread_no
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: date
      external_type: text
      name: date
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: e_mail
      external_type: text
      name: e_mail
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: register
      external_type: text
      name: register
      read_only: false
    -
      allow_null: false
      class_name: String
      column_name: contents
      external_type: text
      name: contents
      read_only: false
  class_name: BBS::Comment
  class_properties:
    - e_mail
    - register
    - thread
    - date
    - contents
    - comment_no
    - thread_no
  external_name: comment
  name: Comment
  primary_key_attributes:
    - comment_no
    - thread_no
  relationships:
    -
      delete_rule: nullify
      destination: Thread
      join_semantic: inner
      joins:
        -
          destination: thread_no
          source: thread_no
      mandatory: false
      name: thread
      to_many: false

これで TapKit を利用する準備が整いました。

8. TapKit オブジェクト用クラス

次に TapKit を利用してデータベースにアクセスするクラスを作成しましょう。

CGIKit で利用する*.rb の実装では以下のようにクラスにアクセスする事を想定して作成します。

def init
  threads = Thread.list
  @thread_list = Array.new
  threads.each do |obj|
    @thread_list.push({
                        'query' => {'thread_no' => obj.thread_no},
                        'date' => obj.date,
                        'title' => obj.title,
                        'register' => obj.register
                      })
  end
end

上記の通り、作成するクラス名は Thread という名前のクラスになります。 Thread クラスを作成する前に、TapKit::GenericRecord という Tapkit 独自のクラスを継承するクラスを作成します。 このクラス (BBSRecord) ではモデルファイルから TapKit の Application オブジェクトを作成します。

class BBS
  class BBSRecord < TapKit::GenericRecord
    def self.connect(model)
      @@tap = TapKit::Application.new(model)
    end
  end
end

BBSRecord クラスの connect メソッドは index.cgi というこの BBS アプリケーションでアクセスする cgi ファイルから呼ばれるようにします。

#!/usr/bin/env ruby
require 'cgikit'
require 'tapkit'
require 'lib/bbs.rb'
require 'lib/bbs_record'
require 'lib/thread'
require 'lib/comment'
BBS::BBSRecord.connect('sql/model.conf')
app = BBS.new
app.main = 'MainPage'
app.run

これで以下の Thread クラスの list メソッドで@@tap という Application オブジェクトを利用出来るようになります。

以下の list メソッドでは、Quqlifer.format でデータを取得するための条件を記述します。list メソッドでは全てのスレッドを取り出したいので空を指定しています。 次に、SortOrdering.new では thread_no というフィールドで降順にデータを取り出すという並び変えの設定を行っています。SQL でいう ORDER BY 句にあたります。 FetchSpec.new ではこれらの条件式、並び変えの設定を指定し、fetchspec.limit でデータの取得件数を指定します。 実際にデータを取り出すのは、@@tap.create_editing_context.fetch で取得します。

class BBS
  class Thread < BBSRecord
    def self.list(count = 20)
      qualifier = Qualifier.format('')
      sort = SortOrdering.new('thread_no', SortOrdering::DESC)
      fetchspec = FetchSpec.new('Thread', qualifier, [sort])
      fetchspec.limit = count
      obj = @@tap.create_editing_context.fetch(fetchspec)
      return obj
    end
  end
end

obj には以下のような Hash に似た値が帰ってきます。

[{thread_no = 2, contents = "内容", date = "2005-01-22",
  e_mail = "kida@netlab.jp", register = "喜多川",
  title ="開設しました",
  comments = <FaultingDelayEvaluationArray -89021887>},
 {thread_no = 1, contents = "内容 2", date = "2005-01-20",
  e_mail = "kida@netlab.jp", register = "喜多川です",
  title = "BBS ですよ",
  comments = <FaultingDelayEvaluationArray -89021881>}]

実装では

obj[0].thread_no
obj[0].contents

等で値を取り出す事が出来ます。 comments は FaultingDelayEvaluationArray と記述されていますが、これは comment テーブルは thread と 1:多でリレーションシップの関係を持っているため、thread_no でリレーションされている comment テーブルのデータも取得出来ます。 上記の comments は、

obj[0].comments[0].contents

等でアクセス出来ますが、アクセスしない限り comment テーブルに対してデータベースにアクセスしません。これは TapKit のフォールティングの機能があるからです。

9. まとめ

簡単ですが CGIKit と TapKit で Web アプリケーションを作成してみました。 コンポーネントの再利用や O/R マッピングの利便性等は説明しきれませんでしたが、CGIKit、TapKit に興味を持っていただけると幸いです。 今回製作した BBS のソースは bbs.tar.gz になります。 CGIKit はバージョン 2 系の開発も進んでおり、リリースされた時はまたレビューしてみたいと思っております。

著者について

喜多川 豪
喜多川は株式会社ネットワーク応用通信研究所にて研究開発職に就いています。日々仕事で Ruby を使って Web アプリケーションを作成していたりします。
かずひこ
Web アプリケーションをこよなく愛するオープンソースエンジニア。
更新日時:2005/02/15 01:00:23
キーワード:
参照:[Rubyist Magazine 0005 号] [0005 号 巻頭言] [分野別目次] [各号目次] [prep-0005]