«前の日記(2006-10-03 (火)) 最新 次の日記(2006-10-05 (木))»  

まちゅダイアリー


2006-10-04 (水)

Rails に(再)挑戦 3日目 - 更新順序で並び替え

昨日はビューの見栄えを変えるところまで。 今日はロジック(モデルとコントローラ)に手を加えてみよう。 今の状況だと、各ページは id 順(つまり古い順)に並んでいる。 これを、更新した順(新しい順)に表示されるようにする。

更新順に並べるために、まずは Page クラスへ更新時刻とついでに作成時刻を追加する。

マイグレーション

migration の機能を使って、データベースの pages テーブルに更新時刻と作成時刻を追加する。 マイグレーションファイルの雛形は、 script/generate で生成できる。

$ script/generate migration AddUpdatedToPage
      exists  db/migrate
      create  db/migrate/002_add_updated_to_page.rb

pages テーブルに対して、生成時刻 (created) と更新時刻 (updated) のカラムを追加する。 検索速度を速くするために、それぞれにはインデックスを張るようにしている。

$ vi db/migrate/002_add_updated_to_page.rb
class AddUpdatedToPage < ActiveRecord::Migration
  def self.up
    add_column :pages, :created, :time
    add_index  :pages, :created
    add_column :pages, :updated, :time
    add_index  :pages, :updated
  end

  def self.down
    remove_column :pages, :created
    remove_column :pages, :updated
  end
end

rake コマンドを使って、データベースのスキーマを修正する。

$ rake migrate
(in /home/machu/work/misc/rails/rn)
== AddUpdatedToPage: migrating ================================================
-- add_column(:pages, :created, :time)
   -> 0.0275s
-- add_index(:pages, :created)
   -> 0.0084s
-- add_column(:pages, :updated, :time)
   -> 0.0300s
-- add_index(:pages, :updated)
   -> 0.0040s
== AddUpdatedToPage: migrated (0.0734s) =======================================

コントローラの修正

データベースの準備ができたので、NoteController に手を加える。

一覧表示 (list) にて、更新時刻の新しい順に並び替えるようにオプション(:order => 'updated DESC')を指定する。 それから、新規作成 (create) と更新 (update) の時に、生成時刻と更新時刻と設定するようにした。

$ vi app/controllers/note_controller.rb

class NoteController < ApplicationController
  def list
    @page_pages, @pages = paginate :pages,
                                   :per_page => 10, :order => 'updated DESC'
  end

  def create
    @page = Page.new(params[:page])
    @page.created = Time.now
    @page.updated = Time.now
    if @page.save
      # 中略
    end
  end

  def update
    @page = Page.find(params[:id])
    @page.updated = Time.now
    if @page.update_attributes(params[:page])
      # 中略
    end
  end
end

ビューの修正

せっかく生成時刻と更新時刻を記録するようにしたので、画面にも表示するようにビューを修正する。 具体的には Page の共通ビュー (_page.rhtml) に下記の2行を追加している。 このビューは一覧表示 (list) と個別表示 (show) で共通に使われるようにしているので、一箇所を変えれば一覧表示と個別表示の両方に反映される。 共通化しておいて良かった。

$ vi app/views/note/_page.rhtml

  created: <%= page.created.strftime('%Y-%m-%d %H:%M') if page.created %>
  updated: <%= page.updated.strftime('%Y-%m-%d %H:%M') if page.updated %>

データの移行

これまでに作られたページには生成時刻も更新時刻も含まれていない。 そこで、コンソール (script/console) を使って、既存のデータに生成時刻と更新時刻を設定する。 迷ったけど、とりあえず現在時刻を両者に設定することにした。

$ script/console
Loading development environment.
>> Page.find(:all).each do |page|
?> page.created = Time.now
>> page.updated = Time.now
>> page.save
>> end

これで、新規に作ったページや更新したページが、上に表示されるようになった。

Tags: Ruby Rails

Subversion への登録

この時点で、いったんソースコードをリポジトリ (Subversion) に保存しておこう。 Rubyist - バリケンのRuby日記 - Subversionの練習を参考にしながら、不要なファイルやディレクトリを登録対象から除外する。

