ActiveRecord の dup の挙動

ひとことで言うと、 ActiveRecord には dirtry な attribute を追跡するための便利なメソッド群が定義されていますが、dup とともにこれらを扱うときには要注意ですよ、というお話です。

_was とか _changed? は便利

dirty な attribute を追跡するってのはどういうことかっていうと、たとえばこういうことです。

# DBからuserをひとり引いてくる
user = User.first
user.name # => "shinpei"
user.name_was # => "shinpei"
user.name_changed? # => false

# name atribute を書き換える
user.name = "nekogata"
user.name # => "nekogata"
user.name_was # => "shinpei"
user.name_changed? # => true

# DBに保存する
user.save!
user.name # => "nekogata"
user.name_was # => "nekogata"
user.name_changed? # => false

要するに「書き変わってるんだけど、未だにDBに保存されていないattributeはどれですか、書き変わる前の値はなんですか」みたいなことを記録してくれてるんですね。オー、便利。

dup すると面白い挙動する

# DBからuserをひとり引いてくる
user = User.first
duped = user.dup

user.name # => "shinpei"
user.name_was # => "shinpei"
user.name_changed? # => false

duped.name #=>"shinpei"
deped.name_was # => ""
deuped.name_changed? # => true

おおお?という感じの挙動ですね。ちょっとバグっぽい感じもする。しかしこれは意図された挙動です。それは ActiveRecord のテストからも確認できます。

rails/dup_test.rb at master · rails/rails · GitHub

test_dup_not_persistedtest_dup_has_no_id、きわめつけに test_dup_with_changes を見てみましょう。

このあたりを読むと、ActiveRecorddup は以下のような使われた方をすることを想定したメソッドであることが見て取れます。

# DBからユーザーを引く
user = User.first 

# (pk以外は) 同じ値を持ったユーザーをDBに保存
user.dup.save!

なるほど? つまり、ActiveRecorddup は、「オブジェクトのコピー」を作るメソッドではなくて、「レコードのコピー」を作るメソッドとして意図されているわけですね。

オブジェクトのコピーが欲しいなら clone を使おう

よけいなことしないでくれ!俺は単にオブジェクトのコピーが欲しいんや!というときにはどうすればいいかというと、clone メソッドを使ってあげましょう。こうすれば普通の挙動をします。

# DBからuserをひとり引いてくる
user = User.first
cloned = user.clone

user.name # => "shinpei"
user.name_was # => "shinpei"
user.name_changed? # => false

cloned.name #=>"shinpei"
cloned.name_was # => "shinpei"
cloned.name_changed? # => false

API docも読もう

dup (ActiveRecord::Core) - APIdock

clone (ActiveRecord::Core) - APIdock

Rails で content-type が application/json などの場合、MethodOverrideが効かない件

背景

Rails には MethodOverride の仕組みがあって、これはなにかっていうと、HTTP method が POST の場合でも _method=PUT とかそういうパラメーターを付けてリクエストしてあげると Rails のレイヤーではそのリクエストを PUT として扱ってくれるやつです。ブラウザの form が GET と POST しかサポートしてないけど疑似的に RESTfull にアプリケーションを作りたいみたいなときに重宝するやつですね。

さて、このMethodOverrideですが、content-typeが "application/x-www-form-urlencoded" か "multipart/form-data" のときにしか効いてくれません。これはまあ普通に考えてそれ以外のリクエストは Ajax によるリクエストとかなんか別の(iOSアプリだとかAndroidアプリだとか)クライアントからのリクエストであることが想定できるので、クライアントが PUT とか DELETE とかを送ってくれば良いだけの話なので、合理的と言えると思います。

しかし、その合理的な判断がちょっと困る場合が無い訳ではありません。GET と POST しか通さない邪悪なプロキシの下でインターネット接続をしなければならないことを強いられているひとびとにサービスを提供したい場合などです。この場合、Ajax であっても PUT メソッドや DELETE メソッドが使えないので、しょうがないので POST でリクエストを投げたいな、となったりします。

筋の良い解決方法

この MethodOverride を実際にやってくれてるのは Rails ではなくて Rack::MethodOverride です。実際にそいつを見てみると、_method というパラメータだけではなくて、X-HTTP-Method-Override という拡張ヘッダに PUT や DELETE を指定してあげれば良いことがわかります。まあこれが普通に「筋の良い」解決方法だと思います。

