File.exists?を使わずに…
<% if File.exists?(model.path) %>
...
<% end %>
ではなく、
<% if model.file_exists? %>
...
<% end %>
というのが37signals流。メリットは、
- ビューがすっきりする
- 結果をキャッシュできる
- ユニットテストしやすい
- ローカルファイルシステムから別システム(S3とかMogileFSとか)へ移すことも想定
- MVCとしてモデルがファイルを参照するなら、それがどこにあるかはモデルだけが把握すべき
といったことが挙げられています。
A design and usability blog: Signal vs. Noise (by 37signals)
http://37signals.com/svn/posts/1944-dont-do-this-if-fileexistsmodel
[AD] Railsが自社Webサービス開発に向く10+1の理由

MOONGIFTではRailsによる受託開発、教育を行っております。そして今回、なぜRailsが良いのかという理由について挙げてみましたのでRails開発に迷いを感じられている方は参考にしていただければと思います。書いてあることは前々から言われているようなことばかりですが…。
まず第一前提としてよく「Railsは生産性が高い」と言われますが、今回は書かないようにしています。目に見えづらく、経験則によるものなのでかえって怪しく見えてしまうからです。数字にまで落とし込めれば良いのですが、正確性がないので今回は省きます。
詳細は以下より。
BackgrounDRbを使ってControllerのメソッドを呼び出す方法
が、いまいち分からなかったりします。やりたいことはApplicationController内に定義してある関数を呼び出す処理。BackgrounDRbの中で、expire_pageを呼び出したい時もあると思うので。キャッシュの生成処理をBackgrounDRb内でやらせるのは高速化につながるのではないかと思います。そうすると、キャッシュの生成にかかるコストをバッググラウンドでやらせつつ、表示が高速化できるようになります。
妥協的な方法は以下。
CSSとHTML、そしてRailsを使ってPDFを作成するTips
subLog : HTML / CSS to PDF using Ruby on Rails
答えを言ってしまうと、Prince XMLを使います。各種プラットフォーム向けに提供されているライブラリなので、これを使えば手軽に作成できそうです。そのためのライブラリprince.rbとpdf_helper.rbが公開されています。ちなみにPrince XMLは個人の非商用利用に限りフリーのライブラリなのでご注意ください。