$ svn add *
$ svn remove --force log/*
$ svn propset svn:ignore "*.log" log
$ svn remove --force tmp/sessions/*
$ svn propset svn:ignore "*" tmp/
$ svn propset svn:ignore "*" tmp/sessions
$ cp db/development.sqlite3 .
$ svn remove db/development.sqlite3
$ svn propset svn:ignore "*.sqlite3" db/
$ svn commit -m 'ignore database' db
$ mv development.sqlite3 db

ついでに trac のリポジトリブラウザでソースを見られるようにした。 これで安心して、ソースを修正できるよ。

更新時刻の設定をコントローラからモデルへ

さっきの例では、ページの更新時刻をコントローラー (note_controller.rb) で設定していた。 でもよく考えると、コントローラのアクションが増えるごとに、同じような処理を書かないといけない。 既に、 NoteController の create と update で、同じ処理(@page.updated = Time.now)が書かれているし。 コントローラをシンプルに書けるのが Rails のいい所らしいので、処理をコントローラ (note_controller.rb) からモデル (page.rb) へと移す。

Rails のモデル (ActiveRecord::Base) にはコールバックという仕組みがあって、生成や更新時などに実行したい処理を登録できるようになっている。 生成時 (before_create) と更新時 (before_save) にそれぞれ現在時刻を設定する処理を追加した。

Index: app/models/page.rb
===================================================================
--- app/models/page.rb  (リビジョン 2)
+++ app/models/page.rb  (作業コピー)
@@ -1,4 +1,7 @@
 class Page < ActiveRecord::Base
+  before_create {|model| model.created = Time.now }
+  before_save   {|model| model.updated = Time.now }
+
   def title
     body.each {|title| return title.chomp }
   end

そして、先ほど追加したコントローラ (note_controller.rb) の create と update の処理を削除する。

Index: app/controllers/note_controller.rb
===================================================================
--- app/controllers/note_controller.rb  (リビジョン 2)
+++ app/controllers/note_controller.rb  (作業コピー)
@@ -23,8 +23,6 @@

   def create
     @page = Page.new(params[:page])
-    @page.created = Time.now
-    @page.updated = Time.now
     if @page.save
       flash[:notice] = 'Page was successfully created.'
       redirect_to :action => 'list'
@@ -39,7 +37,6 @@

   def update
     @page = Page.find(params[:id])
-    @page.updated = Time.now
     if @page.update_attributes(params[:page])
       flash[:notice] = 'Page was successfully updated.'
       redirect_to :action => 'show', :id => @page

これでもちゃんと、生成時刻と更新時刻が設定されていることを確認。 うん。大丈夫そうだ。

リポジトリへも登録しておいた。

さらに簡略化

さらに調べてみると、モデルに created_on と updated_on というカラムを作っておくと、生成時刻と更新時刻を Rails が自動的にセットしてくれるらしい。 惜しい…「_on」が足りなかった。 でも仮に、この機能を知らずに created_on, updated_on なんて名前を付けていたら、それはそれで不思議な挙動に悩んでいただろうな。 モデルのコールバックの使い方も覚えられたし、まぁいいか。

とりあえず、 created を created_on に、 update を updated_on に変更する。 まずはデータベースのマイグレーションから。

$ vi db/migrate/003_rename_update_of_page.rb

class RenameUpdateOfPage < ActiveRecord::Migration
  def self.up
    remove_index  :pages, :created
    remove_index  :pages, :updated
    rename_column :pages, :created, :created_on
    rename_column :pages, :updated, :updated_on
    add_index     :pages, :created_on
    add_index     :pages, :updated_on
  end

  def self.down
    remove_index  :pages, :created_on
    remove_index  :pages, :updated_on
    rename_column :pages, :created_on, :created
    rename_column :pages, :updated_on, :updated
    add_index     :pages, :created
    add_index     :pages, :updated
  end
end

この内容でマイグレーションを実行すると、なぜか既存の時刻の内容が消えてしまった (created_on と updated_on が空になった) 。 まぁ、今はサンプルデータだから支障は無いんだけど。

他にもモデル・コントローラ・ビューを修正する。 Webブラウザからアクセスして、ちゃんと更新順に並ぶことを確認した。

screenshot (Ruby on Rails) - (6)

Tags: Ruby Rails

時刻の整形をビューからヘルパーへ

さらにコードの見直し。 生成時刻と更新時刻を表示する時に、時刻のフォーマットを整形しているんだけど、これをビュー (_page.rhtml) に直接書いていた。 ビューの中に nil チェック(if page.created の箇所)が入っているし、時刻のフォーマットを変えるたびに複数の箇所を修正しないといけなくて面倒。

created: <%= page.created_on.strftime('%Y-%m-%d %H:%M') if page.created %>
updated: <%= page.updated_on.strftime('%Y-%m-%d %H:%M') if page.updated %>

Rails では、ビューの見栄えを整えるために、ヘルパーという仕組みが用意されていて、ここで定義したメソッドはビューから直接呼べるようになってる(ヘルパーは module なので、きっとビューに include されるんだろう)。 昨日使った文字を切り取る truncate メソッドも、デフォルトで用意されているヘルパーの一つみたい。

ヘルパーを独自に用意する場合は、 app/helpers/note_helper.rb を編集する。 とりあえず、時刻を整形する show_time メソッドを追加する。

module NoteHelper
  def show_time(time)
    time ? time.strftime('%Y-%m-%d %H:%M') : ''
  end
end

ビュー (_page.rhtml) 側はヘルパーを使うように修正する。

created: <%=h show_time(page.created_on) %>
updated: <%=h show_time(page.updated_on) %>

見た目や動きは変わらないけど、コードはすっきりした。 リポジトリにも登録して、おしまい。

Tags: Rails Ruby