2008-05-25 Ruby on Railsで延々作るTwitterもどき - 1日目
Ruby on Railsで延々作るTwitterもどき
Ruby on Railsで10分で作るTwitterもどきに触発されて、Ruby on RailsでTwitterもどきを作ってみることにしました。
が、実はRailsは初。どう考えても10分じゃ無理なので、ゆっくり作っていくことにします。
作成過程はちょっとずつここで書いていく予定。
一応作ったものはオープンソースということで随時晒していくつもりです。
が、ろくに本も持っていないので、全てWebの情報だけが頼りですが、これで果たしてどこまでできるか…。
とりあえず形から入るべく、名前だけ決めておきます。
独り言をつらつら書くということで、「monologue」。
とりあえず基本設定
作業環境はMac OS X 10.5を使います。rubyはもちろん、なんとrails標準インストール済み(ただしver1.2.6)。
というわけで、さっそく作成開始です。まずは、railsの開発環境を生成。
% rails monologue
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
create config/environments
create components
create db
...
| ディレクトリ | 内容 |
|---|---|
| app/controllers | コントローラ |
| app/helpers | ヘルパークラス |
| app/models | モデル |
| app/views | ビュー |
| config | アプリケーションの設定 |
| db | DB用のファイルが入る |
| db/migrate | migration用ファイル。DBを自動で作ってくれるetc. |
| doc | ドキュメント(READMEとか。多分しばらく書かないが^^;) |
| lib | ライブラリ? |
| log | ログ |
| public | Webサーバの公開フォルダ |
| public/images | 画像ファイル |
| public/javascripts | JavaScript (.jsファイル) |
| public/stylesheets | スタイルシート (.cssファイル) |
| test | ユニットテスト(これも最初は書かない… *1 ) |
| script | スクリプトなど。Webサーバもここに入っている |
| vendor/plugin | プラグインなど |
という感じらしい。
DBとしては最近は sqlite がデフォルトなので、何の設定もいりません。が、MySQLを使う場合は以下の設定を書き換えます。
config/database.yml の
adapter: sqlite3 database: db/production.sqlite3 timeout: 5000
となっている部分(3カ所)を全て、
adapter: mysql username: monologue password: ****** database: monologue socket: /tmp/mysql.sock
と変更。MySQL側でもDBとユーザを作ります。日本語を使いたいので、utf8を文字コードとして指定しておきます。
mysql> create database monologue default character set utf8;
このときユーザ設定(権限の設定)もしておきます(詳細略)。
とりあえずサーバを起動してみます。
% cd monologue % script/server => Booting Mongrel (use 'script/server webrick' to force WEBrick) => Rails application starting on http://0.0.0.0:3000 => Call with -d to detach => Ctrl-C to shutdown server ...
この状態で http://localhost:3000/ にアクセスしてみると、Railsのロゴと Welcome aboard / You’re riding the Rails! というメッセージが。これで、いよいよ中身を作っていきます。
DB設計
まずはDB設計から。といっても簡単に。Railsなら後から変更も楽に違いない(思い込み)。
┌──────────┐ ┌────────┐ │User │n⇔n│Follow │ │──────────│ │────────│ │名前、パスワード、 │ │user_id │ │プロフィール、設定、│ │ followee_id │ │などなど… │ └────────┘ └──────────┘ 1 ↓ n ┌──────────┐ │Status │ │──────────│ │user_id │ │発言内容 │ └──────────┘
なんか崩れてるが心の目で見てください。
User:ユーザ情報を管理。ログイン等もこの情報をもとに行う。
Status: ユーザの発言を管理するテーブル。複数の発言が特定のユーザに属する(1:nの関係)
Follower: 特定のユーザがFollowしているユーザ(Follower)、あるいは逆にFollowされているユーザ(Followee)を関連づけるテーブル。
Railsでは普通全てのテーブルの各エントリにID (integer)をつけて管理するようなので、テーブル間の関連はそのIDで示します。
で、こんだけ書いておいて何ですが、まずはFollowerは抜きの、単なる独り言サイト(ユーザが独り言を書き込むだけのモノ)を作り、後からFollow機能を入れていくという方針にしようと思います。
ユーザ認証用のプラグインの導入とテーブルの生成
普通なら、ここでDBにテーブルを作ってしまうか、テーブル生成用のmigrationを書くのでしょうが、今回はユーザ認証のシステムを作る必要があるため、まずはプラグインを入れておきます。restful_authenticationというのがあるらしいのですが、今回Rails自体のバージョンが古いため、代わりに古い acts_as_authenticated を導入します。
プラグインの導入
といってもコマンドはまた一行だけ。
% script/plugin install acts_as_authenticated
すると、自動的に必要なものをダウンロードして vendor/plugins 以下にプラグインを導入してくれます。
README的なメッセージも現れます。
Userモデルの自動生成
表示された説明に従って、このままユーザを管理するためのモデルやコントローラ、ビューも生成してしまいます。
% script/generate authenticated user account
このscript/generateは、いろんなファイルを自動生成してくれるスクリプトで、RoRが高速開発ができるための肝です。script/generate 単体で実行すると、生成できるものの種類が分かります。なお今回プラグインを入れたため、authenticatedが追加されています。
今回は上記のコマンドで、userという名前のモデル(≒DBにアクセスするクラス)とaccountというコントローラ(≒ロジック部分)が自動的に作られます。
アプリケーション全体への認証システムの適用
ここで、app/controllers/application.rb 内に
class ApplicationController < ActionController::Base include AuthenticatedSystem # 追記 before_filter :login_from_cookie # 追記
の二行を書き加えておくと、導入した認証システムをアプリケーション内のどこでも使えるようになります。
Usersテーブルの作成
しかしまだ肝心のDB上のテーブルが出来ていませんから、作る必要があります。
migrationファイルの確認
RoRではmigrationという仕組みを使ってテーブルを作成することができます。これを使うようにしておくと、後々テーブル構造の変更時にも便利とのこと。具体的には、db/migrate/00n_xxx.rb (00nは連番)というファイルに、DBを作るためのスクリプトを書き、rakeコマンドを実行すればOKです。
実は先ほどUserモデルを作ったときにUser用のmigrationファイルも自動生成されています。db/migrate/001_create_users.rb です。内容は下のような感じ。
class CreateUsers < ActiveRecord::Migration def self.up create_table "users", :force => true do |t| t.column :login, :string t.column :email, :string t.column :crypted_password, :string, :limit => 40 t.column :salt, :string, :limit => 40 t.column :created_at, :datetime t.column :updated_at, :datetime t.column :remember_token, :string t.column :remember_token_expires_at, :datetime end end def self.down drop_table "users" end end
upメソッドがテーブルを作るメソッド、downが消すメソッドですね。
テーブルの名前は、モデルクラスの名前(User)を複数形にした users とするのがRails流らしいです。この対応付けはRails内部が自動で行うため、この規約に従う限り、ユーザが明示的に名前を設定する必要がなくなっています。
カラム(フィールド)の追加
さて、せっかくなので、usersテーブルに自己紹介を記述するカラムを追加してみます。upメソッド内のテーブル作成メソッド create_table 内の適当なところに、真似して
t.column :profile, :text
と書き加えます。ちなみに string はMySQLのVARCHAR(255)に対応する型なので、255byteまでしか入りません(UTF8なので日本語だと80文字ちょっと)。:string :limit => 40ならVARCHAR(40)。一方、:textや:binaryなら任意長です。ちなみに数字は :integer、日付&時刻は :datetime、など。
テーブルの自動生成
こうしておいて、
% rake db:migrate
を実行すると、勝手にテーブルを生成してくれます。rakeコマンドは、ruby版のmakeコマンドです。
Userモデルやコントローラはは全く触ってませんが、RailsがDBのテーブルに合わせて動的にメソッドを作ってくれるので、いまのところ変更の必要はありません。
サーバの起動とユーザ認証のテスト
ここまでで、
% script/server
を実行してサーバを立ち上げ、http://127.0.0.1:3000/account/signup で新規ユーザ作成ができます。 http://127.0.0.1:3000/account/login にアクセスすると、ログイン画面が出ます。
現在、何も作っていないので、ログインすると謎のポエムが出現します。ログアウトしても出ます…。まあ、この辺の見た目(ビュー)は後々変更していきます。
URLの意味
http://127.0.0.1:3000/account/login にアクセスするときには、app/controllers/account_controller.rb 内のロジックが実行されます。メソッド名はlogin。そして、app/views/account/login.rhtml のビューの内容をもとにページをレンダリングしています。
account_controller のなかでは、Userというクラスを使ってユーザ情報にアクセスしていますが、このクラスの定義はapp/models/user.rb にあります。テーブル名やフィールド名については全く定義されていない(ただしvalidation(フィールド内容チェック)などは自動生成されている)のですが、名前を複数形にしたusersテーブルにアクセスし、動的にフィールド名は取得してくれています。
発言管理用のテーブル&モデルを作成
ユーザ管理テーブルではいきなり変則的なモデルの作り方をしてしまいましたが、次は「普通」に発言を管理するStatusテーブルを作ってみます。
まず、次のコマンドを打ち込んでmodelとmigrationのファイルを自動生成します。
% script/generate model status
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/status.rb
create test/unit/status_test.rb
create test/fixtures/statuses.yml
exists db/migrate
create db/migrate/002_create_statuses.rb
そして、生成された db/migrate/002_create_statuses.rb を編集し、Statusesテーブルに必要なカラムを書き加えます。
class CreateStatuses < ActiveRecord::Migration def self.up create_table :statuses do |t| t.column :user_id, :integer #ここから追記 t.column :status, :text t.column :created_at, :datetime t.column :updated_at, :datetime #ここまで end end def self.down drop_table :statuses end end
user_idはユーザ管理テーブルとの関連用、statusが発言内容を格納するカラムです。user_id という名称には「Userクラスのidをキーとして関連する」という意味があります。また、created_at、updated_at というのはRailsでは特殊な意味があり、エントリの作成日時/更新日時を自動的に保存してくれるというものです。
以上を追記した後、
% rake db.migrate
を実行すると、statusesテーブルが生成されます。
次に、User - 1:n -> Status という関連を示すために、モデル部分に追記を行います。
app/models/status.rb に次のように belongs_to メソッドを追記し、userと関連することを示します。
class Status < ActiveRecord::Base belongs_to :user # 追記 end
逆に、app/models/user.rb には次のように has_many メソッドを追記します。
class User < ActiveRecord::Base has_many :statuses, :dependent => :destroy #追記 ...
認証用に自動生成したので既にいろいろ入っていますが、適当な場所に書きます。
なお、belongs_to のあとは単数系、has_many のあとは複数形で書きます。英語的に読むと自然ですが、ちょっとややこしい…。また、 :dependent => :destroy と書いておくと、Userを削除した際に、関連するStatus(es)も削除してくれます(デフォルトでは残ってしまう)。
とりあえず、ここまでで各ユーザが発言した内容(複数)を管理するためのモデルが出来上がりました。
理解すべきことは非常にたくさんありますが、多分まだ10行ちょっとしか書いてません。
でも、まだ発言の追加・削除はもちろん、表示も出来ません*3。というわけで、次はコントローラ、ビューを作っていきます。
ここまでのソース
Userコントローラの作成
Userコントローラの自動生成
ユーザの情報を表示するためのコントローラとして、userコントローラというのを作ることにします(userモデルと名前を一致させておきます)。例によってscript/generateで自動生成します。
% script/generate controller user
生成されたもののうち、app/controllers/user_controller.rb がコントローラの定義です。まだ中身は空っぽ。
class UserController < ApplicationController end
ここに、ユーザの情報(名前とか発言内容とか)を表示を行うためのメソッドを書いていきます。
コントローラのリファレンスはここ(日本語、UTF-8)とかここ(英語)にありますので、必要に応じて参照しながら書いていきます。
indexメソッドの記述
まず、ユーザの情報を表示する index メソッドを作ります。
http://localhost:3000/user/?id=NeoCat
もしくは
http://localhost:3000/user/index/NeoCat
のようにユーザのログイン名が指定された場合にはそのユーザの情報を表示し、
指定なしだとログイン中のユーザを表示する、という感じにしてみます。
URLのコントローラ名の後に何も指定されなかった場合、indexメソッドが呼ばれます。そこで、下記のメソッドをUserControllerに加えます。
class UserController < ApplicationController def index redirect_to(:controller => :account, :action => :login) and return if !logged_in? if params[:id] @user = User.find_by_login(params[:id]) render :text => "not found" and return unless @user else @user = current_user end end end
コントローラでやることはこれだけです。実際の表示するものの指定はビューの中でやりますので、ここではロジック部分のみを書きます。
中身の説明
まず、ユーザがログイン中であるかを確認し、ログインしていなかった場合は account/login にリダイレクトさせます。and returnはここでメソッドの処理を終わらせるためです。
次に、ビューに表示すべきユーザを渡すための処理を行います。@つきの変数は、ビューの内部でもアクセスできるようになります。ここでは@userを使って、ユーザ情報を渡します。
if文に出てくる params は渡された情報を示すハッシュです。params[;id]には http://localhost:3000/user/index/NeoCat のようにアクセスされた場合、NeoCatが入ります。URLの末尾に ?id=NeoCat と指定された場合も同じです。
この指定があれば、それをログイン名に持つユーザを検索します(idっていう名前はちょっと変だが)。User.find_by_loginというメソッドはフィールド名からRailsが自動生成したもので、文字通り引数に指定されたlogin名を持つユーザをusersテーブルから一人検索してくれます。この仕掛けはActiveRecordというもので、O/Rマッピングを提供してくれます。find*についてはid:elm200:20070316:1174044163あたりにいろいろな書き方が載ってます。
見つからなかった場合はnilになりますので、render_to メソッドを使って "not found"という文字をレンダリングし、処理を中止します。この場合ビューは呼び出されません。
idが指定されていなかった場合は、current_userを代入しておきます。current_userにはログイン中のユーザがいればUserクラスのインスタンスが代入されています。
renderもredirect_toも指定せずにメソッドを抜けると、自動的に app/views/user/index.rhtml の内容がレンダリング対象になります(userはコントローラ名、indexはメソッド名)。とりあえずこのファイルに
<%= @user.login %>
とだけ書いて、ログイン名だけを表示するようにしておきます。
accountコントローラの変更
なお、ログイン後にポエムがでるのは、app/controllers/account_controller.rbの中で account/index にリダイレクトしているためです。これを、今作った user/index に飛ばすようにしましょう。
同ファイルの以下の部分を変更します。3カ所くらいあります。
redirect_back_or_default(:controller => '/account', :action => 'index')
↓
redirect_back_or_default(:controller => '/user', :action => 'index')
また念のため、ポエムをレンダリングしている本体であるaccount_controller.rbのindexメソッドを書き換えて
def index if logged_in? redirect_to(:controller => '/user', :action => 'index') else if User.count > 0 redirect_to(:action => 'login') else redirect_to(:action => 'signup') end end end
のように、適当な場所にリダイレクトするようにしておきます。
これでもうポエムの入っている views/account/index.rhtml は不要なので、消してしまいましょう。:-P
動作テストしてみる
これで、ログインしてみたり http://localhost:3000/user/ や http://localhost:3000/user/index/ユーザ名 にアクセスしてみると、上記の動作を試すことが出来ます。
次に、もうちょっとちゃんとビューの中身を作っていきます。
全体レイアウトを作る
先ほど生成されたビューを見てみると、本当にユーザ名しか書かれておらず、<html><head>〜などの基本的な構造すら入っていないのが分かります。しかし、これからビューを増やしていくことを考えると、いちいち全てのビューに同じことを書いていくのは大変です。
そこで、Railsではlayoutという仕組みを用意しています。詳しくはこことかここ 辺りに。
今回は、アプリケーション全体に適用するレイアウトを書いてみます。場所は app/views/layouts/application.rhtml 。
スタイルシートとか、共通で使うJavaScriptもここに書くと良いでしょう。例えば
<html> <head> <title><%= @title %></title> <%= stylesheet_link_tag('monologue') %> </head> <body> <div id="subpage"> <%= link_to image_tag("monologue.png", :align => 'left', :id => 'logo'), "/" %> <br style="clear:both"> <%= @content_for_layout %> </div> </body> </html>
とか。
ちなみに <%= 〜 %> や <% 〜 %> はerbの表記です。〜にはrubyの命令を埋め込めます。前者は結果をHTML内に出力し、後者は出力しません。後者はループやif文などの制御構造を書くためにも使えます。
image_tag はimgタグの生成。public/stylesheets/images/ 内においたファイルへのimgタグを生成します。
stylesheet_link_tag は、public/stylesheets/ においたcssファイルへのlinkタグを生成するメソッドです。
JavaScriptの場合も、javascript_include_tag メソッドで、 public/javascripts/ 内のファイルを呼び出すscriptタグが生成できます。
今回はロゴの画像を配置するとともに、monologue.cssという全体に適用するスタイルシートへのリンクを張っています。
ユーザ情報のビューを作成
app/views/index.rhtml を編集し、次のようにします。ログインした状態で http://localhost:3000/user/ を見ながら、どんどん書いていきます。発言内容のフォームもまだ機能しませんが、書きます。
<div id="left"> <!--表示対象のユーザがログイン中のユーザの場合のみ、発言用のフィールドを表示--> <% if current_user == @user %> <div id="update_status"> <p><% form_for :status, :url => { :controller => :status, :action => :new } do |f| %> <p><label for="status">めっせーじ?<small>(140文字以内)</small></label><br> <%= f.text_area :status, :cols => 70, :rows => 4 %><br> <%= submit_tag "送信" %></p> <% end %></p> </div> <% end %> <!--ここまで--> <b><hr></b> <!--対象ユーザの過去の発言内容を表示--> <% @user.statuses.reverse.each do |s| %> <%= image_tag('person.png', :size => "32x32", :id => "user_icon", :align => "left") %> <strong><%= @user.login %></strong> <%= s.status%> <small>(<%= s.updated_at,to_s %>)</small> <hr style="clear:left"> <% end %> </div> <!--ここまで--> <div id="right"> <!--ユーザ情報の表示--> <%= image_tag('person.png', :size => "64x64", :id => "user_icon") %> <strong><%= h @user.login %></strong><br> <%= link_to(image_tag("mail.png"), "mailto:" + h(@user.email)) %><br> <b>プロフィール:</b> <%= h @user.profile %> <hr> <!--ここまで--> </div>
全体構成
二つのdivから成り立っています。
id="left"の方は左側に表示する大きめの枠で、発言用のボックスや、過去の発言が入る枠。
id="right"は右側の小さな枠で、ユーザのプロフィールなどの情報などを表示する枠。
発言用のフィールド
発言用のフィールドは、自動生成されたログイン用のフォームを物まねして、とりあえず書いておきます。コントローラを書いていないので、今のところ送信してもエラーになるだけです。
form_forの使い方はこの辺を参考に。モデルの新しいインスタンスを作るときに使うと良いようです。
過去の発言
ここでは、対象ユーザの過去の発言を @user.statuses で取得し、その内容を逆順に表示しています。user.statuses なんて定義したっけ?と思われるかもしれませんが、モデル作成時にUserモデルにhas_many: :statuses を書いておいたことで自動生成されています。自動的にStatusのuser_idが @user.id と一致するものを検索してくれます。発言時刻はRailsがupdated_atとかcreated_atに記録してくれているので、それを参照すればOK。よくできてるなあ。
ユーザ情報
@userのプロフィールなどを表示しています。ここが一番シンプル。
見た目はCSSで
あわせて、ユーザアイコンの画像を配置したり、CSSを適宜書いておきます。以上で、見た目的には下のような感じになりました。
ここまでのソース
Statusコントローラを書く
Userのindexビューに発言用のフォームを作ったので、これを受理して発言をユーザに追加登録するコントローラを書きましょう。
まず、コントローラのひな形をgenerateします。
% script/generate controller status
そして、app/controllers/status_controller.rb を以下のように編集します。
class StatusController < ApplicationController def new if logged_in? && request.post? @status = Status.new(params[:status]) if @status if @status.status == '' redirect_to(:controller => :user, :action => 'index') return end begin current_user.statuses << @status flash[:notice] = "更新しました!" rescue flash[:notice] = @status ? @status.errors.full_messages : "エラー:更新に失敗しました。" end end end redirect_to(:controller => :user, :action => 'index') end def delete @status = params[:id] && Status.find(params[:id]) if @status && @status.user == current_user @status.destroy flash[:notice] = "削除しました!" end redirect_to(:controller => :user, :action => 'index') rescue flash[:notice] = "削除できませんでした。" redirect_to(:controller => :user, :action => 'index') end end
ついでに削除も書いておきました。「追加した」「削除した」、あるいは「できなかった」というメッセージは、先ほど追加したflashを使ってユーザにフィードバックします。
追加処理
if logged_in? && request.post?
POSTで投稿されたときのみ受け付ける。
@status = Status.new(params[:status])
フォームから発言内容を受け取って新規にStatusオブジェクトを作る。form_forを使った場合は、このようにいきなりオブジェクトとして取得できます。
current_user.statuses << @status
flash[:notice] = "更新しました!"
ちゃんと中身がある場合は、現在のユーザのステータス配列に新しい発言を追加。また、flash[:notice]でユーザに追加した旨のメッセージを返します(後述)。
削除処理
@status = params[:id] && Status.find(params[:id])
idで渡された削除対象を取得。
if @status && @status.user == current_user
@status.destroy
flash[:notice] = "削除しました!"
発言がちゃんと存在し、発言した人がログイン中のユーザの場合のみ、該当する発言を削除。追加時と同様にflash[:notice]でユーザに追加した旨のメッセージを返します(後述)。
テスト
何かをフォームに書いて送信してみると、一覧表示に追加されるハズ。
statusesをreverseしてから表示いるので、新しいものほど上にきます。
削除は、app/views/user/index.rhtml に
<% if current_user == @user %> <%= link_to "[x]", {:controller => :status, :action => :delete, :id => s.id}, :confirm => '本当に削除しますか?' %> <% end %>
flashを表示できるようにする
AdobeなFlashではなくて、ページをリダイレクトしたときにメッセージを伝達する仕組みがflash。
この辺が詳しい。
実は既に flash[:notice] = "You're logged in" とかいった設定がaccountコントローラ内で自動生成されています。
これを表示するようにしてみます。
どうせなら、全ページにLayoutを使って埋め込んでしまいましょう。ついでに、しばらく放っておくと消えるようにしておけば、全ページに埋め込んでもデザイン上邪魔にならないでしょう。
app/views/layouts/applications.rhtml に、以下を書き加えます。
<% if flash[:notice] %> <div id="notice"><%= flash[:notice] %><% flash[:notice] = nil %></div> <script> var d_notice = document.getElementById('notice'); function setNoticeAlpha(a){ if (a>0) { d_notice.style.opacity=a; setTimeout(function(){setNoticeAlpha(a-0.1)},100); } else d_notice.style.display = "none"; } setTimeout(function(){setNoticeAlpha(1.0)}, 2000)</script> <% end %>
スタイルシートに以下を加え、適当な右上辺りの場所に固定しておきます。
#notice { position: absolute; right: 20px; top: 18px; width: 340px; margin: 0 auto; border: solid 2px #222; background-color: #fce; text-align: center; font-size: 14pt; line-height: 20pt; z-index: 999; }
デフォルトコントローラの設定
URLにいちいち user を書くのが面倒だなあという気がしてきたし、http://localhost:3000/ でアクセスできるようにしたいところです。
この設定は、config/routes.rb で行います。
もとからある「map.connect '' 〜」のコメントを外し、以下のようにデフォルトのコントローラをuserに設定します。
map.connect '', :controller => "user"
また、public/ の中に index.html がおいてあると、こちらが優先されてしまうようなので、削除してしまいます。
これでサーバを再起動し、http://localhost:3000/ にアクセスするとユーザ情報にいきなりアクセスできます。
ログインしていない場合は、ログインページにリダイレクトされます。
ここまでのソース
*1:Test Drivenで書くのが良いのは分かってるのですが、そもそもRoRそのものの理解が先なので…。
*2:URLとの関連付けは、config/routes.rbで設定されています。
*3:ActiveScaffold(もっといろいろ自動生成)を使っていれば、この辺りで一連の機能はできてしまっているのかも…。