Prince XMLのトップページ
subLog : HTML / CSS to PDF using Ruby on Rails
http://sublog.subimage.com/articles/2007/05/29/html-css-to-pdf-using-ruby-on-rails
Railsで開発するときにはキャッシュを予め想定に入れておく
via En attendant Jérémie on Flickr – Photo Sharing!
今回、まともにキャッシュを使おうと思い始めたのですが、ある程度組み上げた段階でキャッシュを組み入れようとすると色々問題が起こることが分かりました。まず知識としては、Railsのキャッシュは3パターンあります。
- Page
- Action
- Fragment
があります。Pageは一番強力で、一度生成後はApacheやlighttpdなどのWebサーバレベルでレスポンスを返してくれます。アプリケーションサーバを通さない分、高速です。が、逆に言えば生成した後はRails側で制御できない状態になるので、表示を細かに変更することはできません。
次にAction Cacheがあります。これはPageアクション同様に生成されるページ全体をキャッシングしますが、Railsのアクションは実行するという点が異なります。アクションを通るので、DBにアクセスして情報を取得することが可能というメリットはありますが、DBへのアクセスがボトルネックになっている場合は使えません。
最後にFragmentキャッシュです。これはViewの中で細かく利用できます。言わば部品を定義していくことで、一部分だけの表示をキャッシングしていくことができます。細かく制御できる利点はありますが、裏を返せば多数使うと管理が大変になります。
では次に問題点や解決策を挙げてみたいと思います。
AtomFeedHelperを使ってネームスペース定義
結構はまった。結局のところバグのようで。次のリリースでは修正されるはずです。
views/hoge/index.atom.builder内において、
atom_feed({’xmlns:openSearch’ => ‘http://a9.com/-/spec/opensearch/1.1/’}) do |feed|
等と定義しても無視されます。ソースは「actionpack-2.0.2/lib/action_view/helpers/atom_feed_helper.rb」で、ネームスペースっぽい定義が見つからない。
Module: ActionView::Helpers::AtomFeedHelper
http://caboo.se/doc/classes/ActionView/Helpers/AtomFeedHelper.html#M006099
ここを見る限り、
Other namespaces can be added to the root element:
app/views/posts/index.atom.builder:
atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
と書かれてはいるのだが、ソースを見る限りできそうにない。で、探してみたらパッチがあった。
#10304 ([PATCH] Atom feed helper should allow to add new namespaces to the feed root element) – Rails Trac – Trac
http://dev.rubyonrails.org/ticket/10304
86 feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'}
87 feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)}
88
89 xml.feed(feed_opts) do
この辺りが修正範囲。xml〜ではじまっていればネームスペースとして見なす模様。これで無事、Atomフィードに任意のネームスペースが追加できるようになる。
Rails2.0によるAtom/RSSフィードの実装(GET)
AtomPubっぽいものを出力する方法です。
- http://localhost:3000/example/index HTML
- http://localhost:3000/example/index.rss RSS2.0
- http://localhost:3000/example/index.atom Atom
という形で定義します。
Rails2.0の場合は、フォーマットによって出力を簡単に変更できますので、それを使います。
respond_to do |format|
format.html
format.atom {
render(:layout => false)
}
format.rss {
render(:layout => false, :action => "index.rxml")
}
end
これで、Atomの場合は/views/example/index.atom.builderが、RSSの場合は/views/example/index.rxmlが読み込まれます。
Atomのテンプレートは以下のようになります。
atom_feed do |feed|
feed.title("#{@title} » Example Feed")
feed.updated(@memos.first.created_at)
@memos.each do |m|
feed.entry(m, :url => create_link(m)) do |entry|
entry.title(m.title)
entry.content(trans(m), :type => "html")
entry.author do |a|
a.name(@user.name)
end
end
end
end
これはベースのようなものです。@memosというリストごとにフィードのエントリーを定義して、タイトルやcontentを作成しています。次にRSS側も同様に処理できます。
xml.instruct!
ml, :version => "1.0", :encoding => "UTF-8"
xml.rss('version' => '2.0', 'xmlns:atom' => 'http://www.w3.org/2005/Atom', 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearchrss/1.0/') do
xml.channel do
xml.title @title
url = url_for(:controller => :memo, :action => :index, :id => @user.name,
nly_path => false)
xml.link url
xml.tag!("atom:id", url)
xml.description(@title)
xml.language "en-us"
xml.ttl "40"
xml.pubDate(Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z"))
xml.managingEditor(@user.name)
xml.generator("Jot a Memo")
xml.tag!("openSearch:totalResults", @memos.paginate.total_entries)
xml.tag!("openSearch:startIndex", @startIndex)
xml.tag!("openSearch:itemsPerPage", @perpage)
xml.guid(url, "isPermaLink" => 'false')
xml.tag!("atom:updated", Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z"))
@memos.each do |m|
xml.item do
xml.title(m.title)
xml.tag!('atom:id', create_link(m))
xml.guid(create_link(m), "isPermaLink" => 'false')
xml.pubDate(m.created_at)
xml.tag!("atom:updated", m.updated_at)
xml.tag!("atom:summary", trans(m))
xml.link(create_link(m))
xml.description(trans(m))
xml.guid(create_link(m))
xml.author(m.user.name)
end
end
end
end
やたらと長いのですが、あまり気にしないでください。大事なのは、xml.tag!を使うと、<atom:id>…</atom:id>のような形式が定義できるということと、xml.guid(aa, bb)のように二つ目の引数を定義すると、それがattributesになるということです。
後は<rss>の中に拡張書式を定義する場合があると思いますが、それもHashで指定可能です。
このように定義しておくと、次に以下のようなURLを考えた場合も容易に対応できます。
- http://localhost:3000/example/view/id HTML
- http://localhost:3000/example/view/id.rss RSS2.0
- http://localhost:3000/example/view/id.atom Atom
でidで指定されたデータの詳細表示を行う場合です。この場合は以下のように対応します。
@memos = [@memo]
respond_to do |format|
format.html
format.atom {
render(:layout => false, :action => "index.atom.builder")
}
format.rss {
render(:layout => false, :action => "index.rxml")
}
end
@memoという1件のデータを@memosに配列で入れた以外は、indexと同じ処理になります。実際、フィードで1件を表示する場合も複数件表示する場合も処理は変わらないので、これで対応できます。もちろん、HTMLの場合は異なると思いますので、view.rhtmlは内容が異なるかと思います。
とは言ってもこれではGETだけの対応なので、これからPOST/PUT/DELETEの実装を完了してはじめてAtomPubへの対応が完了したと言えるのですが。
RubyでGoogle Contacts Data APIを呼び出す
まだ基礎だけですが。とりあえず使うのはGDataライブラリです。
$ sudo gem install gdataSuccessfully installed GData-0.0.4Installing ri documentation for GData-0.0.4…Installing RDoc documentation for GData-0.0.4…
スクリプトは以下のように。
/opt/local/lib/ruby/gems/1.8/gems/GData-0.0.4/lib/gdata/google_contact.rb
require 'gdata/base'
require 'builder'
module GData
class GoogleContact < GData::Base
def initialize
super 'cp', 'gdata-ruby', 'www.google.com'
end
def list
request "/m8/feeds/contacts/#{@email}/base"
end
def authenticate(email, password)
@email = email
super email, password
end
end
使い方は簡単で、
require ‘gdata/google_contact’
>> gc = GData::GoogleContact.new
=> #<GData::GoogleContact:0×157cd34 @url=”www.google.com”, @source=”gdata-ruby”, @service=”cp”>
>> gc.authenticate “example@gmail.com”, “[your password]”
=> {”Authorization”=>”GoogleLogin auth=…\n”, “Content-Type”=>”application/atom+xml”}
>> gc.list
でリストがXMLで返ってきます。本当はこれをさらに解析してvCard形式とかでもとれるようになっていると便利そう。実際に返ってくるデータは次のような感じ(整形しています)。
=> ”
<?xml version=’1.0′ encoding=’UTF-8′?>
<feed xmlns=’http://www.w3.org/2005/Atom’ xmlns:openSearch=’http://a9.com/-/spec/opensearchrss/1.0/’ xmlns:gContact=’http://schemas.google.com/contact/2008′ xmlns:gd=’http://schemas.google.com/g/2005′>
<id>example@gmail.com</id>
<updated>2008-03-10T05:05:34.160Z</updated>
<category scheme=’http://schemas.google.com/g/2005#kind’ term=’http://schemas.google.com/contact/2008#contact’/>
<title type=’text’>Atsushi Nakatsugawa’s Contacts</title>
<link rel=’alternate’ type=’text/html’ href=’http://www.google.com/’/>
<link rel=’http://schemas.google.com/g/2005#feed’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base’/>
<link rel=’http://schemas.google.com/g/2005#post’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base’/>
<link rel=’self’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base?max-results=25′/>
<link rel=’next’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base?start-index=26&max-results=25′/>
<author>
<name>Atsushi Nakatsugawa</name>
<email>example@gmail.com</email>
</author>
<generator version=’1.0′ uri=’http://www.google.com/m8/feeds’>Contacts</generator>
<openSearch:totalResults>376</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>25</openSearch:itemsPerPage>
<entry>
<id>http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base/0</id>
<updated>2007-11-26T02:27:10.592Z</updated>
<category scheme=’http://schemas.google.com/g/2005#kind’ term=’http://schemas.google.com/contact/2008#contact’/>
<title type=’text’>Atsushi Nakatsugawa</title>
<link rel=’self’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base/0′/>
<link rel=’edit’ type=’application/atom+xml’ href=’http://www.google.com/m8/feeds/contacts/moongift%40gmail.com/base/0/1196044030592000′/>
<gd:email rel=’http://schemas.google.com/g/2005#other’ address=’example@moongift.jp’ primary=’true’/>
</entry>
:
タイトルにユーザ名が入って、gd:emailのaddressにメールアドレスが入る形ですね。個別で取得する場合はidを利用するようです。その他、住所などのデータも入っている場合は取得できます。とは言え、http://schemas.google.com/g/2005が見られないので、項目の詳細は不明です。
が、メールアドレスのリストだけであればこれで取得できるので便利です。
will_paginate + acts_as_searchableで全文検索+ページネーション
Rails2.0からページネーションがプラグインベースになりました。普通のページネーションであれば、
@memos = @user.memos.paginate(:page => params[:page], :per_page => 10,
rder=>”memos.id DESC”)
だけで使えるようになって便利ですが、任意の情報に対してページネーションする方法がいまいち分かっていませんでした。そして、acts_as_searchableを使って、HyperEstraierで全文検索を行う際に、ページネーションをどのように行えば良いのか調べつつ分かったところを書いてみたいと思います。
実際にページネーション処理を行っているのは、
vendor/plugins/will_paginate/lib/will_paginate/finder.rb
になります。ここの
def paginate_by_sql(sql, options)
WillPaginate::Collection.create(*wp_parse_options!(options)) do |pager|
query = sanitize_sql(sql)
options.update
ffset => pager.offset, :limit => pager.per_page
original_query = query.dup
add_limit! query, options
# perfom the find
pager.replace find_by_sql(query)
unless pager.total_entries
count_query = original_query.sub /bORDERs+BYs+[w`,s]+$/mi, ''
count_query = "SELECT COUNT(*) FROM (#{count_query}) AS count_table"
# perform the count query
pager.total_entries = count_by_sql(count_query)
end
end
end
が参考になります。
pager.replaceに、リストを入れて、pager.total_entriesに行数を入れればOKのようです。この処理をまねて、
app/controllers/application.rb
に次のように定義します。
module WillPaginate
module Finder
module ClassMethods
def paginate_by_fulltext_search(query, options)
WillPaginate::Collection.create(*wp_parse_options!(options)) do |pager|
pager.replace fulltext_search(query, options)
count_options = Hash.new
count_options[:count] = true
count_options[:attributes] = options[:attributes] if options[:attributes]
pager.total_entries = fulltext_search(query, count_options)
end
end
end
end
end
です。そして、呼び出し側では次のようにします。
@memos = Memo.paginate_by_fulltext_search(params[:q], :attributes => “user_id NUMEQ %d” % @user.id, :limit => perpage,
ffset => offset, :page => params[:page], :per_page => perpage)
ここで、:limitはacts_as_searchable用、:per_pageはwill_paginate用になります。:pageや:offsetは共通で利用できるようです。:attributesはあれば設定します。これさえ設定しておけば、ビュー側は
<%= will_paginate @memos, :prev_label=> _(’« Prev’), :next_label=> _(’Next »’) %>
が利用できて便利です。ちなみにpaginate_by_sqlではoptionsがアップデートされるので、デフォルトの30件になってしまうようですね。これはなぜなんだろう…。


