だるろぐ

だるいぶろぐです

SinatraとDataMapperとERBでサイト作ったのでソースを公開してみる

最初に言っておくとrubyは全然分かりません。


また作って晒してみた。
サイトは http://tiwa.hirafoo.net/ 「違いがわかりません」です。
ソースは http://github.com/hirafoo/tiwa に。
以前の sinatraとActiveRecordとERBでBBS作ったのでソースを公開してみる - だるろぐ跡地 とあまり変わりません。


はい解説ゴー。

  • 基本

railsの構成を模したMVCスタイルで作成。要所要所でrailsのそれとは異なる。

人気の軽量wafです。
今回は最初、勉強がてらRackで作ろうとしてましたが、以下の理由により路線変更し、Sinatraを使うことに。

layout、partial機能をRackが備えていない

railssinatraでは、layout、partialと呼ばれる機能が使える。これは、ビューを共通部分と非共通部分に分けて扱える機能と、部分テンプレートを挿入できる機能だ。詳細は省く。
私はこの機能は、てっきりerbやhamlが標準で備えている機能だと思っていたが、実はrailsSinatraが独自実装していたものだった。そしてRackはこれらの機能を持っていない。
この時点で詰んだ。railsSinatraのソースからこれらの機能を持ってこようと考えたが、そんな事をするくらいならハナからそれらのwafを使うべきだ。よってRackの使用を諦めた。
私はこの事実に実に驚いた。rubyのテンプレート的モジュールであるerb|hamlは、単体での利用を考えられていないのだろうか。これらを使ってサイトを作るときは、wafとの併用が絶対条件とでもされているのだろうか?


さておき、Sinatraはlayout機能は備えているがpartial機能は備えていない。ので、ウノウラボの記事を参考にしつつ独自実装。

  def partial(template, options = {})
    erb "_#{template}".to_sym, options.merge(:layout => false)
  end

ビューの中で呼ぶので、当然ヘルパーとして実装。
なお、erb限定の実装だが、hamlなどを使いたい場合は軽くif文を挟めばよろしいかと。

余談1

perlの標準的なテンプレートモジュールとして位置付けられているTemplate::Toolkitは、モジュール自体がこのlayout、partialに相当する機能を備えている。
当然、wafなどの環境に制限されずこの機能を使うことが出来る。

余談2

実はerb単体でもlayout機能を実現出来る。が、分かりづらい見た目になるのでやめた。

ディスパッチャが無い

Rackの特性上当然と言えば当然だが、ディスパッチャが無い。これもrails/Sinatraは独自実装している。
最初は簡素なディスパッチャを書いていたが、上記と同じ理由によりRackの使用を諦め、窓から投げ捨てた。
rubyのディスパッチャは無いかと軽くググったが、見当たらず。

ハンドラも無い

同上。

とりあえずMとCを読み込む

(Dir::glob("app/{controller,model}/*.rb")).each do |file|
  require file
end

アプリのコアとなるrb(このケースではscript/tiwa.rb)でrequireすれば、任意のM/Cから他のM/Cのメソッドが呼べたりするので楽。
そうなるようにしてるのだから、当然と言えば当然なのだけど。

ビューの位置を変更する

set :views, File.dirname(__FILE__) + '/../app/view'

環境変数でdevelopment/productionを分ける

railsに倣おう。

@env = ENV["TIWA_ENV"] || "development"

この@envは要所要所で使う。例えばdbスキーマの読み込みも

development:
  adaptor: hoge
production:
  adaptor: huga

とでもしておき、

db  = YAML::load_file('config/database.yaml')[@env]

で、環境に応じた設定を読み込む。

ソースの自動再読み込み

