sinatraとActiveRecordとERBでBBS作ったのでソースを公開してみる

rubyのwafでsinatraが最近人気なのでBBS作ってソース晒してみた。
http://github.com/hirafoo/sinatra_bbs/tree/master
rubyrailsも、ほぼ知識無しの状態でやったので色々見苦しいはず。俺が使えるのはrailsの中でのマイグレーションのみです。


sinatraについては以下が大変参考になります。
http://labs.unoh.net/2009/05/sinatra.html
第9回 SinatraとSequel・Hamlで掲示板アプリを作る:Ruby Freaks Lounge|gihyo.jp … 技術評論社


作ったBBSの機能、特徴など。


にげっとのソースを晒したときは、自分なりのノウハウやらテクいことを詰め込んだつもりなのだけど、全然つっこみが無くて寂しかったので軽く解説してみる。

railsライクなソースの配置

sinatraは非常に軽量なwafで、.rb一つで動かす事が出来る。公式のトップにあるコードのみで動くので参照されたし(実はsinatra0.9.2の時点でsinatra自体にバグがあるのでこのままでは動かない。下の方に対策を書いてあります)ここでは start.rb とする。
http://www.sinatrarb.com/
このstart.rbにMVCの全てを詰め込む事もできるが、やはりMVCのそれぞれでファイルを分けたい。
sinatraではMVC的なファイルの配置は、ビュー以外は用意されていない。アプリケーションルート(以後 $APP_ROOTと表記)にソースを置く事ができ、ビューだけは $APP_ROOT/views が標準で使われる。
モデルやコントローラを同じディレクトリに置きたくないので、railsと同じ配置にしてみる。


$APP_ROOT/app 以下に controllers models views のディレクトリを作成。
start.rbに以下を記述。

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

( Dir::glob("app/controllers/*.rb") ).each do |controller|
  load controller
end

( Dir::glob("app/models/*.rb") ).each do |model|
  require model
end

モデルとコントローラはそれぞれ1階層しかファイルを探索しないが、個人的にモデルとコントローラは1階層でいいと思うので。俺の経験が浅いからかもだけど。
ともかく、こうすることで start.rb にはアプリ全体の設定などのみを記述して、MVCはそれぞれのファイルに書くことが出来る。
ビューのディレクトリ指定方法はsinatraのソース読んだ。ドキュメントには指定方法載ってなかったと思う。有ったら俺の阿呆。

ORマッパにActiveRecordを使用

sinatraはいわゆるMVCフレームワークではない。持つのはuriディスパッチャ的なもののみ。
モデルとビューは自分で用意してやる必要がある。(余談だけどcatalystもそうだよね)
ネットで見るsinatraの紹介ではモデルにはSequelを使用する例が多いが、ARを使ってみた。
ARをrailsの外で、つまりAR単体で使うのは割と簡単。

require 'rubygems'
require 'activerecord'

で済む。
ARを選んだのは単純に興味があったからなのだけど、後でやっぱりARにして良かったと思う事になる。

railsと同じコマンドでマイグレーション

一度あのマイグレーションを使ったら、もうそれ以外でスキーマ管理する気になれないので。
migrateはググったら出てきた。rollbackはフィーリングで。
Rakefileを作成。

require 'config/boot'
require 'db/migration_helper'

namespace :db do
  desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"

  task :migrate do
    ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
  end

  task :rollback do
    ActiveRecord::Migrator.rollback("db/migrate/")
  end
end

rake db:migrate と rake db:rollback のみしか使えず、STEPの指定なども出来ないが、正直これだけで俺は十分。
というか分からないのでやってない。create/dropも分からない。修正希望点。

マイグレーションの拡張

http://d.hatena.ne.jp/foosin/20090531/1243765471 と同じ。
今回は db/ 以下に配置。そして Rakefile でrequireするだけ。
本当に皆afterや外部キー制約要らないのか?

セッションをデータベースに保存

ここが一番悩んだ。
sinatraはクッキーベースのセッションしか対応していない。
正確には、対応しているらしいが、ドキュメントが白紙である。
http://www.sinatrarb.com/book.html#database_based_sessions
クライアントにセッションの内容まで持たせるのはセキュリティ的に考えられないので、セッションの内容はデータベースに保存、クッキーにはセッションIDのみを保存するようにしたい。
そこで、クッキーからはクッキーが持つ、自身を一意に特定するIDを取得し、そのIDとセッションに持たせたいデータを紐付けて、データベースに保存する。ユーザがアクセスした際は、クッキーからIDを取得し、データベースにそのIDで問い合わせを行い、セッションの内容を取得する…という仕様にした。
セッションの内容をデータベースに保存するには、データのシリアライズが必要である。
ここでARを使ってよかった点その1だ。ARはデータのシリアライズをする機能を備えている。
使い方も簡単。

