gem の内部を書き換える2つの方法 + おまけ(後から明示的に dependent: :destroy を止める方法 Rails3 限定)
皆さん、こんにちは。MUGENUP の osada です。 いきなりですが、
gem の内部を書き換えたい!
と思ったことはありませんか?
globalize
というgem
が、内部で
has_many :translations, dependent: :destroy
を実行するのですが、このdpendent: :destroy
を止めたい、という要望から発生した、
gem
の内部の挙動をオーバライドする方法のご紹介です。
想定する読者は、ruby
を使い始めたばかりで、オープンクラス
やメタプログラミング
を使ったことがない人です。
概要
dpendent: :destroy
を止める方法として、下記の方法を紹介します。
オープンクラス
を使ったパッチを当てるmodule_eval
を使ってパッチを当てる- has_many を再定義する
- (
Rails 3.2
) destroy 用のメソッドをオーバーライドする - (
Rails 3.2
) destroy 用の callback を停止する
gem のクローンは最終手段
gem の挙動の一部を書き換えたいからといって、 gem を app などにコピーして使うのは、悪手だと考えています。 理由は、 バージョンアップに追従できない からです。 例えば、重要なセキュリティ修正 があった場合、それを取り込むのは大変難しくなるでしょう。
それよりも、gem をクローンし、パッチを宛てたものを 新しい gem として読み込む方が適切です。 この場合、クローン元であるオリジナルの gem をマージしてくることで、 バージョンアップに追従することができます。
しかしこれも、マージなどのメンテナンスコストを考えると最終手段だと考えます。
(もし、大きな修正が必要である場合、gem を継承して拡張するほうが適切でしょう)
オープンクラスを使ったモンキーパッチを当てる
軽度な修正であれば、モンキーパッチを当てる方が簡単です。
ruby の オープンクラス
は、拡張として使うことができます。
オープンクラスについては、 シンボルでも文字列でもアクセス可能なHashを使おう!ActiveSupport {Hash編} - MUGENUP技術ブログ でも触れました。
同名でクラス定義を行うと、クラスの再定義(上書き)ではなく、クラスへの追加拡張になる仕組み。
です。 つまりそれは、gem の中で定義したものであっても、app 側で再定義できる、ということです。
今回書き換えたい globalize
の has_many
は、vendor/bundle/gems/globalize-3.1.0/lib/globalize/active_record/act_macro.rb
の、def setup_translates!(options)
に定義されています。
module Globalize module ActiveRecord module ActMacro ...... def setup_translates!(options) ...... has_many :translations, :class_name => translation_class.name, :foreign_key => options[:foreign_key], :dependent => :destroy, :extend => HasManyExtensions, :autosave => false ...... end end ...... end end
まずこのファイルを、config/initializer/extensions/globzlize/active_record/act_macro.rb
にコピーし、setup_translates!
以外を削除、has_many
から dependent: :destroy
を
削除します。
module Globalize module ActiveRecord module ActMacro protected def setup_translates!(options) ...... has_many :translations, :class_name => translation_class.name, :foreign_key => options[:foreign_key], :extend => HasManyExtensions, :autosave => false ...... end end end end
以上、終了です。簡単ですね!
別の場所でクラスを開きなおして、メソッドを再定義するだけで良い のですから、
わざわざ gem
をコピーしたり、クローンしたりする必要はありません。
メソッドチェインを使用する。
上記のメソッドを再定義するタイプのパッチは、非常に簡単ですが、
alias_method_chain
などのメソッドに紐づいた実装
を切ってしまう、という欠点があります。
どこかで誰かが、さらにパッチを当てているかもしれません。
できれば、メソッドチェインタイプのパッチを宛てる方が良いでしょう
module Globalize module ActiveRecord module ActMacro def setup_translates_with_stop_dependent_destroy!(options) setup_translates_without_stop_dependent_destroy!(options) has_many :translations, :class_name => translation_class.name, :foreign_key => options[:foreign_key], :extend => HasManyExtensions, :autosave => false end alias_method_chain :setup_translates!, :stop_dependent_destroy end end end
実際、このglobalize
は globalize-versioning
という拡張gemを用意しており、
このタイプの拡張でなければ、同時に使うとエラーになってしまいます。
module_eval を使ってパッチを当てる
さて、alias_method_chain
であれば、オープンクラスである必要はなさそうな気がします。
それがもう一つの方法、module_eval
を使う方法です。
config/initializers/globalize_patch.rb
に先ほどのパッチをコピーします。
先ほどとは異なり、名前空間が異なるので、HasManyExtensions
は、
::Globalize::ActiveRecord::HasManyExtensions
のように宣言する必要があります。
Globalize::ActiveRecord::ActMacro.module_eval do def setup_translates_with_stop_dependent_destroy!(options) setup_translates_without_stop_dependent_destroy!(options) has_many :translations, :class_name => translation_class.name, :foreign_key => options[:foreign_key], :extend => ::Globalize::ActiveRecord::HasManyExtensions, :autosave => false end alias_method_chain :setup_translates!, :stop_dependent_destroy end
以上、終了です。簡単ですね!
といっても、これらはコードを読む側
からすると、
どこで何が再定義されているかわからない 、
知らないところで何かが勝手に変わっている 、という意味で魔術的手法です。
用法・用量を守ってお使いください
オープンクラスと module_eval(class_eval) の違い
さて、この2つはどう違うのでしょうか?
みんな大好き stackoverflow
に、回答がありましたので、引用します。
ruby - class_eval and open classes - Stack Overflow
reopen class will not inform you (but class_eval will raise error) if the existing class does not exist or not loaded.
class_eval
は拡張元のクラスがないとき エラーになる。オープンクラスは何も教えてくれない。
オープンクラスというのは、クラスを定義する際の特徴的な動き、というだけのことであり、 そもそも拡張を意図としている訳ではない、ということですね。
特徴的なオープンクラス
他の言語では、module_eval のような拡張の仕方がメインなのではないでしょうか。
例えば、python であれば、後からクラスにメソッドを追加するときは、定義したメソッドを追加する、というコードになります。
まず、オープンクラスではないので、class A
を再定義すれば、メソッドは消えてしまいます。
class A(object): def hello(self): print "hello", self.__class__.__name__ A().hello() # => hello A class A(object): def bye(self): print "bye", self.__class__.__name__ A().hello() # Traceback (most recent call last): # File "tmp.py", line 11, in <module> # A().hello() # AttributeError: 'A' object has no attribute 'hello'
よって、メソッドを定義し、クラスに追加することで、拡張します。
class A(object): def hello(self): print "hello", self.__class__.__name__ def bye(self): print "bye", self.__class__.__name__ A.bye = bye a = A() a.bye() a.hello() # => bye A # => hello A
メソッドへの割り込みは例えば以下のようになるかもしれません (他にも拡張の方法があると思います)。
class A(object): def hello(self): print "hello", self.__class__.__name__ def hello_with_bye(self): self.hello_without_bye() print "bye", self.__class__.__name__ A.hello_without_bye = A.hello A.hello = hello_with_bye A().hello() # => hello A # => bye A
coffeescript(javascript) の場合も同じです。
class A hello: -> console.log "hello" new A().hello() # hello class A bye: -> console.log "bye" new A().hello() # TypeError: Object #<A> has no method 'hello' # at Object.<anonymous> (/Users/osada/tmp.coffee:11:9) # at Object.<anonymous> (/Users/osada/tmp.coffee:1:1) # at Module._compile (module.js:456:26)
class A hello: -> console.log "hello" A.prototype.bye = -> console.log "bye" a = new A() a.hello() a.bye() # hello # bye
class A hello: -> console.log "hello" A.prototype.hello_without_bye = A.prototype.hello A.prototype.hello = -> this.hello_without_bye() console.log "bye" new A().hello() # hello # bye
……ちょっと例が悪かったでしょうか。
python
と javascript
は大変親和性が高いので、
同じようなコードになってしまいました。
いずれにせよ、
- クラス定義
- クラスに対して、メソッドを追加
という手順を踏むことになります。
オープンクラス
のように
- クラス定義
- 同名のクラス定義を使用して、定義の続きを行う
という動作は、他の言語と比べると、かなり特徴的であると思います。
予想外の箇所でオーバライドしてしまうこともあるため、 理解して使うことがオススメです。
コラム [オープンクラスでテストが落ちた]
単体テストは通るのに、全体テストが通らない、という原因がオープンクラスだったことがあります。 モジュールのテストのために、適当なクラスを宣言して、include して使っていた所、
class Foo include SomeModule end
別の箇所でも
Foo
を定義しており、全体テストのときのみ、再定義が発動して テストが落ちていました。 他の言語であれば、同名のクラスは再定義されるので、no method error
で落ちるところが、 オープンクラスでは、メソッド自体は定義されないので落ちないが、 他で定義されたコードにより想定外の干渉が起きるということです。 オープンクラスならではだと思います。
has_many を再定義する
さて、パッチは魔術だ、というお話を致しました。
実際、該当のソースを読んだだけでは、dependent: :destroy
が無効化されていることには気づけないでしょう。
できるなら、明示的に オーバライドしていることを宣言したいところです。
この globalize
は translates
メソッドを呼ぶときに、内部で has_many
を定義しています。
translates :description, versioning: :paper_trail
# translates メソッドを呼ぶと、gem 内のメソッドで下記が実行される has_many :translations, :class_name => translation_class.name, :foreign_key => options[:foreign_key], :dependent => :destroy, :extend => HasManyExtensions, :autosave => false
よって、translates
の後に has_many
を再定義してしまえば、上書きされます。
例えば、Project
というクラスに使用した場合、
translation_class.name
は Project::Translation
になります。
class Project translates :description, versioning: :paper_trail has_many :translations, :class_name => Project::Translation, :foreign_key => nil, :extend => ::Globalize::ActiveRecord::HasManyExtensions, :autosave => false
これを毎回書くのは大変なのですが、読み手には優しくなります。 もう少し楽に書きながら、明示する方法はないでしょうか?
destroy 用のメソッドをオーバーライドする (Rails3.2)
dependent: :destroy
は、before_destroy
で発動します。
このメソッドをオーバーライドしてしまえば、
事実上、destroy
を止めることができます。
メソッドはhas_many
で定義されており、名前は "has_many_dependent_for_#{name}"
。
つまり今回で言うと、"has_many_dependent_for_translation"
です。
def configure_dependency if options[:dependent] ............ model.before_destroy dependency_method_name end end def dependency_method_name "has_many_dependent_for_#{name}" end
よって、translates
の後に、true
を返すメソッドとして再定義すればOKです。
translates :description, versioning: :paper_trail has_many_dependent_for_translations = -> { true }
このように再定義できることを知っておくと、コールバックに割り込みができて便利です。
しかし単にコールバックを止めることはできないのでしょうか?
実は、skip_callback
として下記のコードを書いても、停止されなかったのです。
translates :description, versioning: :paper_trail skip_callback :destroy, :before, :has_many_dependent_for_translations
ここには一つ、バッドノウハウを知っている必要があります。
destroy 用の callback を停止する(Rails3.2)
結論から書くと、dependen: :destroy
のコールバック自体を止めるには下記のように書きます。
translates :description, versioning: :paper_trail skip_callback :destroy, :before, "(has_many_dependent_for_translations)"
括弧をつけたメソッド名の文字列 です。
callback
には、メソッド名のシンボル、文字列、または Proc を設定できます。
ここに登録されるときの名前が、文字列の場合には 括弧が付く ようです。
def _compile_filter(filter) method_name = "_callback_#{@kind}_#{next_id}" case filter when Array filter.map {|f| _compile_filter(f)} when Symbol filter when String "(#{filter})”
こちらは destroy コールバック自体を止めているので、一番適切な止め方だと考えています。
括弧が付く
というのは、かなりのハマりどころでした。
しかし残念なことに、Rails4.0 でこの実装はなくなってしまいました。 汎用的にコールバックを停止することは難しいようです。
Rails 4 では、メソッド名に由来する削除ができない
Rails 4
では、コールバックの登録が lambda
になっており、
メソッド名を使用した、オーバーライドや、コールバックの停止、が出来なくなっています。
def self.add_before_destroy_callbacks(model, reflection) ....... model.before_destroy lambda { |o| o.association(name).handle_dependency } end
before_destroy
での登録が lambda
なので、
skip_callback
を使おうとしても対象が宣言できないためです。
よって、対応策として、下記の2つを考えました。
- reset_callbacks(:destroy)
- callbacks を直接操作する
reset_callbacks
は全てのコールバックを消してしまうため、
dependent: :destroy
以外の destroy callback
を使っている gem
には、使用できません。
また、他の destroy
よりも前に宣言しておく必要があります。
translates :description, versioning: :paper_trail reset_callbacks(:destroy)
一方、callbacks
を直接操作するのは、かなりの黒魔術です。
translates :description, versioning: :paper_trail skip_callback :destroy, :before, _destroy_callbacks.instance_variable_get(:@chain).last.filter
こちらは、直近に登録された filter
を取り出すことで、skip_callback
を使っています。
こちらも、複数のcallback
が登録されていた場合、dependent: :destroy
が last
とは
限らないため、慎重な使い方が要求されますし、使うべきではないでしょう。
よって、Rails 4
の場合、明示して打ち消すのは難しいようです
まとめ
gem の中を書き換えるには、下記の4つの方法が考えられます。
- app に コピーして修正して使う
- クローンして修正した gem を使う
- オープンクラスでパッチを当てる
- module_eval(class_eval) でパッチを当てる
dependent: :destroy
を止めるには、Rails 3
系であれば、下記2つを使い、明示的に止めることができます。
- destroy 用のメソッドをオーバーライドする
- destroy 用の callback を停止する
しかし、Rails 4
では、パッチを当てることが有効でしょう。
ということで、gem の内部を書き換える方法の共有でした。
検索用
今回、幾つかググっても情報が出てこなかったので、検索用に英語用のワードも記載しておきます。 この記事が誰かの助けになることができれば幸いです。
Cancel (remove) the
dependent destroy
defined in gem
「Qiitaの中の人とQiita:Teamを使う人と共に理想のチームについて考えてみませんか?」というイベントに出てきました
こんにちはMUGENUPの伊藤です。
2014年5月15日(木)に開催された「Qiitaの中の人とQiita:Teamを使う人と共に理想のチームについて考えてみませんか?」というイベントでQiita:Teamを使う人としてお話させていただきました。
MUGENUPでは昨年の夏頃からQiita:Teamを使っています。 以前にはIncrementsさんに事例として取り上げていただきました。
Qiita:Teamを使った議事録の共有・保存で仕事の効率化をめざす MUGENUP CTO 伊藤勝悟氏 - Qiita Blog
イベント当日はIncrements代表取締役の海野さんと二人で「チーム」「組織」「情報共有」「コニュニケーション」と言った切り口から各社でどのような取り組みをしているかについてお話させていただきました。
ここでは弊社のコミュニケーションについて起こった問題と解決のためのトライを書いてみます。
エンジニアと他部署でのコミュニケーションの必要性
開発部ではクラウドソーシングのためのクリエイター、クライアント、MUGENUPを結ぶ管理システムをメインで開発しています。 このシステムは社内の殆どの人が日々の仕事をする上で多くの時間を割く重要なものです。
自分たちが作っているものを実際に使う人(=社内の人)とコミュニケーションを取ることは重要な事です。
人が増えてオフィスが増えた
現在MUGENUPはオフィスが3部屋あります。 これまで人数の増加に伴い段々と拡張してきました。
開発部も途中から新しい部屋にお引っ越しになり、当初は広い部屋を専有して開発していたのですが、あっという間にその部屋も埋まってしまいました。
コミュニケーションが減る
部屋がわかれると他の部屋にいる人たちとのコミュニケーションが大きく減ります。 一日中顔も合わせないということもざらにあります。
自分たちが作っているものを実際に使っている人とのコミュニケーションが減ってくると、 今まで出来ていた開発時の意思疎通が難しくなってきます。
どういう対応をしているか
そこで、現在開発部と別の部署でコミュニケーションを取る機会を増やしています。 一緒にランチしたり、時間をとってもらいどのように仕事をしているかの話を聞いたりなどです。
ただ、これは一時的な解決の部分もあり、より人数が増えた場合どうするのかといった問題も有ります。 そのような点も踏まえて開発の仕組みを考える必要があると再認識しました。
(全社的な取り組みについて)
イベントでお話した際に皆さんの反応が大きかったのが「パワーランチ」という取り組みです。 これは新しく入った社員の方が「他の部署の人たちとなかなか接する機会がない」ということで提案した企画でした。
内容は週に一回各部署から何人かずつ選び、その人達でランチに行く(経費は会社持ち)というものでした。
3月くらいから初めて、先日全社員が一通り行き終わったところだったのですが、私も普段から開発部の人たちとしかランチには行かないのでなかなか新鮮な体験でした。
「うちの会社でも似たようなことやってます」という方もいらっしゃったので、結構メジャーな企画なんですかね?
ドメイン名を使ってEC2を運用していたら、ELBのスケールアウトで苦労した話
2014年5月14日 13時00分 修正 タイトルが誤解を招くものだったので、「なぜ URL に www を付けるのか。または、サブドメインなしでは CNAME が使えない件」から変更致しました。併せて、画像に Public IP と Private IP の明記を行いました。
皆さん、こんにちは。MUGENUP の osada です。
今回は、スケールアウト時にELB(Amazon Elastic Load Balancer)
を使うときの注意点についての記事です。
といっても、インフラ・エンジニア
には自明のことと思いますので、読者の対象は
インフラ・エンジニアではないけど、インフラもやる
というベンチャーならではのエンジニア向けです。
要旨
ELB
にはEIP
などのAレコード
を関連付けることが出来ず、ドメインとサーバーを結びつけるには提供されるCNAMEを使う必要がありますサブドメイン無しのドメイン名 (zone apex)
には、CNAMEレコード
を貼ることはできません。ELB
を使ったスケールアウトを考えているなら、zone apex
ではなく、サブドメイン
で運用すると簡単ですzone apx
を使って運用する場合、スケールアウトには、route 53
が必要そうです。
弊社 のインフラは AWS です。
まず弊社のインフラをお話すると、AWS(Amazon Web Service)を使用しており、CTO の伊藤がメイン・オペレーションを担当しています
(彼は TCP/IP や WebSocket の RFP などを嬉々として語る通信系のツワモノです)。
しかし、それでは CTO が 単一障害点(Single Point of Failure, SPOF)
となってしまうため、
社員全員が AWS へのアクセス権を持ち、一通りオペレーションができる状態となっています。
スケールアウトの頓挫
さて、先日、ご好評いただいている弊社のゲーム攻略サイトみなゲー(β版)が アクセス増のため、スケールアウトが必要となりました。
サーバ1台だったものを、ロードバランサーを使って、2台構成にする
という、とても単純な作業です。
……と、その時はまだ思っていたのです。
元々は、サーバ1台に対して、IP を使って DNS を貼っていました。
そこで、ELBに従来のIPを設定し、ELBから、新しい2台のサーバに振り分けることを計画しました。
ところが、AWS の ロードバランサーである、ELB (Elastic Load Balancing) には、 IP を振ることができなかったのです。
ELB が IP を持たない理由
SPOF(単一障害点)
の話を上述しましたが、これはロードバランサーにおいても同じです。
もしロードバランサー1台が壊れてしまったら、どのサーバにもアクセスできなくなってしまいます。
ELBはそれを避けるため、複数台のロードバランサーを1つとして扱っているようです。
そのため、ELB 自体には、DNS が振られるのであって、IP を振ることはできません。
IP を振る とは? [DNS の設定]
DNS とは、ドメイン名(mina-game.com
など)を扱うサービスです。
ドメイン名の扱い方には複数ありますが、
本稿では、IPアドレスと結びつけるAレコード
と、別名を付けるCNAMEレコード
を取り上げます
レコード名 | 役割 | 例 |
---|---|---|
A (Adress) レコード |
IPアドレスと結びつける | mina-game.com => 123.456.78.90 |
CNAME レコード |
ドメイン名に別名を付ける | mina-game.com => www.mina-game.com |
今まで、mina-game.com
にアクセスすると、123.456.78.90
のサーバにアクセスできていたのは、
Aレコード
が付いていたからです。
ところが、ELB
には IP を付けられない ため、Aレコードを貼ることができません。
よって、この場合、 CNAME を貼る ことが正解となります。
……そう、その時は、それで解決すると思っていたのです。
CNAME には、サブドメイン(ホストドメイン)が必須
ところが、ドメイン名mina-game.com
に対して、CNAME を振ることは出来ない のです。
少々長いですが、AWS から引用します。
ドメイン名を登録する際は、ドメイン名自体だけでなく、一連のサブドメイン名全体も予約することになります。例えば、example.com をカスタムドメイン名として登録する場合、foo.bar.example.com や foo.myLB.example.com などのサブドメイン名を作成することができます。この一連のドメインとサブドメイン名は、ゾーンと呼ばれます。予約した example.com などのドメイン名は、ゾーンの階層の最上部にあるため、Zone Apex と呼ばれます。 DNS ルールでは Zone Apex(example.com など)での CNAME レコードの作成が許可されていません。例えば、example.com というドメイン名を所有している場合、CNAME レコードはサブドメイン名である foo.example.com には使用できますが、Zone Apex である example.com には使用できません。 http://docs.aws.amazon.com/ja_jp/ElasticLoadBalancing/latest/DeveloperGuide/using-domain-names-with-elb.html
mina-game.com
は ゾーンの階層の最上部にある、Zone Apex
であり、それには CNAME
を使用する事ができないのです。
もし、www.mina-game.com
などの サブドメイン を指定してあったなら、
cname
の差し替えで期待どおりの処理が終わっていたことでしょう。
しかし、今回は zone apex
で運用していたため、ELB
を使ったスケールアウトをすることはできませんでした。
Route 53 を使用する
zone apex
で運用を始めてしまった以上、ELB
でのスケールアウトは無理なのでしょうか?
AWSのヘルプの続きに、こう書いてあります。
Zone Apex とロードバランサーの DNS 名を関連付ける場合は、次のオプションを使用します。 Option 2: DNS サービス Amazon Route 53 はホストゾーンのドメインに関する情報を格納するため、Amazon Route 53 を使用してドメインを作成します。
あ、Route 53
使えば良いんですね。
まるでこのために作られたのではないかと思えるくらい、 完璧なお膳立てで、AWS 製の DNS Amazon Route 53 を案内されてしまいました。
というわけで、zone apex
を使って AWS ELB
でスケールアウトするには、route 53
を使おう、という結論に落ち着きました.
元々は使用していなかったサービスが一つ追加で必要になってしまいましたが、無事目的を達成出来ました。
まとめ
ELB
にはEIP
などのAレコード
を関連付けることが出来ず、ドメインとサーバーを結びつけるには提供されるCNAMEを使う必要がありますサブドメイン無しのドメイン名 (zone apex)
には、CNAMEレコード
を貼ることはできません。ELB
を使ったスケールアウトを考えているなら、zone apex
ではなく、サブドメイン
で運用すると簡単ですzone apx
を使って運用する場合、スケールアウトには、route 53
が必要そうです。
というわけで、インフラ初心者が送る
スケールアウトで困った経験
の共有でございました。
参考
もっと詳しい解説
【Rails3.2】【バッドノウハウ】includes 使用時に where で ドットを使うと想定外になるかも知れない件、または、なぜRails4 から references が必要になったのか
皆さん。お久しぶりです。MUGENUP の osada です。
某死にすぎるゲーム
の1周目をクリアしたので、戻ってまいりました!
目標を達成するために大事なことは観察力
である、ということを思い知らされるゲームでした。
さてそんな今回は、Rails 3.2
の includes
に関するバッドノウハウの共有です。
弊社 okuda が、トライ・アンド・エラーの末に見つけた、バグの解説になります。
Rails 4 系
をお使いの方には不要ですので、何かの話のネタになれば幸いです。
includes は LEFT OUTER JOIN と eager loading の 2 種類がある
Rails
では、includes
は 2種類の使い方ができます。
1. eager loading
2. LEFT OUTER JOIN
通常の使い方だと、(1) の eager loading
となり、
where
句の中で、includes
した テーブルを参照すると、
(2) の LEFT OUTER JOIN
となる、という認識でした。
class User < ActiveRecord::Base attr_accessible :email, :name has_many :user_items end describe User do let!(:user) { create :user, name: "user0", email: "user0@example.com" } it "eager loading" do subject = User.where("`name` LIKE ?", "%user0%").explain expect(subject).to include "SELECT `users`.* FROM `users` WHERE (`name` LIKE '%user0%')" # => User Load (0.9ms) SELECT `users`.* FROM `users` WHERE (`name` LIKE '%user0%') # => UserItem Load (1.3ms) SELECT `user_items`.* FROM `user_items` WHERE `user_items`.`user_id` IN (252) end it "outer join by string" do subject = User.includes(:user_items).where("user_items.number IS NULL").explain expect(subject).to include "LEFT OUTER JOIN" # => SQL (2.0ms) SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `user_items`.`id` AS t1_r0, `user_items`.`user_id` AS t1_r1, `user_items`.`item_id` AS t1_r2, `user_items`.`number` AS t1_r3, `user_items`.`created_at` AS t1_r4, `user_items`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `user_items` ON `user_items`.`user_id` = `users`.`id` WHERE `user_items`.`number` IS NULL end end
一体何が問題なのか?
上でLEFT OUTER JOIN
になる条件を
where
句の中で、includes
した テーブルを参照する と書いたのですが、それは誤りでした。
端的に言うと、 where 句の中で、ドットが含まれていれば、LEFT OUTER JOIN とみなす 、ということだったのです。
何が問題になるのでしょうか?
皆さん。where 句に email を使ったことはありませんか?
it "like email" do subject = User.includes(:user_items).where("`email` LIKE ?", "%user0@example.com%").explain expect(subject).to include "LEFT OUTER JOIN" expect(subject).to include "SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `user_items`.`id` AS t1_r0, `user_items`.`user_id` AS t1_r1, `user_items`.`item_id` AS t1_r2, `user_items`.`number` AS t1_r3, `user_items`.`created_at` AS t1_r4, `user_items`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `user_items` ON `user_items`.`user_id` = `users`.`id` WHERE (`email` LIKE '%user0@example.com%')" end
user_items
を join
するつもりがないのに、join
されています。
email にはほとんど必ずドット
が 含まれます。
email 以外では eager loading
なのに、email で where をした途端に、いきなりLEFT OUTER JOIN
になってしまうのです。
無論それは、email
に限りません。name
でドットが含まれていれば同じことです。
it "like name with dot" do subject = User.includes(:user_items).where("`name` LIKE ?", "%user0.name%").explain expect(subject).to include "LEFT OUTER JOIN" end
さらには、string
で構築しているから、という理由でもありません。
arel_table で構築しても、結果は同じです。
it "like name with dot on arel" do subject = User.includes(:user_items).where(User.arel_table[:name].matches("%user0.name%")).explain expect(subject).to include "LEFT OUTER JOIN" end
なぜこんなことになってしまうのでしょうか?
それは、eager loading
と LEFT OUTER JOIN
を区別するものが、下記のメソッドだからです。
references_eager_loaded_tables?
で、どちらなのかを決めますが、
その決定権を握るのがtables_in_string
です。
def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) tables_in_string(join.left) else [join.left.table_name, join.left.table_alias] end end joined_tables += [table.name, table.table_alias] # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq (tables_in_string(to_sql) - joined_tables).any? end def tables_in_string(string) return [] if string.blank? # always convert table names to downcase as in Oracle quoted table names are in uppercase # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end
string.scan
の中で、sql の中に、ドットが含まれているかどうか
、のみで判断していることが見て取れると思います。
末尾の行を pry した結果が下記です。
[1] pry(#<ActiveRecord::Relation>)> to_sql => "SELECT `users`.* FROM `users` WHERE (`users`.`name` LIKE '%user0.name%')" [2] pry(#<ActiveRecord::Relation>)> tables_in_string(to_sql) => ["users", "user0"] [3] pry(#<ActiveRecord::Relation>)> joined_tables => ["users"] [4] pry(#<ActiveRecord::Relation>)> (tables_in_string(to_sql) - joined_tables).any? => true
確かに、ドット
で判断していることが分かります。
まとめ
以上が、Rails3.2
で、includes
に起きる、バッドノウハウです。
知っていればどうということはありませんが、意図せず想定外の結果を産むこともありますので、十分お気をつけ下さい。
むしろ、この機会に、最近リリースされたRails4.1.0
に乗り換えるのも良いのではないでしょうか。
(おまけ) Rails 4.1 では、LEFT OUTER JOIN の使用に、references が必要になった。
さて、そんなわけで、Rails 4から、
LEFT OUTER JOINしたい時は明示的に、
references` を書くことになりました。
User.includes(:user_items).where("user_items.number IS NULL") # => Mysql2::Error: Unknown column 'user_items.number' in 'where clause': SELECT `users`.* FROM `users` WHERE (user_items.number IS NULL) User.includes(:user_items).where("user_items.number IS NULL").references(:user_items) # => SQL (0.4ms) SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `user_items`.`id` AS t1_r0, `user_items`.`user_id` AS t1_r1, `user_items`.`item_id` AS t1_r2, `user_items`.`number` AS t1_r3, `user_items`.`created_at` AS t1_r4, `user_items`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `user_items` ON `user_items`.`user_id` = `users`.`id` WHERE (user_items.number IS NULL)
上述のメソッドは、下記のように書き換わりました
def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) tables_in_string(join.left) else [join.left.table_name, join.left.table_alias] end end joined_tables += [table.name, table.table_alias] # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq (references_values - joined_tables).any? end
変更されているのは、末尾の (references_values - joined_tables).any?
です。
3.2系のときは、(tables_in_string(to_sql) - joined_tables).any?
でした。
これらの問題については、こちらで検討され、修正がされたようです。 includes eager loading is realized with LEFT OUTER JOIN strategy when querying with value contains 'dot' · Issue #7177 · rails/rails
Deprecated
になった、ということでしたが、今回試したところ、エラーになりました。
確認したところ、Rails 4.1
では、使用不可になったようです。
Remove implicit join references that were deprecated in 4.0.
(おまけ 2) Arel を使おう
事例を説明するために、where の中に文字列を書きましたが、sexy ではありません。 綺麗に書きましょう。
it "like name call outer join" do subject = User.includes(:user_items).where(user_items: { number: nil } ).explain expect(subject).to include "LEFT OUTER JOIN" end
(おまけ 3)
使用したテストを置いておきます。
- ruby_test/rails_test/rails3.2/test01/spec/models/user_spec.rb at master · osdakira/ruby_test
- ruby_test/rails_test/rails4.1.0/test01/spec/models/user_spec.rb at master · osdakira/ruby_test
備考1
SQLの確認に、to_sql
ではなくexplain
を使っているのは、Rails 3
系のto_sql
は、eager loading
を考慮しないためです。
Rails 4
系では修正されています。
MUGENUP勉強会を開催しました!
こんにちは、MUGENUPの伊藤です。
今回は先日2/27日に開催した勉強会をまとめてみました!
当日は外部の方を6人お招きし、全部で12人での開催となりました。
開催までの流れ
MUGENUPの開発部では隔週木曜日に勉強会を開催しています。 元々は少人数でそれぞれの人が好きなことを発表するスタイルで、発表中にツッコミとかしながら和気あいあいとやっていました。
また、年末に外部の人を呼んでやったところ好評で、その後定期的に参加してくれるようになった人もいます。
そんなこんなで「本格的に外部の人を呼んでやってみたい!」と思い、元々MUGENUPと御縁のあったエンジニアの方や、知り合いづてなどで何人かに声をかけてみました。
すると皆さん快諾して頂き、晴れてこの勉強会開催にこぎつけました。
会場
今回はMUGENUPの入っているビルにある「シアタールーム」というところで開催しています。 名前の通り映画鑑賞や、カラオケなんかもできちゃう部屋なんですがソファでくつろげたり照明をいじれたりで結構雰囲気を出せるような部屋になっています。
当日の内容
当日は19:30 ~ 21:30の2時間で予定していました。 普段は一人あたり5~10分の発表で、発表中にツッコミを入れたりするので一人あたりだいたい15分くらいでのんびりやっていました。
しかし、段々と参加者が増えていき、「あれ、これそもそも発表時間オーバーするかも」などと思っていたのですが、 最終的には発表者が10名程だったことからなんとかギリギリ全員が発表できました。
さて、気になる当日の発表内容ですが、
vagrant, chef, knife soloを使ってmunin-nodeを複数サーバーに展開し監視をするまで
酔っぱらいレベルを測定できるアプリNomBay
Semantic Versioningについて
psd のファイル構造、または photoshop plugin を作る的な何か
楽しくなるPerl開発環境
RubyのSymbolについて
電子マネーに関する何か
binding.pryに隠れている黒魔術について
といった内容の発表がありました。
各人が好きなことを発表し合った結果、内容がかなりばらつき 普段なら接することはない技術をお互いが知ることができてかなり刺激的な勉強会になりました。
ちなみに私もperlは使ったことがなかったので、その後インストールして色々いじってみました。
社内では「また外部の人を呼んで勉強会をやろう!」という風に盛り上がったので、 また近いうちに開催予定です。
ご興味の有る方は伊藤までご連絡ください!
宣伝
MUGENUP では、知りたがりなエンジニアを募集中です。
無限流開発、ご一緒しませんか?
エントリーはこちら http://mugenup.com/recruit/information#web_application
Rails 4.1 の spring で paralell_tests を使う方法
みなさん、こんにちは! 2週間ぶりのご無沙汰、MUGENUP の osada です。
ruby 2.1.0
、Rails 4.1
で開発した、みなゲー編集部が正式リリースとなりました!
よろしくお願いします。
さて、そんなRails 4.1
の新機能として、プレローダーspring
が標準装備となりました。
本日はspring
とparalell_tests
を併用する方法についてのお話です。
要旨は下記となります。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
では、よろしくお願いします。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせない
Rails 4.1
のテストで、rspec
では成功するのに、paralell_tests
では多くが失敗する、という現象に遭いました。
spring
が原因と考えられます。
通常、rake
やrspec
コマンドを使うとき、コード(code
)を読み込んで、メモリ上にRails
のapp
インスタンスを生成し、使用します。処理が終わると、app
は解放されます。
この処理はかなり重い処理なので、毎回作るのではなく、一度作ったapp
を使いまわそう、というのがspring
の動きです。spring
で作られたapp
は処理が終わっても維持され、再度使用されます。
このspring
はbin_stub
として提供され、bin/
というディレクトリに
rails
, rspec
, rake
のコマンドが用意されます。例えば、
be bin/rake db:create
のように、通常のコマンドの代わりに、bin/
下のコマンドを呼び出すことで、spring
が使用されます。
一方、paralell_tests
というのは、rake
などの処理を複数のプロセスに分けて同時に処理する方法です。
例えば、parallel:spec
を実行すると、下記の4つのコマンドが実行されます。
※
be
はbundle exec
のalias
です
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=3;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=4;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=2;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec
しかし、これにspring
を使用すると、4つのプロセスがあるのに、実際に稼働するapp
は1つになってしまいます。
よって、テストが失敗してしまうのです。
原因はparalell_tests
がbin/rspec
を使用するという点にあります。これをどうにかしましょう。
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bin/rspec ……
結論からいうと、PARALLEL_TESTS_EXECUTABLE
を設定することで、bin/rspec
の使用を回避できます。
PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bundle exec rspec ……
この理由は、parallel_tests
は、実行するファイルを下記のように設定しているからです。
def executable ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable end
またrspec
のdetermine_executable
は下記であり、[bin/rspec
, script/spec
, bundle exec rspec
, spec
, rspec
] のどれかを起動する事がわかります。
def determine_executable cmd = case when File.exists?("bin/rspec") "bin/rspec" when File.file?("script/spec") "script/spec" when ParallelTests.bundler_enabled? cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec") "bundle exec #{cmd}" else %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" } end cmd or raise("Can't find executables rspec or spec") end
bin/rspec
を削除しても良いのですが、それではspring
が使えなくなるので、
PARALLEL_TESTS_EXECUTABLE
を設定する方が適切でしょう。
以上、本項の結論は、
parallel_tests
に、spring
を使わせないためには、PARALLEL_TESTS_EXECUTABLE
を設定する となります。
bin/rspec
を書き換えて、始めのプロセスだけspring
を使う
しかしせっかくのspring
です。
4つの内1つだけでspring
を使い、
残り3つを別のプロセスで動かせば良いのではないでしょうか?
相手はbin_stub
ですから、修正はとても簡単です。
bin/rspec
のこれを、
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") end
こうしましょう!
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else if ENV["TEST_ENV_NUMBER"].nil? || ENV["TEST_ENV_NUMBER"].empty? ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") else exec "bundle", "exec", "rspec", *ARGV end end
ENV["TEST_ENV_NUMBER"].nil?
は通常のbin/rspec
起動時です。
ENV["TEST_ENV_NUMBER"].empty?
は、parallel_tests
の一番始めのプロセスの時です。
つまり、
bin/rspec
として起動したとき、または、parallel_tests
の一番始めのプロセスの時に、spring
を使用する。
というbin_stub
になりました。
さて、肝心のスピードアップですが、feature
テストなどが多いため、私のプロジェクトでは違いが現れませんでした。
参考までとはなりますが、環境の影響が少ないspec/model
に適用した結果を載せておきます。
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" bundle exec rake parallel:spec[model] …… 444 examples, 0 failures Took 216.680576 seconds real 3m47.661s user 3m20.585s sys 0m25.249s
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test bundle exec bin/rake parallel:spec[model] …… 444 examples, 0 failures Took 212.844729 seconds real 3m37.395s user 0m1.378s sys 0m0.285s
real
を見ると10 sec
ほど速くなっているように見えますが、誤差の範囲かもしれません。
もし「速くなったよ!」という方がいらっしゃいましたら、ご一報ください!
FailuresLogger
を使って、失敗したテストを再実行する
feature
テストは実際のブラウザの挙動を再現するため、
タイムアウトなどの理由により、
タイミングによって失敗することもあります。
parallel_tests
を使うと、さらに失敗しやすくなります。
たまたま失敗したのか、本当に失敗しているのかを確認するため、
失敗したテストのみを再度実行するのですが、
その時に役立つのが、FailuresLogger
です。
.rspec_parallel
に paralell_tests
のみのオプションを設定できます。
# .rspec_parallel --format progress --format ParallelTests::RSpec::FailuresLogger --out tmp/failing_specs.log
これには失敗したテストがログに残ります。標準出力と同じものです。
ここから、再度テストを実行しましょう。
$ grep rspec tmp/failing_specs.log | awk '{print $2}' | xargs bundle exec rspec /Users/osada/projects/mugenup/workstation/vendor/bundle/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in `load': cannot load such file -- /Users/osada/projects/mugenup/workstation/spec/views/projects/edit.html.erb_spec.rb:34 (LoadError)
あら、失敗してしまいました。なぜでしょうか?
実際にファイルを開いてみたところ、ascii color code
が含まれていました。
たしかに、これでは、rspec
が通りませんね。
といっても--color
を消してしまうのも、嬉しくありません。
そこで、正規表現を使って、抜き出すことにしました。
- rspec から始まり、「ドット、スラッシュ、コロン、英数字とアンダースコア」で構成される文字列を抽出する
$ ruby -ne 'puts $1 if /(?<=rspec )([\.\/:\w]+)/' tmp/failing_specs.log | xargs bundle exec rspec
これで、失敗したテストのみ、再実行することができます。
rake task
にするべきか?とも思ったのですが、さらに実行を遅らせてしまいそうなので、
alias
の登録だけにしておきました。
こうゆうとき、どういう技を使えばいいか、わからないの
ruby
でワンライナーを書くのは、少し大変ですね。
RuntimeLogger
を使って、テストのグループを平均化する
さて、paralell_tests
はデフォルトでは、ファイルサイズ順に均等化します。
ファイルサイズの大きい順に並べて、4つのグループに順番に追加していくわけです。
RuntimeLogger
を使うことで、テストにかかった時間で、均等化することができます。
--format progress --format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
tmp/parallel_runtime_rspec.log
には、テストファイル名と、その実行時間が記録されています。
[feature/parallel stash]~/projects/mugenup/workstation: tail tmp/parallel_runtime_rspec.log …… ./spec/views/sub_projects/show.html.erb_spec.rb:2.261982 ./spec/views/projects/show.html.erb_spec.rb:2.806851
テストのグループ分けのとき、with_runtime_info
メソッドが呼ばれます。
runtime_log
つまりtmp/parallel_runtime_rspec
があるとき、かつ、
今回のtests
ファイル群が、このログの中に含まれていると考えられるときに、
このログを使ってグループ分けします。
puts "Using recorded test runtime"
が出力されれば、成功です。
def with_runtime_info(tests) lines = File.read(runtime_log).split("\n") rescue [] # use recorded test runtime if we got enough data if lines.size * 1.5 > tests.size puts "Using recorded test runtime" times = Hash.new(1) lines.each do |line| test, time = line.split(":") next unless test and time times[File.expand_path(test)] = time.to_f end tests.sort.map{|test| [test, times[File.expand_path(test)]] } else # use file sizes tests.sort.map{|test| [test, File.stat(test).size] } end end
実際に、使用したときと、使用していないときを比較してみます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Finished in 2 minutes 19.6 seconds 78 examples, 0 failures Finished in 3 minutes 19.1 seconds 124 examples, 0 failures Finished in 3 minutes 10.7 seconds 116 examples, 0 failures Finished in 3 minutes 13.1 seconds 126 examples, 0 failures
使用していないときの処理時間は2:19.6 〜 3:19.1
であり、60
秒以上のズレがあります。
一方、runtime_log
を使用したときは下記です。
Using recorded test runtime
が表示されていることが確認できます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Using recorded test runtime Finished in 3 minutes 7.8 seconds 102 examples, 0 failures Finished in 2 minutes 58.3 seconds 108 examples, 0 failures Finished in 3 minutes 10.2 seconds 123 examples, 0 failures Finished in 3 minutes 11.8 seconds 111 examples, 0 failures
2:58.3 〜 3:11.8
であり、13
秒ほどのズレでした。
残念ながら、こちらも大きなスピードアップは感じられませんでしたが、 速くなったよ、という方はご一報いただけると嬉しいです。
まとめ
parallel_tests
とspring
の使い方、いかがだったでしょうか?
- PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" を設定して、
paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
弊社でもテストが遅いということが、問題になっており、日々改善に勤しんでおります。
こんなやり方あるよ!という情報をお持ちの方、いらっしゃいましたら、 ご教授いただけますと幸いです。
よろしくお願いします!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
【探検】Railsカラム更新のメソッド1
初めに
皆さん、初めまして。そして、明けましておめでとうございます。株式会社MUGENUP 開発部の奥田です。 今回、初めて技術ブログを書くことになりました。よろしくお願い致します。
何回かに分けてRailsのカラム更新メソッドについて書いていき、今回はupdate_attribute
とupdate_attributes
について記述します。2つのメソッドは私の環境だとactiverecord-3.2.16/lib/active_record/persistence.rb
にありました。以下の表は簡単なまとめです。
メソッド名 | validation | callback | 更新カラム数 |
---|---|---|---|
update_attribute |
なし | あり | 1つだけ |
update_attributes |
あり | あり | 複数 |
それではそれぞれのメソッドのソースコードを探検してみましょう〜!!
ActiveRecord::Persistence#update_attribute
# Updates a single attribute and saves the record. # This is especially useful for boolean flags on existing records. Also note that # # * Validation is skipped. # * Callbacks are invoked. # * updated_at/updated_on column is updated if that column is available. # * Updates all the attributes that are dirty in this object. # def update_attribute(name, value) name = name.to_s raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) send("#{name}=", value) save(:validate => false) end
save(:validate => false)
となっているので、validationなしということがわかります。Callbackを起こすは英語でinvokeと表現するのですね〜。updated_at
またはupdated_on
は更新するとわざわざ記述しているので、更新しないメソッドもあるのかもしれません。驚いたのはsend("#{name}=", value)
の部分で、sendメソッドはカラム名=
をメソッドと認識することです。sendメソッドを深堀りするのも面白そうだと思いました。
ActiveRecord::Persistence#update_attributes
# Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. # # When updating model attributes, mass-assignment security protection is respected. # If no +:as+ option is supplied then the +:default+ role will be used. # If you want to bypass the protection given by +attr_protected+ and # +attr_accessible+ then you can do so using the +:without_protection+ option. # def update_attributes(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do self.assign_attributes(attributes, options) save end end
update_attributes
にはoptions
があるんですね〜。:without_protection
とoptions
に入れることでmass-assignmentのprotectionチェックなしにできることを初めて知りました。assign_attributes
の中も見たい気持ちはありますが、今後のネタとして取っておきます(笑)。コメントアウトで強調したいところを+
で囲っていますが、私の知らない記法でした。Markdown記法ではないようなので、要調査です。
番外編 ActiveRecord::Persistence#update_attributes!
#update_attributes
はsave
を呼んでいるのでカラム更新の失敗時に戻り値としてfalse
を返します。そして、#update_attribute
に!
をつけた#update_attributes!
の場合はカラム更新の失敗時に例外を返しますが、その中身はどうなっているのでしょう?save
が呼ばれて、!
がついてるということは・・・?
# Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. def update_attributes!(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do self.assign_attributes(attributes, options) save! end end
予想通りですが、save!
が呼ばれていました〜。
最後に
簡単にではありましたが、Railsのカラム更新メソッドについて見てきました。この記事を読んで初めて知ったことがあれば幸いです。他にもカラム更新のメソッドはあるので、私の次回の記事でもカラム更新のメソッドを探検したいと思います。
宣伝
MUGENUP では、Rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly