Railsでブログを作ろう!(Creating a Weblog in 15 minutes)

Ruby on Railsのサイトで紹介されている有名なscreencastデモ"Creating a Weblog in 15 minutes"を自分の勉強を兼ねて勝手にドキュメント化してみました(解説はこちらで適当にしました。DHHさんのものではありません)。デモは、Mac OSXの環境で、MySQL, iTerm, TextMate, CocoaMySQLを使って行われています。

1. brablogプロジェクトの作成

iTerm(Terminal)を立ち上げて、brablogという名前でプロジェクトを作ります。

 $ rails brablog

作成されたbrablogのフォルダに移動し、webrickサーバーを起動して、プロジェクトが立ち上がっているか確認します。

 $ cd brablog
 $ ./script/server

ブラウザで、 http://localhost:3000/ へアクセス。RailsのWelcomeページが表示されましたね?

2. Blogコントローラの作成

iTermで別セッションを開き(command+T)、そこからBlogプロジェクトの関連ファイルをTextMate上にオープンします。

 $ cd brablog
 $ mate .

既に多数の関連ファイルが作成されているのがわかりますね。
次に、script/generateコマンドを使って、Blogコントローラを作成します。

 $ ./script/generate controller Blog

http://localhost:3000/blog にアクセスすると、indexに対するactionが定義されていないとのエラーメッセージが出ました。
では、indexを定義しましょう。TextMateに移って、app-controllerフォルダにあるblog_controller.rbをオープンし、以下を加えます。

 def index
   render :text => "Hello World!"
 end

http://localhost:3000/blog にアクセスすると、今度は"Hello World!"の文字が表示されましたね?
今度は、def index endの定義だけを残して、中身のrender :text...を削除します。代わりに、app-views-blogフォルダ内に、index.rhtmlを作成して、そこに以下を打ち込みましょう。

hello from the template

同様にブラウザでアクセスして、上の文字が表示されましたか?
では、blog_controller.rb内のdef index endを削除して、どうなるか見てみましょう。これでもindex.rhtmlの内容が同じように表示されましたね。

3. データベースへの設定

次に、blogのデータを格納するデータベースの準備します。最初に、若干の設定をします。configフォルダにあるdatabase.ymlを編集しましょう。developmentのデータベースをblog_development,testのものをblog_testとします。usernameなど他の設定はご自身の環境に合わせてください。

development:
  adapter: mysql
  database: blog_development
  username: root
  password: 
  socket: /tmp/mysql.sock

test:
  adapter: mysql
  database: blog_test
  username: root
  password: 
  socket: /tmp/mysql.sock

production:
  development

4. データベースの作成

次に、CocoaMySQLを立ち上げて、blog_developmentデータベースを作成し、postsテーブルを作成します。テーブル名は複数形(posts)にします。コラムは、id(auto_increment, primary key), title(varchar 255)を作成します。なお、後述しますが、現行バージョンのRailsではmigrationというデータベースのスキーマ管理機能があるので、テーブルの作成はそちらを使う方が便利です。

5. Postモデルの作成

script/generateコマンドを使って、モデルを作成します。

 $ ./script/generate model Post

6. scaffoldメソッド

次に、blog_controller.rbのBlogControllerクラス内に以下の一行を記述します。

 scaffold :post

データベースの設定を変更したので、サーバを立ち上げ直しましょう。

 $ ^C
 $ ./script/server

http://localhost:3000/blog にアクセスしましょう。うまく表示されましたか?では、New Postボタンを押して、タイトル("Hello Brazil!")を入力してみましょう。

7. コラムの追加

次に、CocoaMySQLに移り、コラムbody(text)を追加します。
ブラウザをリロードすれば、bodyの入力エリアが現れました。bodyにも入力してみましょう("Yes, yes, hello indeed!")。
どうですか?うまく動いていますか?
では、更に、作成日時を記録するようにもしましょう。CocoaMySQLで、コラムcreated_at(datetime)を追加します。
ブラウザをリロードしてみましょう。入力したデータをエディットしてみてください。作成日時が追加されますね。
では、CocoaMySQL上で、コラムcreated_atをマウスでドラッグしてbodyコラムの上に移動してみてください。ブラウザをリロードするとどうなりますか?
これらの一連の作業において、サーバーの再起動も、再コンパイルも必要ありません。
Railsの名前規約に従っていれば、Railsアプリケーションは簡単にデータベースへアクセスすることができるようになります。

~Migration~

なお、デモからは外れますが、現行のRailsバージョンではmigrationが使えるので、上記の代わりにこれを使った方が便利です。CocoaMySQLでblog_developmentデータベースを作成した後、db-migrateフォルダにある001_create_posts.rbを編集します。このファイルはモデルの作成時に同時に作成されます。

class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.column :title, :string
      t.column :body,  :text
      t.column :created_at :datetime
    end
  end

  def self.down
    drop_table :posts
  end
end

雛形が出来上がっているので、create_tableのブロック内を追加するだけです。このときidコラムは自動生成されるためここでの追加は不要です。
次いで、migrationを実行すれば、上記upメソッドが実行されて、postsテーブルが生成されます。

 $ rake db:migrate

次に、タイトル入力に対する検証をするようにします。app-modelsフォルダ内のpost.rbに、以下を追加します。

 validate_presence_of :title

タイトルを入れないで投稿して見てください。エラーメッセージが出ますね?
では、ちゃんと投稿してみましょう(title "Better fill it in, then!", body "Aye, aye, sir")。

8. scaffoldジェネレータ

先のscaffoldメソッドはワンラインで便利なんですけど、コードが変更できない問題があります。ですので、他の方法をやってみましょう。scaffoldジェネレータを使います。

 $ ./scrip/generate scaffold Post Blog

途中、blog_controller.rbを上書きしてよいか聞かれますので、a を押して上書きしてください。
TextMateでblog_controller.rbを見てみると、いろいろなコードが生成されているのが分かります。add-viewsフォルダ内には各アクションに対応したビューも自動生成されています。ブラウザをリロードしてみてください。同じ表示が維持されていると思います。

自動生成された表示はブログに適したものになっていないので、list.rhtmlをブログに適したものに編集してみましょう。

<h1>My wonderful weblog</h1>

<% for post in @posts %>
   <div>
	<h2><%= link_to post.title, :action => 'show', :id => post %></h2>
	<p><%= post.body %></p>
	<p><small>
	   <%= post.created_at.to_s(:long) %>
	   (<%= link_to 'Edit', :action => 'edit', :id => post %>)
	</small></p>
   </div>
<% end %>

<%= link_to 'New post', :action => 'new' %>

ブラウザをリロードしてみてください。体裁が良くなりました。新たにデータも追加しましょう(title "Let's just add a third, for good measure", body "Oh yeah")。
最新投稿が一番上に来るように、表示されるリストの順序を逆にしましょう。先ほどのlist.rhtmlの

<% for post in @posts %>

を、以下に修正してください。

<% for post in @posts.reverse %>

次に、textilizeメソッドを使って、表示をリッチにします。先に、bodyに入力した、"Aye, aye, sir"を以下のように修正します。

 Aye, *aye*, _sir_

そして、list.rhtmlの、

 <p><%= post.body %></p>

を、

 <p><%= textilize(post.body) %></p>

のように修正します。このメソッドにはRedClothモジュールが必要です。

 $ sudo gem install redcloth

ブラウザをリロードして、リストが逆順になったことと、bodyの表示がリッチになった点を確認してください。

9. partialの使用

同様の表示をpartialを使ってやってみましょう。app-views-blogフォルダ内に _post.rhtmlというファイルを作成し、先のlist.rhtmlにおける、以下の部分を移動します。

 <div>
   <h2><%= link_to post.title, :action => 'show', :id => post %></h2>
   <p><%= post.body %></p>
   <p><small>
      <%= post.created_at.to_s(:long) %>
      (<%= link_to 'Edit', :action => 'edit', :id => post %>)
   </small></p>
 </div>

そして、list.rhtmlの内容は以下のようにします。

 <h1>My wonderful weblog</h1>

 <%= render :partial => "post", :collection => @posts.reverse %>

 <%= link_to 'New post', :action => 'new' %>

ブラウザをリロードすれば、表示に変化がないことがわかります。
次いで、showビューでもこの_post.rhtmlのテンプレートを使いましょう。app-views-blogフォルダのshow.rhtmlを以下のようにします。

 <%= render :partial => "post", :object => @post %>

 <%= link_to 'Edit', :action => 'edit', :id => @post %> |
 <%= link_to 'Back', :action => 'list' %>

これでshowビューもきれいになりました。

10. Commentデータの管理

さて、次に、ブログにはコメントが必要ですので、これを作ります。まずは、Commentモデルを作ります。

 $ ./script/generate model Comment

生成されたComment.rb(Commentモデル)に、Postモデルとの関係性を示す以下を追加します。

 belongs_to :post

一方、Post.rb(Postモデル)にもCommentモデルとの関係性を記述します。

 has_many :comments

1つのPostが複数のcommentを持ち得るので、":comments"と複数形になります。
次いで、CocoaMySQLで、commentsテーブル(複数形)を作成し、コラムid(auto_increment, primary key), body(text), post_id(int11)を追加します。

現行バージョンではmigrationを使うことができます。002_create_comments.rbを以下のようにします。

class CreateComments < ActiveRecord::Migration
  def self.up
    create_table :comments do |t|
      t.column :body, :text
      t.column :post_id, :integer
    end
  end

  def self.down
    drop_table :comments
  end
end

そして、migrateします。

 $ rake db:migrate

CocoaMySQLにおいて、commentデータを1件入力してみましょう(body "Yes, I agree with the Hello!" post_id "1")。
さて、次に、show.rhtmlに以下を追加して、コメントがそこに表示されるようにしましょう。

<h2>Comments</h2>

<% for comment in @post.comments %>
   <%= comment.body %>
   <hr />
<% end %>

さらに、コメントをこのページで投稿できるようにしましょう。show.rhtmlにさらに以下の入力フォームのコードを追加します。

<%= form_tag :action => "comment", :id => @post %>
	<%= text_area "comment", "body" %><br>
	<%= submit_tag "Comment!" %>
<%= end_form_tag %>

そして、このフォームに応答するcommentアクションを、blog_controller.rbに定義します。

 def comment
   Post.find(params[:id]).comments.create(params[:comment])
   flash[:notice] = "Added your comment."
   redirect_to :action => "show", :id => params[:id]
 end

ブラウザをリロードして、コメントを投稿してみてください("Me too, me too!")。うまく行きましたか?ページの上部にはコメントが追加されたことを示すフラッシュが表示されます。これで、コメント付きのブログができました。

11. テスト他

さて、これまでにどれだけのコードを書いたのでしょうか。調べてみましょう。

 $ rake stats

たった58行のコード!
次に、ログからrailsの挙動を見ることもできます。

 $ tail -f log/development.log

上のコマンドを実行してから、投稿や画面遷移をしてみてください(ctrl+Cで終了)。
Railsではunitテストやfunctionalテストも簡単にできるようになっています。
まず、CocoaMySQLでblog_testデータベースを作成します。そして、以下を実行します。

 $ rake test:units

test-unitフォルダを見ると、既に2つのunitテストが用意されており、これをパスしたようです。
post_test.rbを編集して、別のテストをしてみましょう。

require File.dirname(__FILE__) + '/../test_helper'

class PostTest < Test::Unit::TestCase
  fixtures :posts

  def setup
    @post = Post.find(1)
  end

  def test_adding_comment
    @post.comments.create :body => "My new comment"
    @post.reload
    assert_equal 1, @post.comments.size
  end
end

再度、テストします。

 $ rake test:units

簡単ですね。
また、Railsにはconsoleという優れたツールがあります。
これを使えばWebインタフェースを介さずにデータの操作ができます。

 $ ./script/console

でconsoleを立ち上げて、

 p = Post.find :first
 p.title = "Hello Denmark"
 p.save

などとします。ブラウザをリロードして変化を確かめてください。

 p.comments.create :body => "Greeting to the cold north!"
 p.comments.create :body => "Greeting to the super, super cold north!"
 p.comments

最初の投稿に対してコメントを追加しました。

 p.destroy

これで投稿が削除されます。