class Session < ActiveRecord::Base
  serialize :data
end

これで、sessionsテーブルのdataカラムには、自動でシリアライズされたデータが保存される。もちろん取得時も自動でデシリアライズしてくれる。
データはYAML形式でシリアライズされるだけなので、平文で入るのがちょっと残念ではあるが、手動でやるより非常に楽。
もし平文で入るのが嫌なら手動でYAML形式にシリアライズ後、Base64なりAESなり使えばいいかと。
あと、クッキーから取得したIDがとても長かったのでSHA1でハッシュ化した。


余談だが、perlのwafのcatalystにも、同じことをするモジュールがある。
http://search.cpan.org/perldoc?Catalyst::Plugin::Session::Store::DBIC
こちらはデータをStorableというモジュールを使い独自の形式に変換後、Base64をかけている。

認証

「このページはログインしていないと見れない」という処理はよくある。
普通はその処理の直前や特定のクラスにだけ、ログインを確認する処理を挟むが、sinatraにはそんな機能は無い。むしろuri的なクラスの概念が無い。
一応フィルタはあるが、「全ての処理の前に行う」というフィルタしかない。
仕方が無いので、そこでセッションを確認してその結果を変数に持たせ、ログインが必要な処理の直前でその変数を確認する、という方法を取った。
具体的には、まずセッションを確認するフィルタを作成。

before do
  if request.cookies['rack.session']
    session_id = Digest::SHA1.hexdigest( request.cookies['rack.session'] )
    if @session = Session.find_by_session_id( session_id )
      @login_user = @session.data[:user]
    end
  end
end

で、ログインの確認。

get '/entry/' do
  if @session
    erb :'entry/index'
  else
    myerror "please login"
  end
end

ここでは、@sessionにセッションの有無を保存。/entry/にアクセスされた際、@sessionに値が入っている、すなわちセッションが有効ならばページを表示、そうでなければエラーとしている。


クッキーからIDを取得する方法に自信が無い。多分、別のRackを使ったアプリと混ざったら残念なことになるかもしれない。

ページング

さすがに手動でページングするのは厳しいので、railsにおける標準的なページングモジュールであるwill_paginateを使用する。
これはARに対応している。ARを使って良かった点その2だ。
だが、これをrailsの外で使う方法が分からなかった。各モジュールをrequireすればいいのかと思ったのだけど、それだとモデルにpaginateメソッドが生えない。
#rails-tokyo@freenodeで質問して教えてもらった。答えてくれたmorohashiさんに感謝。
start.rbに以下を記述。

require 'will_paginate'
require 'will_paginate/core_ext'
require 'will_paginate/view_helpers'

WillPaginate.enable_activerecord

つまり WillPaginate.enable_activerecord が足りなかった。ちなみにこれ、will_paginateのソースの先頭にそれっぽいのが書いてある。
分からなければソース読め、ということだ。反省。


これでデータを取得する際の処理は出来たのだが、ビューの中でページャリンクを作成するためにwill_paginateを使う方法は分からなかった。
仕方ないので非常にお粗末なリンクを置いといた。修正希望点。

データベース接続情報の外部ファイル化

プログラム中に定義してもいい情報と、どう考えてもすべきでない情報というものがある。
DBへのコネクション情報は明らかに後者。
アプリの起動時と、マイグレーション時に使用する。
railsと同じ配置で、コネクション情報を書いたyamlを用意する。

db_config = YAML::load_file('config/database.yml')
ActiveRecord::Base.establish_connection(db_config)

ビューでフィルタを使用する

以下参照。
http://www.sinatrarb.com/faq.html#escape_html
こう書けるわけだ。

<%=h hoge %>

変数hogeに、htmlの仕様上エスケープすべき文字があれば、エスケープしてくれる。

sinatraWEBrickで起動しようとしたらエラーが起こる

以下参照。
http://d.hatena.ne.jp/unageanu/20090525/1243268383

ソースを編集した後、WEBrickを再起動するのが面倒

以下参照。
http://d.hatena.ne.jp/foosin/20090611/1244735821


だいたいこんなところですか。
rubyの知識ほぼ無し、ましてやrubyでアプリ作ったのも初めてなので色々残念なところがあると思うので、突っ込んでもらえると喜びます。
以下、todo的なもの。

分かる人居たら教えてもらえるとありがたいです。




ところで。
作ってる最中に自分でちゃんと感づいてはおりました。


それなんてrailsアプリ?


軽量が売りのはずのsinatraが随分ゴテゴテしてしまったという。
勉強になったのでよしとする。