214
Yuichiro MASUI <[email protected]> Railsチュートリアル ToDoを作りながら学ぶ

Railsチュートリアル

Embed Size (px)

DESCRIPTION

2007/08/11 ワイズノット社内勉強会第1回

Citation preview

Yuichiro MASUI <[email protected]>

RailsチュートリアルToDoを作りながら学ぶ

ゴール

ToDoを作る

Railsが分かったような気になる

Railsでアプリを組んでみたくなる

Ruby on Railsって?

Rubyで書かれたフルスタックの

MVCフレームワーク

理念

その1

Conventionover

Configuration

設定より規約

デフォルトの動作が規約として多数盛り込まれてる

JavaのフレームワークはXMLで設定する物が多い

XMLの設定=コード

設定ではコード量は減らない

その2

DRY

Don't Repeat Yourself

同じ事は二度するな

DBのO/Rマッピングを行う時

DBのカラム名をコードに書くのはDRY違反

DB変更したらコードの変更もいる

バグの元

Railsならクラス作るだけ

class User < ActiveRecord::Baseend

テーブル定義はDBから参照する

ただ、リレーションだけは設定する必要がある

class User < ActiveRecord::Base has_many :booksend

これだけでusersテーブルのマッピング終わり

Conventionover

Configuration

設定より規約

ActiveRecordの規約

テーブル名の単数形のクラスでActiveRecord::Baseを継承するとO/Rマッパになる

だからSQLでテーブル定義するだけで設定はいらない

規約に沿わない場合は設定が必要

class User < ActiveRecord::Base set_table_name 'user' set_primary_key 'pkey'end

その3

言語重要

Ruby

歴史は長いがキラーアプリがなかった

DHHがRubyを選んだ理由

Rubyは美しいコードを書くことができる,プログラマをハッピーにする言語だと

感じたのです。

ITProインタービューにて

なぜ美しいと思うか

思考との乖離が少ない

頭で考えた物が作れる

言語の柔軟性が非常に高い

本当のオブジェクト指向、演算子のオーバーライド、オブジェクトへのメソッド追加...

RailsではRubyを拡張してDSLっぽく使っている

標準ライブラリなどにも手を入れまくり

Webに特化させてる故に書きやすい

思いついたのを簡単にコードにできたらプログラムは

楽しい

その4

すぐ動く

まず目の前に動く物があると作業がはかどる

動くまで時間がかかるとテンションが続かない

scaffold

足場

コントローラに1行書くだけでCRUD完成

Create - 作成Read - 表示Update - 更新Delete - 削除

DBにテーブル作って空のモデルクラスつくって

コントローラにscaffoldって書くだけで

DB更新アプリ完成

これが10分Railsムービーの内容

あとは足場を頼りに骨組みを作っていけばいい

scaffold以外に色々なジェネレータも出ている

これらを使えば管理画面とかは

コード書かなくても行ける

生成されたコードは勉強にも役に立つ

その5

豊富なプラグイン

なんでもある

画像アップならfile_columnタグはacts_as_tabbable

などなど・・・

日本語化はGetText-Ruby

以上5つがRailsの生産性の高さを支えている

Railsのスローガン

that's optimized for programmer happiness

and sustainable

プログラマーの幸福と創造性の継続に最適化されたフレームワーク

システム構成

Rails 1.2.3 ← 最新版Ruby 1.8.6 ← 最新版MySQL 5.0.27

PostgreSQLも使えるけどちょっと制約もある

WebサーバはRuby+Cで書かれた

Mongrel

apache+fastcgiとかlighttpd+fastcgiがあるけど、mongrelが

今のスタンダード

Railsのプロジェクトを始める

> rails todo> cd todo

なにもしないでとりあえずRails起動

> ruby script/server↓

http://localhost:3000/

http://localhost:3000/

エクスプローラでC:\InstantRails\rails_apps\todo

を開いてみよう

publicの下がDocumentRoot

appがプログラムdbがデータベースの情報

configが設定scriptが各種スクリプト

Railsプロジェクトでまずやること

DBの設定

config/database.ymlがデータベースの設定

development: adapter: mysql database: todo_development username: root password: hostname: localhost

test: adapter: mysql database: todo_test username: root password: hostname: localhost

production: adapter: mysql database: todo_production username: root password: hostname: localhost

開発環境のDB設定

テスト環境のDB設定

製品環境のDB設定

Railsの3つの環境(実行モード)

開発時のdevelopment  詳細なログやエラーメッセージ自動テスト実行test  UnitTestなどを実行実稼働のproduction  パフォーマンス優先