Sinatraは0.9.2より、ソースの自動再読み込みがされなくなった。
そんなときはshotgunを使うのだが、(http://d.hatena.ne.jp/foosin/20090611/1244735821)それはそれで穴があったりする。
shotgunでSinatraアプリを起動させるとどうなるか。

% shotgun ./script/tiwa.rb -p 1000
 - later - 
% netstat -tln | grep 1000
tcp        0      0 127.0.0.1:1000              0.0.0.0:*                   LISTEN

この通り、指定したポートは127.0.0.1バインディングされる。つまり、Sinatraを動かす開発マシンをA(10.0.12.1)と、ブラウザからアクセスするマシンをB(10.0.12.2)とでもすると、Aのマシン上で localhost:1000 にアクセスはできても、マシンBから 10.0.12.1:1000 にはアクセス出来ない。
単一のマシンで開発・確認までしているのなら問題は無いが、今回はそうでなかったので困った。
ちなみにshotgunを使わずに起動すると

% ./script/tiwa.rb -p 1000
 - later - 
% netstat -tln | grep 1000
tcp        0      0 0.0.0.0:1000                0.0.0.0:*                   LISTEN

となるので、マシンBから 10.0.12.1:1000 でアクセスできる。


最初は、

Listen 1001
RewriteEngine On
RewriteRule ^/(.*) http://localhost:1000/$1 [P]

というapacheで代用していたが、postした瞬間に死んだので、大人しくpassengerを使用し、always_restart.txtを配置した。 passengerについては http://d.hatena.ne.jp/foosin/20090619/1245426335 に。
どうせshotgunがソースを再読み込みするのは、ソース変更後にリクエストを受けたときなので、shotgunを使おうがpassengerを使おうが待たされる時間とイライラ感に大差は無かった。


尚、上記のポート説明で使ったポート番号は捏造である。

余談

こんな記事もある。
http://blog.s21g.com/articles/1638
動くっちゃ動くが、ソースを変更して保存した直後にアクセスしても再読み込みされず、少し経ってからだとされる。よく分からん。

  • DataMapper

前回のBBS作成時と違うのはここぐらいである。
ARに不満があるわけではなく、同じ事やっても詰まらないのでDataMapper(以下DM)を使ってみた。

setup

先ほど読み込んだdbを使って

DataMapper.setup(:default, {
  :adapter  => db["adaptor"],
  :database => db["database"],
  :username => db["username"],
  :password => db["password"],
  :host     => db["host"]
})

ページング

DMにはdm-paginationというページングを行うモジュールが存在する。が、どうもorderに渡した値が読まれないようだった。
(一応ソースは追って、DM.allを呼ぶのに使っているoptionをダンプしたところちゃんと値を渡しているようだった。が、原因分からず)
ので、自力実装。
モデルのベースクラスを作り、クラスメソッドとして実装。

      def self.paginate(cond)
        page   = (cond[:page] ? cond[:page] : 1).to_i
        offset = (page == 1) ? 0 : ((page - 1) * 10)
        order  = cond[:order]

        cond.delete(:page)
        cond.delete(:order)

        @result = self.all(cond)
        @result = @result.all(:limit => 10, :offset => offset)
        @result = @result.all(:order => order) if order

        (@prev, @next) = (page == 1) ? ((@result.size < 10) ? [nil, nil] : [nil, 2]) :
                         (@result.size == 10) ? [page - 1, page + 1] :
                         [page - 1, nil]

        return @result, @prev, @next

実に簡素である。
ところでDMも使い方のサンプルは少ない。例えば公式マニュアルは
http://datamapper.org/docs/
だが、例えばページングの際のoffsetの指定方法などは載ってない。
DMの基本的な使い方はググれば出るが、ちょっと変わった事をしたくなったら適当にソースを読んだ。
そして、全てが終わったあとに http://www.kuwata-lab.com/book_mdar/index.html を見つけたりする。事前調査はちゃんとしよう。

属性設定、バリデーション

上記ドキュメントか app/model/ 以下参照。説明は不要と思われる。

relation

例えばクラス内で

  belongs_to :entry

などと宣言すればrelationを張ってくれる。が、発行するクエリはjoinしたものではなく、各モデルにそれぞれ非joinのクエリを投げる。
その後、まるでjoinしたかのようなオブジェクトを返してくれる。これはありがたい。
データソースが単一のサーバ上に全て納まっている環境では有り難味は無いのだけれど。

マイグレーション

DMでのマイグレーションはARのようにrakeではなく、クラスメソッドを実行する。

DataMapper.auto_migrate!

script/handle_data に簡単なスクリプトを置いておいた。

  • etc

その他。

モデルのベースクラスを作成。

module Tiwa
  module Model
    class Base
      def increment(column)
        self.update(column => (self.__send__(column) + 1))
      end
# 略
    end
  end
end

Tiwa::Model::Baseクラス(厳密にはこの表現は正しくない気がするが、面倒なのでクラスと呼ぶ。どうせ大差無い)をベースとし、他のクラスではこのクラスを継承する。
このincrementメソッドは、受け取った名前のカラムの値を1増やすだけである。これはCでやろうかとも思ったが、
http://twitter.com/hirafoo/status/5762365040
http://twitter.com/kamipo/status/5762395858
http://twitter.com/hirafoo/status/5762459759
http://twitter.com/kamipo/status/5762531208
とのこと。
どっちにしろ、Cにはロジックを極力詰め込まないようにするのが望ましい。
MVCのなんたるかについてはここでは触れない。


そしてこのように、rubyでは任意の名前のメソッドを動的に実行する場合、__send__メソッドを用いる。
perlでは普通に

$obj->$method_name

で呼べる。

  • console

railsの script/console が使いたいと思った。/usr/bin/irb のコードに、script/tiwa をrequireさせるだけで使えた。
ruby素晴らしい。

LoadModule env_module modules/mod_env.so
SetEnv TIWA_ENV production


毎度の通り長々と書いたが、やった事はこんなところ。
そして冒頭の通り、私はrubyが全然分からない。pとmethodsにお世話になっている。riもrefeもイマイチ助けにならない。
この記事、そしてtiwaのコードにも嘘や間違いや残念な個所が多分に含まれていると思われる。