しかし、この「筋の良い」解決方法が使えない場合があります。それは拡張ヘッダを通してくれないような邪悪な(以下略)

最後の手段

というわけで、最後の手段です。既存の RackMiddleware が JSONの中身の _method を見て MethodOverride してくれないなら、そういう Middleware を作っちゃえばいいのです。ほぼ Rack::MethodOverride からのコピペだけど以下みたいな感じ。

module Rack
  class MethodOverrideJson
    HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK)

    METHOD_OVERRIDE_PARAM_KEY = "_method".freeze

    def initialize(app)
      @app = app
    end

    def call(env)
      if env["REQUEST_METHOD"] == "POST" && env["CONTENT_TYPE"] == "application/json"
        method = method_override(env)
        if HTTP_METHODS.include?(method)
          env["rack.methodoverride.original_method"] = env["REQUEST_METHOD"]
          env["REQUEST_METHOD"] = method
        end
      end

      @app.call(env)
    end

    def method_override(env)
      req = Request.new(env)

      body = req.body.read(req.content_length.to_i)
      req.body.rewind if req.body.respond_to?(:rewind)

      json = JSON.parse(body)
      method = json[METHOD_OVERRIDE_PARAM_KEY] rescue nil
      method.to_s.upcase
    rescue JSON::ParserError
      ""
    end
  end
end

で、この RackMiddleware を Rails に乗せてあげれば、一応これで json で _method を指定した場合でも動くようになります。

結論としては、こういう手段が必要になるのはほんとにコーナーケースであるからこんな手段取らなくていいことが多いし、ほぼコピペという罪を犯しているというのもある。あとこれ一度 req.body を read して rewind してるけど、Rails側でも同じことされるので効率もそんなによくない。ただ、まあ、一応邪悪なプロキシ環境下でも疑似 REST するという目的は達成されているので、邪悪なプロキシ環境下でこういうことをしたいひとは参考にしたら良いんじゃないでしょうか。Rails じゃなくて Rack の層で解決してるので Rails 以外の WAF にも応用できると思います邪悪だしニッチなので gem にはしません。

レールルで Controller から ActiveRecord を生で触るのをやめたい話

単純な操作ならいいんだけど、複数テーブルに対してごにょっと操作をして、それがアトミックでなくてはならなくて、みたいなときの話。

単純な複数テーブルに対する操作なら多分主従の関係みたいなのあると思うしそういうときには主のほうを表すARを継承したモデルクラスにメソッド生やせばいいかなって思う(commentable なやつに comment 書くとかそういうの)んだけど、これがもっと複雑な要件になってきて、しかもトランザクション制御が必要となったとき、テーブルそのもの(あるいはインスタンスならばその中の一行)を表すARクラスにトランザクションあるのめっっっっっっちゃ気持悪い。

なので、小規模なやつではトランザクションとかをコントローラーに書いてるんだけど、複雑になってきたときに破綻するのが目に見えてる。し、トランザクション制御ってビジネスロジックだよね〜〜〜。ほんらいそれはコントローラーにあるべき処理ではない。

なので、たとえば「とあるユーザー A がリソース A を変更する権限を持っているかチェックして、権限を持っていたらリソース A を変更する、そんで、そのリソース A へのアクセス権をもっている他のユーザ B ,ユーザ C への通知を書き込む」みたいなやつは、少しめんどうでもサービスクラスみたいなやつを作って、そこでトランザクション制御や複雑な処理を行うべきなのではないかという気持になっている。

が、アプリケーションが充分にシンプルな間はそういうことしなくていい気もするし、どのタイミングでそういうの導入するべきなのかむずかしい。

ただまあ、AR を継承したモデルが責務を持つのはあくまでリレーションを含めた「テーブル内のデータに変なデータが入り込まないようにする」「そのテーブルに対する操作に名前を付けて抽象化する」という部分であって、「ビジネスロジック的に不整合がないようにする」みたいなところに責務を持つクラスは別にきちんと作るべきなのかな、というのが最近のわたしの考え方です。

