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 より
ちょっとかっこいいです。