Railsめも
インストール(Windows)
(1) Rubyのインストーラを入手
http://rubyinstaller.rubyforge.org
実行 → 「すべてのコンポーネント」を選択
(2) Railsのインストール
gem install rails --include-dependencies
※ MySQLもインストールしておく
http://dev.mysql.com
アップデート
gem update rails
インストール(Linux)
(1) yum install ruby rubygems
(2) gem install rails --include-dependencies
Q. 次のようなエラーが出たときは・・・
Bulk updating Gem source index for: http://gems.rubyforge.org
ERROR: While executing gem ... (Gem::GemNotFoundException)
Could not find rails (> 0) in any repository
ERROR: While executing gem ... (Gem::RemoteFetcher::FetchError)
SocketError reading http://gems.rubyforge.org/gems/activerecord-1.15.3.gem
A. 同じコマンドで何回か試すとうまくいくかも
※ なので rails → Rails と直したらOKになったのは たまたま?
※ MySQL
yum install mysql mysql-server
用語
ルータ:リクエストを適切なアクションに結びつけるコンポーネント
アクション:コントローラ内の1つのメソッド
コントローラ:なにをすべきかをモデルに伝える
モデル:どのように実行すべきかを知っている
ORM
ORMについて
・クラス:テーブル
・オブジェクト:行
・属性(プロパティ):列
・クラスメソッド:テーブル単位の操作
・インスタンスメソッド:行単位の操作
という風にオブジェクトとRDBをマッピングする手法。
RailsでのORM
ActiveRecord クラス
一般的なORMの問題点
マッピングを大量のXMLで設定しないといけないこと
→ ActiveRecordはそうでもないらしい
作成
scaffoldユーティリティで作成可能
※例:products テーブルの adminコントローラを作るとき
※ Admin って名前は他の名前でもよい
ruby script/generate scaffold Product Admin
設定ファイル
config/database.yml
参照(結果が1行の場合)
order = Order.find(1);
puts "Order #{order.customer_id}, amount=#{order.amount}"
参照(結果が複数行の場合)
Order.find(:all, :conditions => " name='dave' " ) do |order|
puts order.amount
end
※ TODO: 整理 → Order.find(params[:id]) という書き方も
※ :all の代わりに :first というのもある
更新
Order.find(:all, :conditions => " name='dave' " ) do |order|
order.discount = 0.5
order.save
end
外部キー(FK)の設定
→ 「1対多」の「多」に当たる方を belongs_to メソッドを使用して定義
※ 「1対多」の「1」 に当たる方は has_many メソッドを使用
class LineItem < ActiveRecord::Base
# 外部キーの設定
belongs_to :product
# ↓ これは必須ではない ※「作っとくと良いよ」っていうオススメのメソッドらしい。
def self.for_product(product)
item = self.new
item.quantitiy = 1
item.product = product # belongs_to で指定した分はこう使える
item.unit_price = product.price
item
end
end
※ DDL書いても generate コマンドで生成できたりはしないので注意
where 句を動的に云々
Order.find(:all, [:conditions => " name = :name and pay_type = :pay_type" ,
{:name => 'xxx' , :pay_type => 'xxx' }])
※ 'xxx' の部分を変数に出来る
※ {} ごとハッシュにして渡してもいい
find_by_sql
例
customer = Customer.find_by_sql('select * from xxxx' )
なにが入ってるか見たい
p customer[0].attributes
p customer[0].attribute_names
count, count_by_sql
→ 戻り値はオブジェクトじゃなくて数字
find_by_xxx
find_by_col1('hello' )
find_by_col1_and_col2('hello' , 'world' )
→ よくやるなあ
Model
Q. モデル層にDBと関係ないクラスを作成したいのだけども、
テーブルが無いと困ったことになったりする?
A. Activerecord::Base を継承しなければOK!
→ 普通にクラスを作ればいい
class Cart
....
end
ActionPack
ビューとコントローラがActionPackというコンポーネントで ひとまとめにしてある。
動的コンテンツはテンプレートによって生成
方法1: ERb (Embedded Ruby)で、HTML内にRubyコードの断片を埋め込む。拡張子は.rhtml。 ← JSPみたいな
方法2: ビルダスタイルビュー。Rubyコードを使用してXMLを生成 ← TODO:イメージ沸かないので後で読み返す
※ ERb は現在の呼び方は eRuby。
rails コマンド
# プロジェクトを作成 (demoという名前として)
rails demo
# テスト用サーバ起動
cd demo
ruby script/server
→ http://localhost:3000/
※ 止めるときは Ctrl+C でOK
# コントローラを生成 (Sayという名前として)
ruby script/generate controller Say
→ app/controllers/say_controller.rb ほかが生成される
# ※ アクションも設定する場合
ruby script/generate controller Say index
ruby script/generate controller Say action1
→ index はデフォルトのアクション
# アクションの定義
app/controllers/say_controller.rb にメソッドを追加
# ビューの作成
app/views/say/hello.rhtml
# テーブル作成後・・・ (TODO: 適切なコメントを・・・)
ruby script/generate scaffold Product Admin
↑ ↑
| コントローラの名前
モデル名(テーブル名(products)の単数形を指定すること)
→ データのCRUD操作が出来るページが生成される
※ ここでgenerateコマンドは 開発用DB(xxx_development) のテーブル定義を見に行ってる
※ scaffold は、建築現場などの「足場」を意味するそうだ。
# Validation(フォームの入力チェック)のための記述は、モデルのクラスに書き加える
こんな感じ↓
class Product < ActiveRecord::Base
# Railsが用意しているメソッドで そのまま使える条件
validates_presence_of(:title, :description, :image_url) # 空でないことチェック
validates_numericality_of(:price) # 数値として妥当かチェック
validates_uniqueness_off(:title) # ユニーク制約
validates_format_of( # 正規表現で指定
:image_url,
:with => %r{^http:.+\.(gif|jpg|png)$}i,
:message => "はGIF、JPG、PNG画像のURLでなければなりません"
)
# 自由に条件を書き足す場合
protected
def validate
# 正の数であることチェック
errors.add(:price, "は 0 より大きくなければなりません" ) unless price.nil? || price > 0.0
end
end
ディレクトリ構成について
D:.
└─demo
│ Rakefile
│ README
│
├─app
│ ├─controllers # コントローラはここに配置
│ │ application.rb # - アプリケーション共通のフィルタ処理とか
| | xxxx.rb # - コントローラ毎に1ファイル。アクションはファイル内の1メソッド
│ │
│ ├─helpers # ヘルパー:コントローラで共通で使うメソッドを定義
│ │ application_helper.rb # - アプリケーション全体で使うものはここに
| | xxxx_helper.rb # - コントローラ毎にも定義できる
│ │
│ ├─models # モデル(含ORMのオブジェクト)はここに配置
| | # Validateの処理もここに書く
│ └─views # ビュー(rhtml)はここに配置
| | # 1コントローラに1ディレクトリ、
| | # 1アクション1ファイルに対応
│ └─layout # レイアウトファイル(各画面共通のファイル)
├─config # 設定情報
│ │ database.yml # - データベース接続設定
│ │ routes.rb
│ │ boot.rb
│ │ environment.rb # - 文字コード設定とか
│ │
│ └─environments
│ production.rb
│ development.rb
│ test.rb
│
├─components
├─db
├─doc
│ README_FOR_APP
│
├─lib
│ └─tasks
├─log
│ server.log
│ production.log
│ development.log
│ test.log
│
├─public # 公開ディレクトリ
│ │ .htaccess
│ │ dispatch.rb # ディスパッチャ
│ │ dispatch.cgi #
│ │ dispatch.fcgi #
│ │ 404.html
│ │ 500.html
│ │ index.html
│ │ favicon.ico
│ │ robots.txt
│ │
│ ├─images
│ │ rails.png
│ │
│ ├─javascripts
│ │ prototype.js
│ │ effects.js
│ │ dragdrop.js
│ │ controls.js
│ │ application.js
│ │
│ └─stylesheets # スタイルシート
├─script # 開発過程で使うユーティリティ
│ │ about
│ │ breakpointer
│ │ console
│ │ destroy
│ │ generate
│ │ runner
│ │ server # - WEBrickサーバ(テスト用のWebサーバ)
│ │ plugin
│ │
│ ├─performance
│ │ benchmarker
│ │ profiler
│ │
│ └─process
│ reaper
│ spawner
│ inspector
│
├─test
│ │ test_helper.rb
│ │
│ ├─fixtures
│ ├─functional
│ ├─integration
│ ├─mocks
│ │ ├─development
│ │ └─test
│ └─unit
├─vendor
│ └─plugins
└─tmp
├─sessions
├─sockets
├─cache
└─pids
URLについて
http://xxx.xxx.xxx.com/say/hello
↑ ↑
| アクション
コントローラ
ERb (rhtml) について ※ view、ビュー
埋め込むもの
<% スクリプト %> ← 画面表示したくないとき(ifとかwhileのブロックの開始行とか)
<%= スクリプト %> ← こうすると戻り値が表示される
<% スクリプト -%> ← こうすると行末の改行が出力されない(HTMLを見たときに変なところで改行が入らないようになってうれしい)
HTMLの特殊文字のエスケープ
<%= h("xxxx" ) %>
※ h("(-_-) --> (._.)" ) とか書いたときに > が > に変換される
変数
コントローラのインスタンス変数(@xxx)にアクセスできる。
※ PHPでいうところの smarty->assign($xxx); が不要である
↑ ステキ機能だと思う
リンク
<%= link_to("表示名" ,
:action => "アクション名" ,
:confirm => "確認メッセージ" , # 3つ目以降の引数はoptional
:id => "モデル名" , # モデル名.id がフォームパラメータとして渡される
:class => "CSSクラス名" ) %>
※ 他に指定できるもの
:id => 'モデル名?'
:controller => 'コントローラ名' というのもある?
一覧表示について
次のようにして全部の行、全部のカラムについて処理している
(scaffoldで吐き出されるコード)
<% for product in @products %>
<% for column in Product.content_columns %>
ページネーション
@product_pages.current.previous : 前のページ
@product_pages.current.next : 次のページ
レイアウトファイル
app/view/layout ディレクトリにファイルを配置することで、共通のHTMLを呼び出せる。
・コントローラと同じ名前で作成すると、そのコントローラ内の全てのアクションで共通
・
<html>
<head>
<title> @page_title || "デフォルトのページ名" </title>
....
</head>
<body>
:
(メニュー(ホーム, news, お問い合わせ)とか)
:
<%= @content_for_layout %>
</body>
</html>
※ レイアウトファイル内で参照できる変数
@content_for_layout : 各ページ固有の内容
@page_title : ページのタイトル
※ @page_titleは どこで設定してる?
→ コントローラでもいいけど @content_for_layout に当たるファイル内で記述できる
<% @page_title = "このページのタイトル" -%>
共通部分を切り出す
<%= render(:partial => 'form' ) %>
のように書くと、
・同ディレクトリにある _form.rhtml (頭に _ が要るよ) が include される
<%= render(:partial => 'order_line' , :collection => @pending_orders) %>
のように書くとさらに、
・@pending_orders の要素数分だけ繰り返し(TODO:合ってる?)
・@pending_orders の1要素には、 _order_line.rhtml 内から
order_line というローカル変数としてアクセス可能
他のアクションを呼び出して表示する
<%= render_component(:action => "display_cart" , :params => { :context => :checkout }) %>
※ レイアウトファイルを通さずに直接表示したい場合(ヘッダとかフッタが不要な場合)は
呼び先のアクション(display_cart)で、次の行を書く
render(:layout => false)
フォーム関連のヘルパー
<%= error_messages_for("table_name" ) %> # 入力エラーメッセージ表示用
<%= start_form_tag(:action => 'create' ) %> # <form>
<%= text_field("table_name" , "column_nmame" , :size => 40) %> # <input type="text" ...>
<%= textarea("table_name" , "column_name" , :size => 40) %> # <textarea> ..
<%= datetime_select('table_name' , 'column_name' ) %> # <select .. 日付と時刻
<%= select("table_name" , "column_name" , [["1" ,"あ" ],["2" ,"い" ]]) %> # <select .. 普通の
<%= check_box("to_be_shipped" , order_line.id, {}, "yes" , "no" ) %> # <input type="checkbox" ...>
# 3つ目の引数( {} ) はどういうときに使われるのだろう
<%= submit_tag("Create" ) %> # <input type="submit" ...>
<%= end_form_tag %> # </form>
※コントローラで受け取り時のデータは、例えばこんな感じ
@params = { "to_be_shipped" => { "1" => "yes" } }
なので、全部のチェックボックスに関して処理をするなら
to_ship = params[:to_be_shipped]
if to_ship
to_ship.each do |order_id, do_it|
if do_it == "yes"
# 何か処理
end
end
end
データベース関連
テーブル作成時
データベースを3つ作る
・dbname_development : 開発用
・dbname_text : テスト用
・dbname_production : 本番用
TODO: 切り替え方の話について追記
接続情報の設定
config/database.yml
※ YAML形式のファイル
※ 上記の3種類分のための設定項目が、あらかじめ書かれている
テーブル名の規約
テーブル名
アンダーバー(_)区切りで、複数形の名前をつける
→ generate コマンドで生成するクラスは先頭大文字、単数形のクラス名にする
例: (テーブル名) (モデルのクラス名)
products Product
line_items LineItem
主キー
id
外部キー(これは規約?)
constraint fk_items_product foreign key (product_id) references products(id),
セッション
設定
session[:キー名] = xxxx;
(1) コントローラにメソッドを追加
private
def find_cart
session[:cart] ||= Cart.new # 無かったらnewする
end
※ 保存されるファイル
/tmp/ruby_sess*****
エラー処理
コントローラ側
アクションのメソッド内に記述
def action1
xxx = xxx
...
redirect_to(:action => 'xxxx' )
rescue # begin は省略可能ということ?
logger.error("ログに書くメッセージ" )
flash[:notice] = 'エラー画面に出すメッセージ'
redirect_to(:action => 'index' )
end
テンプレート側
<% if @flash[:notice] -%>
<div><%= @flash[:notice] %></div>
<% end -%>
→ レイアウトファイルに記述すると各画面共通にできて良い
※さらに拾いきれてないエラーは、ApplicationControllerで処理するといい
class ApplicationController < ActionController::Base
...
def rescue_action_in_public(exception)
... # という書き方で良いのかしら ※ TODO:
end
...
end
ログ機能
書き方
logger.info("これは通常" )
logger.warn("これは警告" )
logger.error("これはエラー" )
logger.fatal("これは致命的" )
書き出される場所
log/development.log
どのレベルまで出すか設定
TODO:
デバッグ用機能
ruby script/console
irb が開き、コマンドプロンプトのようにRubyスクリプトが書ける
ruby script/breakpointer
あらかじめソースに埋め込んでおいた breakpoint() メソッドの位置で(ブラウザ操作で)進むと、、
irb が開き、breakpoint() 部分の状態で、オブジェクトが参照/操作できる
irb を終了すると(quitでいいのかな)、ブラウザ操作に戻る
※ 他のサーバと通信する場合は breakpointer -s 192.168.x.xxx
generate コマンドいろいろ
# コントローラのみ生成
ruby script/generate controller Say
# (コントローラと)アクションを生成
ruby script/generate controller Store index
↑ ↑
| アクションメソッド名(省略可)
コントローラ名
# モデルを生成
ruby script/generate model LineItem
※ TODO:確認 → 無いテーブルを指定すると db/migrate というところにテーブル作成用のスクリプトらしきものが出来る
# CRUDの画面を生成
ruby script/generate scaffold Product Admin
↑ ↑
| コントローラの名前
モデル名(テーブル名(products)の単数形を指定すること)
オプション引数
-f : 強制的に上書き
-s : 存在するファイルはスキップする
-q : 表示を抑える
ジェネレータ
script/generate xxxxxx の、xxxxx部分
scaffold : 足場作成
model : モデルのみ
controller : コントローラのみ
integration_test
mailer
migration
observer
plugin
resource
scaffold_resource
session_migration
web_service
日本語設定
(1) my.iniの設定
[mysql]
default-character-set=utf8
[mysqld]
default-character-set=utf8
init-connect=SET NAMES utf8
skip-character-set-client-handshake
(2) config/environment.rb の先頭に KCODE の設定をする
$KCODE = "UTF8"
(3) config/database.yml に encoding パラメータを設定する
development:
adapter: mysql
database: xxxx
:
encoding: utf8 # ←ここ
(4) app/controllers/application.rb (ApplicationControllerクラス) に
文字コードセット設定の記述を追加。
※ ここに書くと全てのコントローラの一番最初に呼ばれる。
(こういうのを「フィルタによる処理」という)
class ApplicationController < ActionController::Base
before_filter :set_charset
private
def set_charset
headers["Content-Type" ] = "text/html; charset=UTF-8"
end
end
(5) データベース作成時にデフォルトのキャラクタセットにUTF-8を設定
create database xxx_development default character set utf8;
ActiveRecord
フック:
例えば・・
before_createフック:
ActiveRecord::Base を継承しているクラスで
before_create()メソッド をオーバーライドすると
before_create()メソッド内に書いた処理が create文を実行する前に実行される
class User < ActiveRecord::Base
:
def before_create
self.name = self.name << "様"
end
end
TODO:他のフックは・・
スキーマについての規約
・テーブル名とクラス名
- テーブル名は複数形
- 単語の区切りは _ (アンダーバー)
- 対応するクラス名は単語の先頭を大文字にして _ を取り除いたもの
※ 例:テーブル favorite_songs に対して、クラス FavoriteSong
・キーのカラム名
- 主キーのカラム名は「id」、int auto_increment
- 外部キーのカラム名は「テーブル名の単数_id」
※例: vip_customers テーブルに対して vip_customer_id
・日付関連のカラム名
- DATE型のカラムには名前を 「受動態_on」にする
- TIMESTAMP型のカラムには名前を「受動態_at」にする
※例: sent_at, fired_on
- 更新日時、作成日時は「updated_at」、「created_at」(日までなら「〜_on」)
・関連テーブル(「多対多」のときに間にくるテーブル)は
- 関連させたいテーブル名をくっつけた名前にする
- カラム「id」を作らずに、関連させる2つのキーのセットを主キーにする
※例:
create table products_categories (
product_id int,
category_id int,
primary key(product_id, category_id)
);
・lock_version というint default 0 のカラムがあると、
参照してから更新するまでの間に その行が他の人によって更新されていると、
exceptionが吐かれる (こういうのを「楽観的ロック」というらしい)
Q. 規約に沿ってないテーブル名も使いたい
A.
(1) 別のテーブル名が使いたい場合はset_table_name ディレクティブを使う
class Customer < ActiveRecord::Base
set_table_name "T-00859_03"
end
(2) 単数複数で分けたくない場合は environment.rb に設定する
config.active_record.pluralize_table_names = false
(3) 主キーを id という名前以外にしたい場合
set_primary_key "isbn"
(4) belongs_to
belongs_to :xxxxx,
:class_name => "Order"
:foreign_key => "order_id"
:conditions => "paid_on is not null"
has_and_belongs_to_many :recommend_products,
:join_table => 'recommend_lists'
Q. 型とかデフォルト値知りたい
A. column_hash を使う
p Customer.column_hash['birthday' ].type
p Customer.column_hash['birthday' ].default
Q. 型キャストされる前のデータが取り出したい
A. 属性名 xxxx のとき xxxx_before_type_cast で取り出せる
boolean のデータを評価するときは カラム名の後ろに ? をつけたアクセサを使うこと
user.superuser : nil, false 以外 (0 とか f とか) が true と見なされてしまう
user.superuser? : 0, f, false '' , nil, false が false と見なされる
値を更新した後に DBに反映させるには save() メソッドを使用する
INSERTの仕方の例
(1) new メソッドの場合
order = Order.new(:name => 'xxx' , :price => 'xxx' )
order.save
(2) create メソッドの場合
Order.create(:name => 'xxx' , :price => 'xxx' )
※ create だと saveしなくても反映される
※ 配列渡すとまとめて insert出来たりする
UPDATEの仕方の例
(1) 1件だけ
order.name = 'xxx'
order.save()
(2) 1件だけ(2)
order.update_attribute(:name, "xxx" )
(3) まとめて
DELETEの仕方の例
(1) delete
User.delete(id)
delete_all は まとめて消す
(2) destroy
destroy
destroy_all
→「コールバック」の処理を通したい場合は destroy じゃないとダメらしい TODO:?
失敗した場合に exception 吐かせたい場合は save ではなく save! を使う
フィルタ
# コントローラ内の全部のアクションで、実行前に呼ぶ
class AdminController < ApplicationController
before_filter :authorize
:
end
# 特定のアクション以外全部、実行前に呼ぶ
class LoginController < ApplicationController
before_filter :authorize, :except => :login
:
end
Active Support
全てのRailsコンポーネントによって共有されてるライブラリのセット。
※ なので、Rubyでは使えないが Railsでは使える
数値を拡張して追加されたメソッド
even? ※ if (num.even?) print "偶数" みたいにかけるのだろうか
odd?
1.hours # => 72000
1.hour.from_now # => 1時間前の時刻
時刻
now = Time.now
now.last_month # => 先月
文字列
"cat" .pluralize # => cats (複数に)
"cats" .singularize # => cat (単数に)
未整理メモ
組み込みのヘルパーメソッド
link_to
redirect_to
number_to_currency
stylesheet_link_tag "scaffold" , "depot" , :media => "all"
「:action => "goodbye" 」という記述について
→ 「{:action => "goodbye" }」の省略形、つまりハッシュ
→ :(コロン)が要るのはなんで?
→ Rubyの「シンボル」という文法らしい
Q.
app/models/product.rb:6: syntax error, unexpected kEND, expecting $end
みたいなエラーが出るとき
A.
ファイルをSJISで書いてしまってないか、UTF-8(BOMつき)で保存していないか確認。
※ UTF-8(BOMなし)でOK
※ 単に文法間違いの可能性も
truncate(str, 80) → 80文字で折り返し
コントローラからアクションを指定して画面遷移
redirect_to(:action => 'display_cart' )
flash
@flash : 次のリクエストの処理が終わるまで保持される
カラム名の規約
xxxx_at : datetimeフィールド ※ sent_at とか
xxxx_on : dateフィールド ※ last_edited_on とか
※
updated_at, updated_on
created_at, created_on
テストサーバ起動したときに、test, production の環境でも実行したい
ruby script/server -e test
ruby script/server -e production
コントローラにサブディレクトリを掘った場合、ディレクトリ名は「モジュール名」として定義
つまり、controllers/admin/register_controller.rb というファイルの場合
class Admin::RegisterController < ApplicationController
...
end
となる
Q. 標準の日付形式を変える。
(scaffoldで作ったページで日付があると横に長くて見づらい・・・)
A. config/environment.rb に次の記述を追加
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!({
:default => "%Y/%m/%d %H:%M" ,
})
※ scaffold 実行時はエラーになるのでコメントアウトしとこう
Q. 一覧に罫線がつけたい(ついでにマージンとかフォントとか色の設定も)
A. public/stylesheets/scaffold.css (の一番うしろ) に次を追加
body, p, ol, ul, td {
font-family: 'MS ゴシック' ;
}
table, th, td {
border-color: silver;
border-width: 1px;
border-style: solid;
padding: 5px;
margin: 0px;
vertical-align: top;
border-collapse: collapse;
}
tr:hover {
background-color: ghostwhite;
}
th {
background-color: lavender;
}
p {
padding: 5px;
margin: 0px;
}
a {
padding: 3px;
}
Q. ページネーションで1ページに表示する件数を変更
A. コントローラ(app/controllers/xxx_controller.rb)に記述する
→ :per_page に指定する数字を変更すればいい
def list
@user_pages, @users = paginate :users, :per_page => 10
end
ActiveRecordいろいろ
親 子
1対1 : has_one belongs_to
1対多 : has_many belongs_to
class Category < ActiveRecord::Base
has_many :products
:
end
:
category.products.each do |product|
p product.name
end
has_and_belongs_to_many のことを略して habtm と呼んだりするらしい
has_and_belongs_to_many :services, :join_table => 'price_list'
<% action_card.character_classes.each do |character_class| %>
<%=h character_class.name %>
<% end %>
同じテーブル同士で多対多のとき
継承するといい
ActiveRecordを単体で使う("ActionRecord" は覚え間違い
#!/usr/bin/ruby -w
require "rubygems"
require "active_record"
ActiveRecord::Base.establish_connection(
:adapter => "mysql" ,
:host => "localhost" ,
:username => "xxxxxxxx" ,
:password => "xxxxxxxx" ,
:database => "xxxxxxxx" ,
:socket => "/var/lib/mysql/mysql.sock" ,
:encoding => "utf8"
)
class User < ActiveRecord::Base
end
User.find(:all).each { |user|
p user.name
}
Q. ActiveRecordを単体で使おうとして次のように書いた
require "rubygems"
require_gem "activerecord"
実行時に次のwarningが出た
「./test.rb:4:Warning: require_gem is obsolete. Use gem instead.」
A.
require_gem "activerecord" の代わりに
require "active_record" と書けばいい (active と record の間の _ が必要)
Q. scaffoldでカンタンなCRUDのページは出来るのは分かったけど、リレーション組んであるページにも使いたい!
A. 例を書いてみた
+---------------+
| products | +---------------+
+---------------+ | categories |
| id | +---------------+
| category_id | (∞ --------------- 1) | id |
| name | | name |
+---------------+ +---------------+
のようになっていたとして
1. DDL
create table products (id int not null auto_increment, category_id int not null, name varchar(100) not null, primary key(id))engine=InnoDB;
create table categories (id int not null auto_increment, name varchar(100) not null, primary key(id))engine=InnoDB;
2. 足場作成
ruby script/generate scaffold Product AdminProduct
ruby script/generate scaffold Category AdminCategory
3. URL
http://localhost:3000/admin_product
http://localhost:3000/admin_category
4. 関連を定義
モデル側
(1) app/models/product.rb に 依存関係を記述する
belongs_to :category
→ View側の(2)のように書くために必要
View側
(1) app/views/admin_product/_form.rhtml を編集 : 入力画面と編集画面で使用される
<p>
<%=
# これは selectの要素に
@categories = Category.find(:all).map{|u| [u.name, u.id] }
# <select> ... </select> を生成
# select 'テーブル名(の単数)' , 'カラム名' , @一覧の配列
select 'product' , 'category_id' , @categories
%>
</p>
(2) app/views/admin_product/list.rhtml を編集 : 一覧表示
<td><%=h product.category.name %></td>
(3) app/views/admin_product/show.html を編集 : 詳細表示
※ 一覧表示で情報足りてるし、なくてもいいや
コントローラ側
特に何もしない
5. 完了! 4行足すだけ!
※ でもこれも単調作業だから、「モデル」か設定ファイルに依存関係を定義するだけで、残りはgenerate コマンドで一緒に生成されてくれたらうれしいなあと思ったり、知らないだけで、そういう方法もあるのかもしれないと思ったり。
Q. 上の例で、関連してるテーブルを参照する方法は分かった。
こんどは「多対多」の関係のテーブルを扱うときに、相手のテーブルが参照したい。
A. 例を書いてみた
+------------+ +--------------+
| customers | | orders | +-------------+
+------------+ +--------------+ | products |
| id | (1 ---- ∞) | customer_id | +-------------+
| name | | product_id | (∞ ---- 1) | id |
+------------+ +--------------+ | name |
| category_id |
+-------------+
注文(orders)テーブルに、誰が(customers)、何の商品を(products) 買うかを記録するという設定。
先の例だと、ordersテーブルのデータを登録するのにプルダウンが使えるようになるが、
customers の一覧で、その人が どの商品を注文しているのか表示したい場合の話。
1. DDL
create table products (id int not null auto_increment, category_id int null, name varchar(100) not null, primary key(id)) engine=InnoDB;
create table customers (id int not null auto_increment, name varchar(100) not null, primary key(id)) engine=InnoDB;
create table orders (customer_id int not null, product_id int not null, primary key(customer_id, product_id)) engine=InnoDB;
2. 足場作成
ruby script/generate scaffold Product AdminProduct
ruby script/generate scaffold Customer AdminCustomer
ruby script/generate scaffold Order AdminOrder
3. URL
http://localhost:3000/admin_product
http://localhost:3000/admin_customer
http://localhost:3000/admin_order
4. 取りあえず orders テーブルの入力画面を作る
モデル側
app/models/order.rb に 依存関係を記述する
belongs_to :customer
belongs_to :product
View側
(1) app/views/admin_order/_form.rhtml を編集
<p><%=
@customers = Customer.find(:all).map{|u| [u.name, u.id] }
select 'order' , 'customer_id' , @customers
%></p>
<p><%=
@products = Product.find(:all).map{|u| [u.name, u.id] }
select 'order' , 'product_id' , @products
%></p>
(2) app/views/admin_order/list.rhtml を編集 : 一覧表示
<td><%=h order.customer.name %></td>
<td><%=h order.product.name %></td>
と、ここまでは先の例と同じ。
5. customer の一覧から products が参照できるようにする (★ ポイントはここだけ)
モデル側
app/models/customer.rb に依存関係を記述
has_and_belongs_to_many :products, :join_table => 'orders'
→ 実は orders テーブルの代わりに
customers_products っていう名前にしとくと 第2引数は不要
ビュー側
app/views/admin_customer/list.rhtml を編集
<th>買ったもの</th>
...
<td>
<% customer.products.each do |product| %>
<%=h product.name %><br />
<% end %>
</td>
→ customers から(ordersテーブルを挟んで向こう側の)
productsテーブルが配列として取れる!
6. ついでに罫線とかつけたいから css をいじる
public/stylesheets/scaffold.css
table, th, td {
border-color: silver;
border-width: 1px;
border-style: solid;
padding: 5px;
margin: 0px;
vertical-align: top;
border-collapse: collapse;
}
th {
background-color: lavender;
}
7. 完了! これをSQLいっこも書かずにいけるってすごいですよね。
Q. でも「多対多」の結合って、同じテーブルを指してるときあるよね?
A. 例を書いてみた
+--------------------+ +----------------------+
| products | | recommend_list |
+--------------------+ +----------------------+
| id | (1 ―┬ ∞) | product_id |
| name | └ ∞) | recommend_product_id |
| category_id | +----------------------+
+--------------------+
お勧めリスト(recommend_list)テーブルに、商品(products)に対する、お勧め商品(products)を登録しておくという設定。
先の例で has_and_belongs_to_many, :products としたいけど、どっちのフィールドも products に関連づいてるからどうしようという話。
→ Railsではこういう場合、「継承」が便利に使える。
モデルの product を継承した recommend_product を作成すると、
次のように、あたかも recommend_products というテーブルがあるかのように扱える。
+--------------------+ +----------------------+
| products | | recommend_lists |
+--------------------+ +----------------------+
| id | (1 ―― ∞) | product_id |
| name | ┌ ∞) | recommend_product_id |
| category_id | | +----------------------+
+--------------------+ |
△ |
| (継承) |
+--------------------+ |
| recommend_products | |
+--------------------+ |
| (id) | (1 ―┘
| (name) |
| (category_id) |
+--------------------+
1. DDL
create table products (id int not null auto_increment, category_id int null, name varchar(100) not null, primary key(id)) engine=InnoDB;
create table recommend_lists (product_id int not null, recommend_product_id int not null, primary key(product_id, recommend_product_id)) engine=InnoDB;
2. 足場作成
ruby script/generate scaffold Product AdminProduct
ruby script/generate scaffold Customer AdminRecommendList
3. URL
http://localhost:3000/admin_product
http://localhost:3000/admin_recommend_list
4. 子クラスを定義する(★ポイントはここだけ)
モデル側
app/models/product.rb に 子クラスを定義する
class RecommendProduct < Product
end
5. 後はいままで同様 recommend_lists テーブルの入力画面を作る
モデル側
app/models/recommend_list.rb に依存関係を定義
belongs_to :product
belongs_to :recommend_product
View側
(1) app/views/admin_recommend_list/_form.rhtml を編集
<p><%=
@products = Product.find(:all).map{|u| [u.name, u.id] }
select 'recommend_list' , 'product_id' , @products
%></p>
<p><%=
@recommend_products = RecommendProduct.find(:all).map{|u| [u.name, u.id] }
select 'recommend_list' , 'recommend_product_id' , @recommend_products
%></p>
(2) app/views/admin_recommend_list/list.rhtml を編集 : 一覧表示
<td><%=h recommend_list.product.name %></td>
<td><%=h recommend_list.recommend_product.name %></td>
6. product の一覧から recommend_products が参照できるようにする
モデル側
app/models/product.rb に依存関係を記述
has_and_belongs_to_many :recommend_products, :join_table => 'recommend_lists'
ビュー側
app/views/admin_product/list.rhtml を編集
<th>買ったもの</th>
...
<td>
<% customer.products.each do |product| %>
<%=h product.name %><br />
<% end %>
</td>
→ customers から(ordersテーブルを挟んで向こう側の)
productsテーブルが配列として取れる!
7. 完了! なんだかスマートだ。
未整理
TODO: PHP+Smartyで開発している人を対象としたRoR勉強会してみる
※他にこんなのがあるよって話
Python: Django
PHP: Symphony, Maple(国産!+DIコンテナ使用が特徴。でも開発が止まってる),
※ Ruby使わなくてもPHPでも参考に出来るパターンがたくさん詰まってるよって話
※ いいと思うところ
「よくある悩み」のキレイな解決法が示されている
・テンプレートの共通部分(layout)置き場と書き方。
・テンプレートでコントローラのインスタンス変数が参照できること
・O/Rマッピングされたクラスがgenerateコマンドで生成できること(ActiveRecord!ActiveRecord!)
・フィルタ
・フォーム関連のヘルパー
TODO: Symphonyも使ってみる→機能の対応表とか作る?→そんなモチベーションはなくなってきた