ActiveRecord の named_scope に by_<column_name>みたいな名前つける人間は腹を切って死ぬべき……だよね?

最近仕事で ActiveRecord ばかり触ってるのでその話題

ActiveRecoedには named_scope という便利な機能があって、これはまあ動的にクエリを組み立てるのに便利で良いよねって話がまあよくありますね。その話をします

最初に、Railsの嫌なところの愚痴

クエリビルダー死すべきみたいな話はあると思うし一理あると思うんだけど Rails 使う以上 AR 使わないとやってられんみたいなのもあるので AR 内でのベストプラクティスでやるしかない。こういうあたり自由じゃないなーって感じがしてわたしは PerlTIMTOWTDI な感じがやっぱり好きだなーってなるんだけど、まあその話はおいておく。

named_scope は便利って言われるけど何が便利なの

今回は別に named_scope ってのは便利だねってことに関して異を唱えるつもりはなくて、named_scope が嬉しいのはなんでなの? と言う話をしたい。メリットはなんなのかという話だ。なんか scope を chain させていくと動的にクエリが組み立てられて便利!!!しかも遅延評価だから必要になるまでクエリ発行されない!!! みたいなメリットが目立ってる感じするんだけど、それは別に named_scope に限った話じゃなくて普通に where を chain させたっていいし scoped をchain させたって同じことができる。

じゃあ named_scope の何がすばらしいのかというと、わたしが思うに二つある。

ひとつめ

ひとつは、集合(RDB内のレコードたちは集合の一種だ)を絞り込む、その「やりかた」を隠蔽した上で名前をつけられることで、実装を隠蔽して名前をつけるってのは要するに手続きの抽象化だ。たとえば以下のようなものを考えてもらえばいい。

class Player < ActiveRecord::Base
  named_scope :top, lambda{|n|
    {:order => "score DESC", :limit => n}
  }
end

Player.top(3)
# SELECT * from players ORDER BY score DESC LIMIT 3

中ではあるカラムをもとにレコードを整列させて上からn件取ってくるということをやっているんだけど、使う側はそれを外から意識しなくてよくなる。テーブル構造のことを(理想的には)意識しなくていい。これは手続きの抽象化だ。で、これをchainさせていくとデータのロードは遅延されるので、手続きが抽象化された上で、無駄なロードも発生しない(しにくい)。Player.age_under(18).top(3) みたいなことも、 テーブル構造を意識しないまま、無駄なクエリなくできるわけだ。

ふたつめ

もうひとつのすばらしいところは、named_scope で絞り込んだ集合に対する操作を scope 内に定義できることだと思う。これに関しては今回は書かない。

抽象化されないならメリットない(あるいは半減する)

今ここで問題にしたいのはひとつめのメリット、手続きを抽象化(実装を隠蔽)できるのがすばらしいと言う話について。で、ここでようやくこの記事のタイトルが出てくるんだけど、named_scope の利点(のひとつ)は「中でどういうテーブル操作してるか」を見なくていいことであって、だから named_scope に by_<column_name> みたいな名前をつけるのはクソなんじゃないかなー。それはテーブル構造を抽象化できてない。 scoped(:conditions => {:column_name => }) してるのと変わらない。実装がむき出しになってる。だったらそれ named_scope じゃなくてもいいよね、適材適所で使えてないよね。あと、余談として、scope1_and_scope2(param1, param2)みたいな命名も、何も抽象化してないから意味ないよね。だったら普通に scope1(param1).scope2(param2)みたいにすればいいじゃん。

この記事の言いたい事

というわけで、ほんらい named_scope は「どのような意味を持ったサブ集合に注目(scope)するのか」という観点で命名、定義すべきであって、「どうやってクエリを発行するのか」という観点から命名、定義するべきではないんじゃないの、chainできて便利ー遅延評価で便利ーみたいなのはちょっと見るべきポイントがズレてるんじゃないの、というのが、この記事で問題提起したいことです。

最後に言うまでもない余談

余談として、言うまでもないけど、named_scopeの中身、実装の部分では、実行効率と保守性の観点からどのようなSQLを発行するべきなのかもきちんと考えるべき(というかそれが本分)だと思います。SQLRDBMS わかんないならそもそもO/Rマッパーも使えるはずない、という気持ちも持つべきだと思う。