save していない ActiveRecord インスタンスで、親知らずな子レコードが取得される
save していない ActiveRecord インスタンスで、association メソッドと、association 先の scope を chain した際の挙動に違和感を感じたので書いてみます
# Rails 3.2.3 で試しています
Model を用意して、
class Firm < ActiveRecord::Base has_many :clients end
class Client < ActiveRecord::Base belongs_to :firm end
データを作成します
firm = Firm.create! name: '37signals' # => #<Firm id: 1, name: "37signals"> firm.clients << Client.new(name: 'WWF') # => [#<Client id: 1, name: "WWF", firm_id: 1>] firm.clients << Client.new(name: 'patagonia') # => [#<Client id: 1, name: "WWF", firm_id: 1>, #<Client id: 2, name: "patagonia", firm_id: 1>]
この状態で、Firm#clients を実行した結果は以下です
firm.clients
# => [#<Client id: 1, name: "WWF", firm_id: 1>, #<Client id: 2, name: "patagonia", firm_id: 1>]
つぎは、order という scope を chain して実行した結果です
firm.clients.order :name # => [#<Client id: 2, name: "patagonia", firm_id: 1>, #<Client id: 1, name: "WWF", firm_id: 1>]
"association メソッドと、association 先の scope を chain した" というのはこのケースをいっています
ここまでは、まったくイメージ通りで、問題なしです
つぎに、少しデータを編集します
firm.clients = []
# => []
上を実行すると内部的には以下の SQL が実行され
UPDATE `clients` SET `firm_id` = NULL WHERE `clients`.`firm_id` = 1 AND `clients`.`id` IN (1, 2)
"37signals" の clients として、"WWF", "patagonia" は参照できない状態になります
この状態で以下を実行した場合の結果は何でしょうか?
Firm.new.clients
空の配列です
# => []
では、以下を実行した結果は何でしょうか?
Firm.new.clients.order :name
んんん、以下になりますー
# => [#<Client id: 2, name: "patagonia", firm_id: nil>, #<Client id: 1, name: "WWF", firm_id: nil>]
改めて実行してみると、違和感どころの騒ぎじゃないような
実行される SQL は、以下となっていて気持ちは伝わってきますが、こちらの気持ちは汲み取ってもらえていないですね
SELECT `clients`.* FROM `clients` WHERE `clients`.`firm_id` IS NULL ORDER BY name
ちなみに、
Rails 2.3.14 で、同じことをすると イメージ通りの結果がかえります
# order という scope は、Client に自前で実装している前提です
Firm.new.clients.order :name # => []
実行される SQL は以下となっています
SELECT * FROM `clients` WHERE (`clients`.firm_id = NULL) ORDER BY name
どっちにしても、
データベースに保存されていないインスタンスが、association メソッドを実行する場合、データベースへの問い合わせは必要ないんじゃないだろうか
今日知った Rails AR#to_param
Rails では、 routes.rb で、こんな指定をすると
ActionController::Routing::Routes.draw do |map| map.resources :users end
いつもの Routing が定義され
users GET /users {:controller=>"users", :action=>"index"} formatted_users GET /users.:format {:controller=>"users", :action=>"index"} POST /users {:controller=>"users", :action=>"create"} POST /users.:format {:controller=>"users", :action=>"create"} new_user GET /users/new {:controller=>"users", :action=>"new"} formatted_new_user GET /users/new.:format {:controller=>"users", :action=>"new"} edit_user GET /users/:id/edit {:controller=>"users", :action=>"edit"} formatted_edit_user GET /users/:id/edit.:format {:controller=>"users", :action=>"edit"} user GET /users/:id {:controller=>"users", :action=>"show"} formatted_user GET /users/:id.:format {:controller=>"users", :action=>"show"} PUT /users/:id {:controller=>"users", :action=>"update"} PUT /users/:id.:format {:controller=>"users", :action=>"update"} DELETE /users/:id {:controller=>"users", :action=>"destroy"} DELETE /users/:id.:format {:controller=>"users", :action=>"destroy"}
いつもの URL が使えます。
/users/1
これはこれで素敵ですが、
すぐに :id ではなく、名前等の属性を利用した Routing を定義したくなります。
イメージはこんな感じです。
user GET /users/:name {:controller=>"users", :action=>"show"}
しばらく良い方法がわかりませんでしたが、
AR#to_param をみつけました。これで解決できそうです。
class User < ActiveRecord::Base def to_param # overridden name end end user = User.find_by_name('Phusion') user_path(user) # => "/users/Phusion"
Routing が変わるわけではないので、Controller では、params[:id] を利用します。
class UsersController < ApplicationController def show @user = User.find_by_name(params[:id]) end end
id を name にするのではなく、identifier として、name を使うと考えた方が良さそうです。
これで、いつもの URL より
ちょっとかっこいいです。
はじめます
はじめまして
よろしくおねがいします