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が備えていない
railsやsinatraでは、layout、partialと呼ばれる機能が使える。これは、ビューを共通部分と非共通部分に分けて扱える機能と、部分テンプレートを挿入できる機能だ。詳細は省く。
私はこの機能は、てっきりerbやhamlが標準で備えている機能だと思っていたが、実はrailsやSinatraが独自実装していたものだった。そしてRackはこれらの機能を持っていない。
この時点で詰んだ。railsやSinatraのソースからこれらの機能を持ってこようと考えたが、そんな事をするくらいならハナからそれらの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のコードにも嘘や間違いや残念な個所が多分に含まれていると思われる。