development: ← Railsの環境 adapter: mysql ← 接続するDB database: todo_development ← DB名 username: root ← ユーザ名 password: ← パスワード hostname: localhost

初期値ではプロジェクト名_development

プロジェクト名_testプロジェクト名_production

の3つのDBが必要

ユーザもrootじゃヤバいので適当なものを作るtodo / todopass

ユーザもrootじゃヤバいので適当なものを作るtodo / todopass

ダメな例

development: ← 3つの環境とも adapter: mysql database: todo_development username: todo ← ユーザ名 password: todopass ← パスワード hostname: localhost encode: utf8 ← 追加

phpMyAdminでテーブルとユーザを作り

権限を与える

todo_developmentとtodo_testデータベースを作る

todoユーザも作りそれぞれにアクセス出来るように

権限付与する

セッションをDBに保存するようにする

PHPと同じようにセッションをDBやファイルに

保存する↓

初期値ではファイルなのでDBに変更する

各種設定はconfig/environment.rb

# 要求するRailsのバージョンRAILS_GEM_VERSION = '1.2.3' unless defined? RAILS_GEM_VERSION

# 起動ファイルrequire File.join(File.dirname(__FILE__), 'boot')

# Railsの初期設定Rails::Initializer.run do |config|

end

config/environment.rb

# 要求するRailsのバージョンRAILS_GEM_VERSION = '1.2.3' unless defined? RAILS_GEM_VERSION

# 起動ファイルrequire File.join(File.dirname(__FILE__), 'boot')

# Railsの初期設定Rails::Initializer.run do |config| config.action_controller.session_store = :active_record_store ← コメントを消すend

config/environment.rb

DBにセッション用のテーブルを作る

rakeコマンド

Rakefileなどに書いてあるレシピを実行させる

> rake -T> rake db:sessions:create

db/migrate/001_add_sessions.rb

class AddSessions < ActiveRecord::Migration def self.up create_table :sessions do |t| t.column :session_id, :string t.column :data, :text t.column :updated_at, :datetime end

add_index :sessions, :session_id add_index :sessions, :updated_at end

def self.down drop_table :sessions endend

db/migrate/001_add_sessions.rb

> rake db:migrate

DBマイグレーション

phpmyadminで確認しよう

↓http://localhost/mysql/

rake db:migrateを実行するとdb/migrate/の中のupメソッドを順番に実行

SQLじゃなくてRubyでテーブル生成

↓DBの構造をバージョン管理

Todoモデルを作る

todoモデルはToDoの内容を書くdescriptionカラムだけ

> ruby script/generate model todo description:text

マイグレーションファイルも自動生成

db/migrate/002_create_todos.rb

> rake db:migrate

Todoモデルクラスapp/models/todo.rb

class Todo < ActiveRecord::Baseend

app/models/todo.rb

AR::Baseを継承してクラスを作るとクラス名の複数形のテーブルをO/Rマッピング

APIリファレンスはhttp://railsapi.masuidrive.jp

> ruby script/console>> t = Todo.new>> t.savephpMyAdminで確認>> t.description = ‘hoge’>> t.savephpMyAdminで確認

phpMyAdminで見ると主キーに勝手にidが割り当てられてるRailsの規約

Todoクラスに定義しなくてもdescriptionカラムを扱える

phpMyAdminで1行適当に追加

>> Todo.find(:all)[#<Todo:0x26efa10 @attributes={"id"=>"1", "description"=>"hoge"}>, #<Todo:0x26ef970 @attributes={"id"=>"2", "description"=>"new todo"}>]

>> t = Todo.find(2)>> t.description“new todo”>> t.description = “test!?”>> t.save

これを操作するコードを書いてToDoアプリを作る

ついにきましたscaffold

モデルに対するCRUDを生成

Create - 生成Read - 表示Update - 更新Destroy - 削除

> ruby script/generate scaffold todo todo

モデル名コントローラ名

TodoモデルをCRUDするTodoコントローラ

app/controllers/todo_controller.rbapp/views/todo/*.rhtml

Railsでコントローラにアクセスするには・・

↓http://サーバ/コントローラ名/アクション名/id

Railsでコントローラにアクセスするには・・

↓http://サーバ/コントローラ名/アクション名/id

省略可能

Railsでコントローラにアクセスするには・・

↓http://サーバ/コントローラ名/アクション名/id

省略可能Railsの規約

ruby script/server

http://localhost:3000/todo/

ToDo完成

と、いう訳にはいかない

まずはscaffoldの流れを追おう

http://localhost:3000/todo↓

Todoコントローラのindexアクションを実行

コントローラのコードを追おう

class TodoController < ApplicationController def index list render :action => 'list' end

# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) verify :method => :post, :only => [ :destroy, :create, :update ], :redirect_to => { :action => :list }

def list @todo_pages, @todos = paginate :todos, :per_page => 10 end

def show @todo = Todo.find(params[:id]) end

def new @todo = Todo.new end

def create @todo = Todo.new(params[:todo]) if @todo.save flash[:notice] = 'Todo was successfully created.' redirect_to :action => 'list' else render :action => 'new' end end

def edit @todo = Todo.find(params[:id]) end

def update @todo = Todo.find(params[:id]) if @todo.update_attributes(params[:todo]) flash[:notice] = 'Todo was successfully updated.' redirect_to :action => 'show', :id => @todo else render :action => 'edit' end end

def destroy Todo.find(params[:id]).destroy redirect_to :action => 'list' endend

class TodoController < ApplicationController def index list render :action => 'list' end

# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) verify :method => :post, :only => [ :destroy, :create, :update ], :redirect_to => { :action => :list }

def list @todo_pages, @todos = paginate :todos, :per_page => 10 end

def show @todo = Todo.find(params[:id]) end

def new @todo = Todo.new end

def create @todo = Todo.new(params[:todo]) if @todo.save flash[:notice] = 'Todo was successfully created.' redirect_to :action => 'list' else render :action => 'new' end end

def edit @todo = Todo.find(params[:id]) end

def update @todo = Todo.find(params[:id]) if @todo.update_attributes(params[:todo]) flash[:notice] = 'Todo was successfully updated.' redirect_to :action => 'show', :id => @todo else render :action => 'edit' end end

def destroy Todo.find(params[:id]).destroy redirect_to :action => 'list' endend

短いと言っても長い

順に追ってみましょう

ブラウザRails

mongrel

Controller

Railsアプリの構造

Model

View

Controller

MVCの流れ

Model

View

Controller

MVCの流れ

Model

View

params[]URLやフォームで渡される値

Controller

MVCの流れ

Model

View

params[]URLやフォームで渡される値

URLからコントーラとアクションを決定

Controller

MVCの流れ

Model

View

Model.find,model.save

params[]URLやフォームで渡される値

URLからコントーラとアクションを決定

Controller

MVCの流れ

Model

View

Model.find,model.save

params[]URLやフォームで渡される値

インスタンス変数

URLからコントーラとアクションを決定

Controller

MVCの流れ

Model

View

Model.find,model.save

params[]URLやフォームで渡される値

インスタンス変数

アクション名のテンプレ

URLからコントーラとアクションを決定

class TodoController < ApplicationController ...end

コントローラを定義するには、コントローラ名+ContollerのクラスをActionController::Baseを継承して作る。ApplicationControllerが、ActionController::Baseを継承している。Todoコントローラは、TodoControllerとなる。

def index list render :action => 'list' end

アクションはその名前のpublicメソッドが呼び出される。その為indexアクションはindexメソッドが呼ばれる。ここでは、listメソッドを呼び、ビューをlistアクションのテンプレートを使って実行している。

def list @todo_pages, @todos = paginate({:todos, :per_page => 10})end

第一引数で指定したモデルを:per_pageの件数だけ取得する。第一返値には、Paginator管理オブジェクト、第二返値に取得したモデルデータの配列が返ってくる。URLのパラメータでpageが指定されていた場合、page*per_page件目からのデータが取得される

Railsの便利機能

def index list render :action => 'list' end

デフォルトでは、Viewでアクション名のテンプレートファイルが呼ばれるが、今回はrender命令を使いテンプレートファイルをlistに指定しているので、listアクションのテンプレートが実行される

ビューはどこ?

app/views/コントローラ名/アクション名.rhtml

app/views/コントローラ名/アクション名.rhtml↓

app/views/todo/list.rhtml

<h1>Listing todos</h1><table> <tr> <% for column in Todo.content_columns %> <th><%= column.human_name %></th> <% end %> </tr>

<% for todo in @todos %> <tr> <% for column in Todo.content_columns %> <td><%=h todo.send(column.name) %></td> <% end %> <td><%= link_to 'Show', :action => 'show', :id => todo %></td> <td><%= link_to 'Edit', :action => 'edit', :id => todo %></td> <td><%= link_to 'Destroy', { :action => 'destroy', :id => todo }, :confirm => 'Are you sure?', :method => :post %></td> </tr><% end %></table>

<%= link_to 'Previous page', { :page => @todo_pages.current.previous } if @todo_pages.current.previous %><%= link_to 'Next page', { :page => @todo_pages.current.next } if @todo_pages.current.next %> <br />

app/views/todo/list.rhtml

ブラウザのコードと見比べてみよう

.rhtmlは、ERBと呼ばれる埋め込みRuby

<%~%>の部分が実行され、<%= ~%>は結果が表示される

<h1>Listing todos</h1><table> <tr> <% for column in Todo.content_columns %> <th><%= column.human_name %></th> <% end %> </tr>

<% for todo in @todos %> <tr> <% for column in Todo.content_columns %> <td><%=h todo.send(column.name) %></td> <% end %> <td><%= link_to 'Show', :action => 'show', :id => todo %></td> <td><%= link_to 'Edit', :action => 'edit', :id => todo %></td> <td><%= link_to 'Destroy', { :action => 'destroy', :id => todo }, :confirm => 'Are you sure?', :method => :post %></td> </tr><% end %></table>

<%= link_to 'Previous page', { :page => @todo_pages.current.previous } if @todo_pages.current.previous %><%= link_to 'Next page', { :page => @todo_pages.current.next } if @todo_pages.current.next %> <br />

app/views/todo/list.rhtml

Todoモデルのカラム一覧を取得

paginateしたデータのループ

前後ページへのリンク

データ表示各ページへのリンク

<h1>Listing todos</h1><table> <tr> <% for column in Todo.content_columns %> <th><%= column.human_name %></th> <% end %> </tr>

<% for todo in @todos %> <tr> <% for column in Todo.content_columns %> <td><%=h todo.send(column.name) %></td> <% end %> <td><%= link_to 'Show', :action => 'show', :id => todo %></td> <td><%= link_to 'Edit', :action => 'edit', :id => todo %></td> <td><%= link_to 'Destroy', { :action => 'destroy', :id => todo }, :confirm => 'Are you sure?', :method => :post %></td> </tr><% end %></table>

<%= link_to 'Previous page', { :page => @todo_pages.current.previous } if @todo_pages.current.previous %><%= link_to 'Next page', { :page => @todo_pages.current.next } if @todo_pages.current.next %> <br />

app/views/todo/list.rhtml

Todoモデルのカラム一覧を取得

paginateしたデータのループ

前後ページへのリンク

データ表示各ページへのリンク

リンクを生成するヘルパー

HTMLをエスケープするヘルパー

Showのリンクは要らないので削ってしまおう。

ついでにメッセージも日本語に

文字コードはUTF-8で改行コードは問わず

<h1>ToDo一覧</h1><table> <tr> <% for column in Todo.content_columns %> <th><%= column.human_name %></th> <% end %> </tr>

<% for todo in @todos %> <tr> <% for column in Todo.content_columns %> <td><%=h todo.send(column.name) %></td> <% end %> <td><%= link_to 'Show', :action => 'show', :id => todo %></td> <td><%= link_to '編集', :action => 'edit', :id => todo %></td> <td><%= link_to '削除', { :action => 'destroy', :id => todo }, :confirm => 'ホントにいいの?', :method => :post %></td> </tr><% end %></table>

<%= link_to ’前のページ', { :page => @todo_pages.current.previous } if @todo_pages.current.previous %><%= link_to '次のページ', { :page => @todo_pages.current.next } if @todo_pages.current.next %> <br />

app/views/todo/list.rhtml

でもソースにあった<head>とかは?

アクションのテンプレの前にapp/views/layouts/コントローラ.rhtml

が呼び出される

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <title>Todo: <%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold' %></head><body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield %>

</body></html>

app/views/layouts/todo.rhtml

ここにアクションのテンプレが展開される

編集画面は?

編集リンクを押すと↓

http://localhost:3000/todo/edit/1Todoコントローラのeditアクション、idは1

def edit @todo = Todo.find(params[:id]) end

URLで指定されたidは、params[:id]に入っているのでこれを主キーにして検索し、その結果を@todoに代入@todoはインスタンス変数なので、そのままビューであるapp/views/todo/edit.rhtmlへ渡る

外部からのパラメータ

<h1>Editing todo</h1>

<% form_tag :action => 'update', :id => @todo do %> <%= render :partial => 'form' %> <%= submit_tag 'Edit' %><% end %>

<%= link_to 'Show', :action => 'show', :id => @todo %> |<%= link_to 'Back', :action => 'list' %>

app/views/todo/edit.rhtml

他のテンプレファイルを読み込む:partialで指定された場合は_名前.rhtmlが読み込まれる

<%= error_messages_for 'todo' %>

<!--[form:todo]--><p><label for="todo_description">Description</label><br/><%= text_area 'todo', 'description' %></p><!--[eoform:todo]-->

app/views/todo/_form.rhtml

@todoのエラーを表示

<textarea>を生成して値に@todo.descriptionをセット

formの飛び先はupdateアクション

def update

@todo = Todo.find(params[:id])

if @todo.update_attributes(params[:todo])

flash[:notice] = 'Todo was successfully updated.'

redirect_to :action => 'show', :id => @todo

else

render :action => 'edit'

end

end

URLのパラメータ

フォームのパラメータをモデルに保存

redirect先で表示するメッセージ

error_messages_forでエラーメッセージ表示

CreateとDestroyは省略

さてこのToDoにはなにが足りない?

なにも書かなくてもToDoが登録できる

入力値チェックを行うvalidate

class Todo < ActiveRecord::Base validates_presence_of :descriptionend

app/models/todo.rb

descriptionを必須項目に

saveするとエラーが起こり保存されなくなる

validatorの種類

validates_acceptance_of「この規約に同意しますか?」などのチェックボックスをチェック。これを指定すると、データベースには保存されない仮想的なカラムが自動生成されます。

例: validates_acceptance_of :kiyakuチェックボックスの値をkiyakuカラムに入れると、チェックされてない場合にはエラーが起こる

validates_confirmation_of確認のために同じ値を入力させた場合の同定をチェック。passwordカラムを指定した場合、データベースには保存されないpassword_confirmationカラムができ、passwordカラムとpassword_confirmationカラムの同一性をチェックする。

例: validates_confirmation_of :emailemailカラムと、email_confimationカラムの値を比較して、不一致の場合にエラーが起こる

validates_inclusion_of指定した値に含まれているかチェックvalidates_exclusion_ofとは逆の挙動

例: validates_inclusion_of :year, :in => 1900..2007yearカラムが1900~2007の間に入っていない場合にはエラーが起こる

例: validates_inclusion_of :sex, :in => ['female', 'male']sexカラムが'famale'でも'male'でもなかった場合には、エラーが起こる

validates_exclusion_of指定した値に含まれて居ないかチェックvalidates_inclusion_ofとは逆の挙動

例: validates_exclusion_of :age, :0..19ageカラムが0~19の間に入っていた場合にエラーが起こる

validates_format_of正規表現で指定した書式でチェック

例: validates_format_of :zip, :with => /^\d{3}-\d{4}$/zipカラムが郵便番号のフォーマットに合っていない場合にエラーが起こる

validates_length_ofvalidates_size_of入力した文字数をチェック。日本語などもバイト数ではなく文字数でチェックされます。

例: validates_length_of :phone, :in => 9..11phoneカラムの桁数が9~11桁にない場合にエラーが起こる

validates_numericality_of入力した文字列が、数字かチェック

例: validates_numericality_of :pricepriceカラムに数字以外の物が入っていた場合にはエラーが起こる

例: validates_numericality_of :num, :only_integer => truenumカラムに整数以外の物が入っていた場合にはエラーが起こる

validates_presence_of入力されているかチェック

例: validates_presence_of :mailmailカラムに何も入力されていない場合にはエラーが起こる

validates_uniqueness_ofデータベース内で重複していないかチェック

例: validates_uniqueness_of :mailmailカラムの値が、既にデータベースに登録されている場合にはエラーが起こる

これらをモデルに加えるだけで入力チェックができる

値チェックはモデルの仕事コントローラに書いていませんか?

エラーメッセージが英語・・・

多言語化ライブラリRuby-GetText

RubyGemsで配布PHPのPEARや

PerlのCPANの様なもの

> gem install gettext 1. gettext 1.10.0 (ruby) 2. gettext 1.10.0 (mswin32)~ 中略 ~ 6. Cancel installation> 1 ← 最新の(win32)の番号を入力

config/environment.rbの最後に下記の一行を追加

↓require 'gettext/rails'

app/controllers/application.rbにも下記の一行を追加

↓init_gettext "todo"

classの次の行

優先順位とかカテゴリとか

優先順位モデルやカテゴリモデルとToDoモデルを繋げる

belongs_tohas_many, has_one

もっとAjaxバリバリに

rjs

続きはこれらの本で

日経Linuxでやってる私の連載もよろしく!

続きはこれらの本で

日経Linuxでやってる私の連載もよろしく!

リクエストがあれば続きもやります

http://blog.masuidrive.jp/