Webフレームワークの新しい波 Waves探訪記

第3回シンプルなWebアプリを作る(その1)

作成するアプリケーションの概要

今回から、Wavesを使って簡単なアプリケーションを作成します。今回作成するのはTwitterに代表されるようなマイクロブログです。Twitterは「鳥のさえずり」という意味ですが、その名の通り、"つぶやき"とも"誰に対してでもない報告"ともとれるようなエントリを次々と作っていくシステムです。チャットとは違いリアルタイム性はありませんし、同じチャネルにいるメンバー全員に同じ内容が送信されるといった押し付けがましいところが無いのが特徴です。Twitter類似のサービスとしてはGoogleの1グループになったJaiku、ファイル共有システムを持つPownceなどがあります。今回は望みは大きく実装は小さく、とりあえずひとりでつぶやけるシステムを作りましょう。

今回使っているシステムはOSがFedora 7(coLinux⁠⁠、データベースはsqlite3 3.4.2、Wavesは0.7.2です。前回も書きましたが、残念ながら現状WavesはWindowsには対応していません。まもなくリリースされるバージョン0.7.3(もしかすると一気に0.8)ではWindowsもサポートされる模様です。

Wavesコマンドでアプリのひながたを作る

Twitter風のアプリケーションということで、名前はTwにしました。

Twアプリケーションの作成
waves tw

wavesコマンドを実行したディレクトリに、アプリケーションのベースになるファイルが生成されます。Ruby on Railsを使ったことがあれば、railsコマンドと同じようなものと思ってください。ただしWavesのバージョン0.7.2の生成するコードにはバグがありますので、以下の手順に従って修正する必要があります。

Wavesコマンドのバグ対応

バージョン0.7.2のWavesコマンドにはいくつかのバグがありますので、ここで修正しておきます。

lib/tasks/schema.rb

この中の8行目付近に「Blog.database」という文字列があります。これは「アプリ名.database」でなければなりません。したがって今回はTw.databaseと修正してください。

lib/tw.lib

44行目付近にある「Sequel.mysql( config.database )」ですが、このままではデータベースがMySql決め打ちになってしまいます。⁠Sequel.open( config.database )」に修正して、設定したデータベースに接続するようにしてください。

textileメソッドは日本語に非対応

Wiki風のマークアップを解釈してくれるtextileメソッドがビューで使えるのですが、残念ながら日本語はうまく表示できないようです。

データベースの設定とテーブル作成

データベースの設定

まずデータベースを設定します。configurations/development.rbをエディタで開いて設定します。今回はSqlite3を使います。

configurations/development.rbの修正
module Tw
  module Configurations
    class Development < Default
    
      database :host=>'localhost', :adapter=>'sqlite', :database=>'db/tw_dev'
      host '0.0.0.0'
      port 3000
      reloadable [ Tw ]
      log :level => :debug  

      application do
        use Rack::ShowExceptions
        run Waves::Dispatchers::Default.new
      end
    end
  end
end

hostは接続するホストのIPアドレスを制限します。デフォルトは'127.0.0.1'になっていますので、同じIPアドレスからの接続しか許していません。公開する場合は'0.0.0.0'などにする必要があるでしょう(公開用のproduction.rbのデフォルトはこうなっています⁠⁠。coLinuxで開発していて、Windows側からcoLinux側のWavesアプリケーションに接続する場合も、'同じマシン'とはみなされないので、設定を変える必要があります。

マイグレーション

まずマイグレーション用のベースを作成します。

以下のコマンドを実行すると、schema/migrations/001_initial_schema.rbというファイルが自動的に作られます(db/tw_devも作成されます⁠⁠。

マイグレーション用のファイルを作る
rake schema:migration name=initial_schema

次に実際のテーブル情報を設定します。個々の発言はwordテーブルに格納することにします。schema/migrations/001_initial_schema.rbを編集して以下のようにします。

マイグレーション用のファイルを作る
class InitialSchema < Sequel::Migration

  def up
    create_table :words do
      primary_key :id
      text :text
      timestamp :created_on
      timestamp :updated_on
    end
  end
  
  def down
    drop_table :words
  end

end

最後にマイグレーションを実行すると、wordsテーブルが作成されます。マイグレーション実行前には、dbディレクトリをアプリディレクトリに作っておいてください。dbディレクトリはユーザもグループもWavesサーバからリードライトできる必要があります。自分のユーザディレクトリで開発している場合はそのままで問題ないはずです。

マイグレーション用のファイルからデータベースを設定する
rake schema:migrate

ここまでやると、コンソールからモデルが触れるようになります。

Waves-console

Rubyに付属するirbをWaves用に拡張したツールです。Wavesの機能をその場で結果を確認しながら試すことができるので大変便利です。ここれもRailsのconsoleと同じと思えばいいでしょう。以下のようにモデルを手作業で操作できます。

waves-consoleでモデルを操作する
% waves-console
irb(main):001:0> M = Tw::Models
=> Tw::Models
irb(main):002:0> M::Word.all
=> []
irb(main):003:0>

Wavesでは全てのモデルやコントローラはモジュールの名前空間で区切られています。⁠アプリ名::Models」とすることで、モデル全体含む名前空間が得られます。面白いことにwordsテーブルはありますがモデルのファイルは作っていません。それでもWordモデルを扱うことができるのは、Wavesが動的にモデルを作って、結果を返しているからです。

新しいエントリを作ることもできます。

新しいエントリを作る
irb(main):010:0> M::Word.create(:text=>'test entry')
=> #<Tw::Models::Word @values={:text=>"test entry", :created_on=>nil, :updated_on=>Thu Apr 24 23:39:15 +0900 2008, :id=>1}>

よく見ると「created_on」が設定されていません。updated_onと異なり、こちらは自動的には設定されないようです。モデルの標準的な振る舞いはmodels/default.rbに記述されているので見てみましょう。

モデルのデフォルトの振る舞い
module Tw
  module Models
    class Default < Sequel::Model     
      before_save do
        set(:updated_on => Time.now) if columns.include? :updated_on
      end
    end
  end
end

updated_onはここで設定されていますが、created_onはそうなっていません。これはバグなのかどうか判然としませんが、修正は簡単です。以下のようにします。

レコードの作成日付を保存する
module Tw
  module Models
    class Default < Sequel::Model
      after_create do
        set(:created_on => Time.now) if columns.include? :created_on
      end
      before_save do
        set(:updated_on => Time.now) if columns.include? :updated_on
      end
    end
  end
end

これでレコードの作成日付が保存されるようになります。

ビューを作る

ではビューを作って、wordsテーブルに保存した内容を見られるようにしてみましょう。templates/word/list.mabを作って、以下のようにします。

一覧表示用のテンプレート
layout :default, :title=>'Tw' do
  form :action=>'/words', :method=>'post' do
    textarea '', :name=>'word.text', :cols=>80, :class=>'words'; br
    input :type=>:submit, :value=>'Update'
  end
  @words.each do |word|
    view :word, :summary, :word=>word
  end
end

見た目テンプレートと言ってもRubyのコードそのものです。これがMarkaby(Ruby言語でテンプレートを記述できるシステム)です。

前半が新規にエントリを追加するためのフォーム、後半がWordの各エントリに対して別のビューであるsummaryを呼び出しています。ここでは先に後半を説明します。

一覧の表示

モデルWordの全レコードは@wordsとして自動的に設定されます。

viewメソッドは、⁠view モデル名, ビュー名, ビューに渡すインスタンス変数名=>変数」と記述します。ここではwordモデルに対してsummaryというビューを使っています。summaryビューでは@wordというインスタンス変数が使え、その値はここでwordとして取り出しているWordテーブルの各エントリになります。

したがって、summaryビューを作る必要があります。templates/word/summary.mabです。

個々のエントリのテンプレート
div.text do
  p @word.text
  p.date @word.created_on.strftime("%Y-%m-%d %H:%M") if @word.created_on
end

summaryビューは他のビューから呼ばれることを前提にしているので、layoutは指定しません。前述したように呼び出し元からWordのエントリが渡されて、@wordとして参照できるようになっています。

これで一覧が表示できるようになります。URLとしては/word/listではなく/wordsでOKです。Wavesのちょっと賢い機能の一つです。

テスト実行してみる

ちょっと動作を確認してみましょう。waves-serverコマンドを使って、ウェブサーバを起動します。

サーバの起動
tw% waves-server
I, [2008-04-24T23:41:36.589731 #17044]  INFO -- : ** Waves Server Starting ...
I, [2008-04-24T23:41:36.617656 #17044]  INFO -- : ** Waves Server Running on 0.0.0.0:3000
I, [2008-04-24T23:41:36.618151 #17044]  INFO -- : Server started in 27 ms.

ブラウザから(同じマシンで開発していれば)⁠http://localhost:3000/words」を入力します。

一覧表示画面
一覧表示画面

エントリの追加

フォームは「form :action=>'/words', :method=>'post'」となっています。つまりwordsというURLにデータをポストしています。このRESTスタイルの呼び出しを、Wavesは「wordテーブルへの追加」と解釈します。実際に追加されるフィールドはtextareaのnameフィールド(word.text)で決まります。wordsテーブルにエントリが追加され、このテキストエリアの値がtextフィールドにセットされます。

ここで注意すべきなのは、RESTスタイルの振る舞いはconfigurations/mapping.rbで設定されているので、mapping.rbを書き換えることでこれを止めることも可能だということです。

さて実際にエントリを追加してみるとエラーになります。デフォルトの振る舞いとして、レコードの追加後にeditorビューが呼ばれるからです。

レコード追加後に一覧に戻る

レコード追加後に一覧に戻るようにするにはconfigurations/mapping.rbにカスタム処理を記述します。

デフォルトのマッピングを変える
module Tw
  module Configurations
    module Mapping
      extend Waves::Mapping
      # your custom rules go here
      path %r{^/words/?$}, :method=>:post do
        use(:word)
        word = controller{create}
        redirect('/words')
      end
      include Waves::Mapping::PrettyUrls::RestRules
      include Waves::Mapping::PrettyUrls::GetRules
    end
  end
end

pathメソッド以降が新たに追加した部分です。

まずpathのパラメータとしてカスタム処理を設定したいURLを正規表現の形で記述します。今回は/entriesにポストしたときの振る舞いなので「path %r{^/words/?$}, :method=>:post」となります。⁠use(:word)」で利用するモデルを指定しています。controllerは、useで設定されているモデルのコントローラを返します。パラメータにブロックを1つとり、コントローラのメソッドを呼び出しています。⁠controller{create}」はWordコントローラのcreateメソッドを呼び出したことになります。これでレコードが追加されました。最後に「redirect('/words')」で一覧に戻ります。

これでエントリを追加できるようになりました。

コントローラの振る舞いを変える

エントリの一覧ではなく、個々のエントリを表示させるには個々のエントリを示すキーが必要です。Wavesではデフォルトでnameフィールドが使われます。例えば「/word/first-word」というURLで呼び出すと、wordsテーブルのnameフィールドが'first-word'であるようなレコードが返るのです。しかし今回はnameフィールドを作ってませんし、マイクロブログでひとつひとつのエントリに名前をつけるというのは非現実的なので、Rails風にレコードのidを使うことにします。

コントローラにはデフォルトの振る舞いを記述してあるファイルがあります。controllers/default.rbです。default.rbには個々のエントリを探すfindメソッドがあり「def find( name ); model[ :name => name ] or not_found; end」と定義されています。レコードのnameフィールドで検索しているのです。今回はnameフィールドは使わないので、ここを「def find( name ); model[ :id => name ] or not_found; end」と書き換えます。これでRails風にidで個々のレコードを指定できます。

個別のエントリの表示

templates/show.mabを作ります。

個別のエントリを表示する
layout :default, :title=>@word.id do
  p @word.text
  a 'home', :href=>'/words'
end

これで「http://localhost:3000/word/1」などとしてエントリを個別に表示できるようになりました。

個別のエントリ表示
個別のエントリ表示

まとめと次回の予定

Wavesの強力なデフォルト機能と、動的にモデルやコントローラを作り出す機能のおかげで、ビューと設定ファイルの書き換えだけでも一通りのことができます。

次回は認証機能などを追加していきます。

おすすめ記事

記事・ニュース一覧