ラベル ruby の投稿を表示しています。 すべての投稿を表示
ラベル ruby の投稿を表示しています。 すべての投稿を表示

2016/02/03

FactoryGirlを使用したDeviseのテストでLinuxとMacでのテスト結果が異なった話

tail -f pinzo.log: Deviseの有効期限設定をテストする で書いた有効期限検証のコードで不思議な事が起こった。Macでは正常にパスするのだが、Amazon Linux上ではパスしない。

ミニマムなコードで示すとこんな感じ

で、こんなデバッグコードを仕込んで実行してみた

Macで実行するとこんな感じ

[test-code][sent_at] to_s: 2016-02-01 23:51:31 +0900, to_i: 1454338291, usec: 197593, subsec: 197593/1000000
[test-code][added]   to_s: 2016-02-11 23:51:31 +0900, to_i: 1456930291, usec: 197593, subsec: 197593/1000000
[devise][now]     to_s: 2016-02-11 23:51:31 +0900, to_i: 1456930291, usec: 197593, subsec: 197593/1000000
[devise][sent_at] to_s: 2016-02-01 23:51:31 +0900, to_i: 1454338291, usec: 197593, subsec: 197593/1000000
[devise][added]   to_s: 2016-02-11 23:51:31 +0900, to_i: 1456930291, usec: 197593, subsec: 197593/1000000
[devise] now > added: false
[devise] now.subsec > added.subsec: false

Amazon Linuxで実行するとこんな感じ

[test-code][sent_at] to_s: 2016-02-01 23:53:20 +0900, to_i: 1454338400, usec: 369658, subsec: 369658841/1000000000
[test-code][added]   to_s: 2016-02-11 23:53:20 +0900, to_i: 1456930400, usec: 369658, subsec: 369658841/1000000000
[devise][now]     to_s: 2016-02-11 23:53:20 +0900, to_i: 1456930400, usec: 369658, subsec: 369658841/1000000000
[devise][sent_at] to_s: 2016-02-01 23:53:20 +0900, to_i: 1454338400, usec: 369658, subsec: 184829/500000
[devise][added]   to_s: 2016-02-11 23:53:20 +0900, to_i: 1456930400, usec: 369658, subsec: 184829/500000
[devise] now > added: true
[devise] now.subsec > added.subsec: true

usecまでは同じなのだが、subsec をみると Amazon Linux では、桁落ちが発生している。

ためしにそれぞれで Time.now.subsec としてみる。

# mac
$ ruby -e 'puts Time.now.subsec'
276987/1000000

# Amazon Linux
$ ruby -e 'puts Time.now.subsec'
704462593/1000000000

つまりこういうことが起こっている

  1. OSにより Time#subsec が返す精度が異なる
  2. FactoryGirlが create で返すのは、コードで指定された値を格納したオブジェクトであり、DBに登録後取得したものではないので confirmation_sent_at はそのままの精度である。つまり、スタブで突っ込んでいるのは元の精度
  3. DB(sqlite)に格納するとき、DBの型に合わせて精度の桁落ちが発生する
  4. 実際に処理されるときは、DBから取得したデータなので桁落ちが発生したデータになる
  5. DB格納前の元の精度の値と、DBから取得した桁落ちした値を比較しているため期限を超えたことになってしまう
  6. Mac上ではたまたま、Time#subsecの精度がsqliteの精度と同じだったため桁落ちが発生せず正常に動作する

これを避けるためには、スタブに突っ込む値をDBから取得したものにすれば良さそうだ。たとえばこんな感じ

ちなみに Mac でも、桁落ちを引き起こすことは可能で、こんな感じで usec を超えた範囲を指定すれば桁落ちさせられる。

あー疲れた、腹減った

2016/01/30

Deviseで複数モデルを利用した場合、ログアウトすると全てのスコープでログアウトされてしまう

管理者とユーザーでのログイン機能が存在し、別モデルで管理したい場合、Devise を使うとscoped_viewsをtrueにすれば簡単に実装できる。(参考:Ruby - Railsでdeviseひとつで複数モデルを管理しよう - Qiita

しかし、デフォルトの場合 Devise のログアウトは全てのスコープでログアウトしてしまう。つまり管理者とユーザーでログインしていた場合、管理者としてログアウトすると同時にユーザーとしてもログアウトしてしまう

これを防ぎたい場合、config/initializers/devise.rb にて config.sign_out_all_scopes = false としてやればよい。

初期状態ではこの設定はコメントアウトされているので、コメントアウトを外しfalseに設定するだけで、スコープごとのサインアウトが実装できる。

テストコード込みのサンプルコードはこんな感じ。

2016/01/18

Rails の TimeWithZone を JavaScript で読み込めるフォーマットで出力する

JavaScript で new Date(text)Date.parse(text) した時に読み込める形で TimeWithZone の値を出力したい。

そのまま to_s すると 2015-01-18 12:34:56 +0900 となるが、これでは Chrome でしか読み込めない。

2015/01/18 12:34:56 +0900 という形式ならば Chrome, IE, Firefox で読み込むことができた

もしくは、せっかく to_json メソッドがあるんだからこれを利用すると安心。しかし余計なダブルクォートは取り除く必要がある。あと、人が見た時は先述のやり方のほうがぱっと見で理解しやすいと思う

to_json 経由だとミリ秒まで含まれているから完全に等価ではないけど、そこらへんは要件しだいということで

まあ、色々やり方はあるので好きなやり方を使えばいいと思う。

2016/01/10

EC2(Amazon Linux)上のRailsアプリにQiita::Markdownをインストール

インストール

Qiita::Markdownはシンタックスハイライトに Pygments を使用しているのでインストールしておく(sudo pip install pygments)

そして、おもむろに Gemfile に gem 'qiita-markdown' として bundle install してみる。

Installing charlock_holmes 0.7.3 with native extensions

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    /usr/bin/ruby2.2 -r ./siteconf20160110-30152-1igbivj.rb extconf.rb
checking for main() in -licui18n... no
which: no brew in (/home/ec2-user/bin:/home/ec2-user/.rbenv/bin:/home/ec2-user/bin:/home/ec2-user/.rbenv/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin:/opt/aws/bin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin:/opt/aws/bin)
checking for main() in -licui18n... no


***************************************************************************************
*********** icu required (brew install icu4c or apt-get install libicu-dev) ***********
***************************************************************************************
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib64
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/usr/bin/$(RUBY_BASE_NAME)2.2
        --with-icu-dir
        --without-icu-dir
        --with-icu-include
        --without-icu-include=${icu-dir}/include
        --with-icu-lib
        --without-icu-lib=${icu-dir}/lib64
        --with-icui18nlib
        --without-icui18nlib
        --with-icui18nlib
        --without-icui18nlib

extconf failed, exit code 1

Gem files will remain installed in /home/ec2-user/rails_app/vendor/bundle/ruby/2.2/gems/charlock_holmes-0.7.3 for inspection.
Results logged to /home/ec2-user/rails_app/vendor/bundle/ruby/2.2/extensions/x86_64-linux/2.2/charlock_holmes-0.7.3/gem_make.out
Installing rugged 0.24.0b11 with native extensions

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    /usr/bin/ruby2.2 -r ./siteconf20160110-30152-1y0cp2j.rb extconf.rb
checking for gmake... yes
checking for cmake... no
ERROR: CMake is required to build Rugged.
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib64
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/usr/bin/$(RUBY_BASE_NAME)2.2
        --use-system-libraries

extconf failed, exit code 1

Gem files will remain installed in /home/ec2-user/rails_app/vendor/bundle/ruby/2.2/gems/rugged-0.24.0b11 for inspection.
Results logged to /home/ec2-user/rails_app/vendor/bundle/ruby/2.2/extensions/x86_64-linux/2.2/rugged-0.24.0b11/gem_make.out

とまあ、2つのエラーが出た。Amazon Linuxは yum だからlibicu-dev じゃなくて libicu-devel にすればOKかな?

sudo yum install -y libicu-devel cmake して bundle install したらインストールは成功したようだ

ハイライト

インストールは成功したのでこんなコードを書いて使えば Markdown に変換された。ばっちりばっちり

module ApplicationHelper
  def markdown(source)
    Qiita::Markdown::Processor.new.call(source)[:output].to_s.html_safe
  end
end

変換はばっちりだがシンタックスハイライトがされていない。いや、正確にはスタイルシートがない。
richleland/pygments-css あたりでお好みのものを利用させてもらおう

絵文字

シンタックスハイライトも完了した。次は絵文字である。
gemoji | RubyGems.org | your community gem host がインストールされているはずなので、Rakefile に下記の行を加える。

load 'tasks/emoji.rake'
あとは bundle exec rake emoji を実行すれば /public/images/emoji 以下に画像はファイルが配置される。
これだけで絵文字が表示できるようになる。bundlerake で導入できるので /public/images/emoji.gitignore に加えておいてもいいかもしれない。大量に画像ファイルをリポジトリに登録したくないもんね

2016/01/09

オブジェクトが存在するときにだけ処理をする

みんな大好き Object#tap

でもこんなソースはあまり書きたくない。

foo.bar.baz.tap do |baz|
  do_something(baz) if baz
end

tap_if とかあれば便利じゃない?

foo.bar.baz.tap_if do |baz|
  do_something(baz)
end

# もしくは
foo.bar.tap_if(:baz) do |baz|
  do_something(baz)
end

似たようなことを考える人はいるものですでにある。→ tap-if | RubyGems.org | your community gem host

そして Rails やる上では try で事足りることがわかった。ブロック取れるのか。

foo.bar.baz.try do |baz|
  do_something(baz)
end

# もしくは
foo.bar.try(:baz) do |baz|
  do_something(baz)
end

結論:Object#try サイコー

2015/12/16

Railsのテストで定数のスタブが欲しい場合

Railsの ActiveSupport::TestCase は minitest を使っていて、minitest/mock には定数をスタブ化する機能はない。
adammck/minitest-stub-const ってのはあるけど、外部ライブラリ入れるほどじゃないよねって場合があるかもしれない。
そんな時はこんな感じのコードでいいんじゃないかな?
こういうトリッキーなのは最終手段的なもので、定数を環境ごとに設定ファイルに定義できるような gem を使うべきだとは思う。
やってみたらできたというお話

assert_change

ユニットテスト時によく使うメソッドの1つに assert_difference がありますね。
でもこれ差分を取るので数値を返す式にしか使えません。使おうとしたら + なんて演算子ないよってエラーが出ます。
仕方なしにこんなテストコードを書いてません?
test 'ユーザーが確認済みになること' do
  assert_not user.confirmed?
  # ここに承認処理
  assert user.confirm?
end
それならば、こんなメソッドを test_helper あたりに定義しておいて
test 'ユーザーが確認済みになること' do
  assert_change 'user.confirmed?' do
    # ここに承認処理
  end
end

# 実際の値を気にしなくていいならこんな使い方も
test '保存したら updated_at が更新される' do
  assert_change 'user.updated_at' do
    user.save
  end
end
と書くとちょっとスッキリしますね。コードはほぼほぼ assert_difference のパクリです。
expression の指定方法は必要になったら広げる感じでいいかな。

2015/12/10

Deviseの有効期限設定をテストする

設定ファイル config/initializers/devise.rb はこんな感じとする。
Devise.setup do |config|
  # 省略
  config.reset_password_within = 30.minutes
  config.confirm_within = 30.days
  # 省略
end
テストコードはこんな感じでかけた。 Recoverable では reset_password_sent_at reset_password_within.ago を比較している。
ActiveSupport::Duration#agoまで遡ると初期値である Time.current に対して演算していたので、最初は Time.stub(:current, ... のようにしていた。
しかし、Confirmable では、合算値を Time.now と比較している。そのため、Time.current は通らないのでこのやり方ではだめだった。
結局は Time.now をスタブ化したらよかったわけだが、こういう同質的な処理は同じ書き方をしてほしいなーとおもった昼下がり。

2015/12/09

Deviseにてシステムからユーザーのメールアドレスを変更してもメールを送信しない

Devise を使用していて、手順を踏まずにシステムにてメールアドレスを変更すると保存時にメールが送信されてしまう。
メールを送信したくない場合は skip_reconfirmation! を使用する。 なお、新規作成時に確認手順を飛ばしたい場合は skip_confirmation! を使う。

2015/12/04

ArelでのOR検索ついでにごにょごにょいじってみた

1つのキーワードで複数テーブルの複数カラムをあいまい検索ってよくある話ですね。
Arelを使って OR の LIKE 検索って冗長になりがちだけどそこそこパターン化出来そうだなーとつらつらとコード書いてみた。 実際は SimpleFinder までやると適用できるパターンが限定されるので、04_finally みたいなところが落とし所な気もする。
人によっては 03_inject_with_symbol ぐらいが一番可読性がいいって意見もありそう。
Arelを使って OR や LIKE をするメリットは scope にして merge した時に壊れないって記述をよく見ますが、こういう風に動的に対応箇所を増やせるように持っていくのも楽というのもメリットですね。文字列で where 内を書いていたらなかなかこうはできない
あ、こんな処理を他にもたくさん書かなければいけない場合は Squeel 入れたほうがいいと思います。

全然関係ないけど、gist って編集時はインスタンス変数に色つけてくれるのに閲覧時には色つけてくれないの何でだろ

[追記] たまたま Ruby on Rails Advent Calendar 2015 が空いてたので飛び入りしました。

2015/11/16

RubyとRailsとminitestのマルチバージョンでのCI環境の話

pinzolo/rails-flog の Rails4.2でのテストが落ちてて、でも動いてるしーで放置してしまってた。
んで、ちゃんとしないとと思って調べてみたら、テストを壊していたのはテストコード(と自分の思い込み)だった。
具体的には mocha というモックライブラリを使用していたんだけど、スタブ作ったらテストケースをまたいで存在し続けるのを、テストケースセーフだと思い込んでいた。以前のバージョンではシーケンシャルにテストが行われていたけど、ランダムになったのでテストが落ちるようになった。
どうりでローカルでテストすると時々によってエラーの数が違うわけだ。
んで、minitest にもモック・スタブ機能があることを知り、ブロックでの影響範囲を限定できるのでこっちのほうがいいじゃん!!と mocha を削除し minitest のスタブに統一した。
ここまで前置き

ローカルでテスト通ったーと喜んで push すると travis でコケる
Ruby1.9&Rails3.2ではstubなんてないよと怒られ、Ruby2.2&Rails3.2では 'minitest/autorun'がないと怒られた。 Ruby1.9に同梱されている minitest には minitest/mock がまだ存在していなかったらしい。

調べてみると明示的に最新の minitest を使うのが良いらしく、gemspec に足してみた。
するとこんどは Rails3.2で軒並みコケた
警告も出てるし、いくつか試行錯誤してRails3.2では minitest のバージョンを 4.7.x となるように指定したら、今度はRuby2.2の時だけtest-unitが足りないと言われる。
そういや削除されたんだったっけ。
これでようやく全部通った。なまじ同梱されてるから環境の差分を調整するのはしんどい。

2015/10/30

自作gemのテストを test-unit で行う

久しぶりに自作 gem でも作るかと bundle gem -t とすると自動的に spec フォルダと .rspec が作成された。
いやいや、test-unit 使いたいんだってば。
仕方ないので自分で設定することに
gemspec に spec.add_development_dependency 'test-unit' を追加し、Rakefile にこんな感じに設定を書くだけだった。

2015/10/27

自作クラスの配列同士で引き算を正しく動作させるには

自作の配列で引き算を行おうとしたら、==のオーバーライドだけでは足りず、eql?hashのオーバーライドが必要だった。

2014/08/29

アソシエーションで定義された属性の名前を取得する

テーブル列である属性を取得するには ActiveRecord::Base#attribute_names で取得できるけど、has_many などで定義された属性名は取得できない。
アソシエーションで定義された属性は ActiveRecord::Base#reflections 経由で取得できる。 ActiveRecord::Base#attribute_names は文字列の配列を返すけど、こっちはシンボルの配列を返す。

2014/08/15

rails-froutes という gem をリリースした

Summary

Rails の routes を peco りたかったんで rails-froutes という gem を作りました。
Github: pinzolo/rails-froutes
RubyGems.org: rails-froutes

How?

インストールして、rake routes FILL_NAME=yesと呼び出すと、従来は
    blog_posts GET    /blogs/:blog_id/posts(.:format)          posts#index
               POST   /blogs/:blog_id/posts(.:format)          posts#create
 new_blog_post GET    /blogs/:blog_id/posts/new(.:format)      posts#new
edit_blog_post GET    /blogs/:blog_id/posts/:id/edit(.:format) posts#edit
     blog_post GET    /blogs/:blog_id/posts/:id(.:format)      posts#show
               PUT    /blogs/:blog_id/posts/:id(.:format)      posts#update
               DELETE /blogs/:blog_id/posts/:id(.:format)      posts#destroy
         blogs GET    /blogs(.:format)                         blogs#index
               POST   /blogs(.:format)                         blogs#create
      new_blog GET    /blogs/new(.:format)                     blogs#new
     edit_blog GET    /blogs/:id/edit(.:format)                blogs#edit
          blog GET    /blogs/:id(.:format)                     blogs#show
               PUT    /blogs/:id(.:format)                     blogs#update
               DELETE /blogs/:id(.:format)                     blogs#destroy
こうだった rake routes の結果が、
    blog_posts GET    /blogs/:blog_id/posts(.:format)          posts#index
    blog_posts POST   /blogs/:blog_id/posts(.:format)          posts#create
 new_blog_post GET    /blogs/:blog_id/posts/new(.:format)      posts#new
edit_blog_post GET    /blogs/:blog_id/posts/:id/edit(.:format) posts#edit
     blog_post GET    /blogs/:blog_id/posts/:id(.:format)      posts#show
     blog_post PUT    /blogs/:blog_id/posts/:id(.:format)      posts#update
     blog_post DELETE /blogs/:blog_id/posts/:id(.:format)      posts#destroy
         blogs GET    /blogs(.:format)                         blogs#index
         blogs POST   /blogs(.:format)                         blogs#create
      new_blog GET    /blogs/new(.:format)                     blogs#new
     edit_blog GET    /blogs/:id/edit(.:format)                blogs#edit
          blog GET    /blogs/:id(.:format)                     blogs#show
          blog PUT    /blogs/:id(.:format)                     blogs#update
          blog DELETE /blogs/:id(.:format)                     blogs#destroy
こうなります。

Why?

ほら routes.rb みれば大体わかるとはいえ、ちょっと規模が大きくなると **_path, とか **_url を頭の中でごにょごにょするのはタルいわけです。
% rake routes | peco | awk '{print $1}' | pbcopy
とかできたら便利じゃないですかね?
でも、通常の rake routes だと名前があったりなかったりで欲しいものが取れないので、こんな gem を作りました。
まあ現実だと rake routes はもっさりなので
% rake routes FILL_NAME=yes > .routes
ってあらかじめしておいて、
% cat .routes | peco | awk '{print $1}' | pbcopy
ってところですかね。
私はこれを proutes として .zshrc に登録しました。
guard で routes.rb 監視して、更新したら吐き出すようにしておくのもいいかもしれません。
そこまで routes.rb を頻繁にいじるかどうかはわかりませんが。

2014-08-16 追記:
改行が入るのは使いづらいのでコマンドを改良した。
% cat .routes | peco | awk '{print $1}' | tr -d '\n' | pbcopy

おまけ

こんなふうに出力するオプションって rake routes にないの?って思ってコード読んでたら、こんなの発見した。 いや〜知らんかった。

2014/08/11

Windowsで解凍できるZIPファイルをRubyで作成する

Windowsで解凍できるZIPファイルを Ruby で作る必要があったのでメモ。
利用したのは rubyzip/rubyzip の v1.1.6
概ね、rubyzip/README.md at master · rubyzip/rubyzip の通りなんだけど、Windows で解凍できるようにするには Shift_JIS に変換したパスで格納してあげる必要がある。
実際に使用する際には、外部から Shift_JIS か UTF8 かを選べるようにするのがいいかもね。

2014/08/03

Redmineプラグイン開発における ActiveModel 使用上の注意点

先のエントリ: tail -f pinzo.log: ActiveSupport::Concern の included と ClassMethods の順序 に関連してます。

Redmine のプラグインでちょっと凝ったことをしようとすると、ActiveModel を使いたくなることがある。
ActiveModel のメリットの一つに ActiveRecord と同様のバリデーションを手軽に定義できることがある。
しかし、Redmine 本体の locales/ja.yml には ActiveModel の設定は入っていないので、プラグインの locales/ja.yml に書くことになる。
その際、たいてい Redmine 本体の locales/ja.yml から ActiveRecord 用の設定をコピーしてくることになりがち。 こんな感じに。

ここまではいい。

問題は、ここからちょいと修正を入れてしまうことにある。
ちょっと文言をいじってしまうと ActiveModel のデフォルトの文言を上書きするということになる。
これを複数プラグインでやってしまった場合、さてあなたのプラグインではどんなエラーが表示されるだろう?
これらの辞書ファイルはマージされるので、ロード順や入れてあるプラグインの状態により変わってしまい想定できなくなる。
文言をテストで使用していたりしたら、テストが失敗してしまう。
それを避けるためにも、先のエントリで書いたようなテクニックを利用して、自分のプラグインの ActiveModel が使用するセクションを独立させてあげよう。
こんな感じに。(my_plugin 部分にはあなたのプラグインの一意な識別子を指定しましょう。)

それでは、今日もよい日曜開発を

2014/08/01

ActiveSupport::Concern の included と ClassMethods の順序

前置き

最初に書いておきます、Rails3.2系の記事です。
Rails4だと ActiveModel::Model を使うだろうから、サンプルコードはずいぶん変わると思います。

ActiveModel を利用したモデルで独自の i18n のセクションを利用したい

まあとある事情で、通常 activemodel.errors.messages.xxx な i18n のキーを my_model.errors.messages.xxx とかにしたかった。
そんな場合は i18n_scope をオーバーライドしてやるんだけど、全モデルクラスに書くのは嫌なので、インクルードするだけの共通モジュールを書くことに。

最初にこんなのを書いた

しかし、これではうまくいかない。

問題点

ActiveSupport::ConcernClassMethods モジュールを反映させてから included のブロックを実行するので、再度デフォルトで上書きしてしまう。

解決策

こんな風に alias_method_chain を使うなどして extend ActiveModel::Translation の後にオーバーライドしてやればいい。

2014/07/22

Railsにてポリモーフィック関連のサンプルコード

Rails のポリモーフィック関連って便利ですよね。
でも、関連の貼り方までは結構サンプルコードが転がっているんですが、データ保存部分まで含めたものがあまりないような気がしたので、どうするのがいいかなと悩みつつ書いてみた。

has_oneの場合

has_one のサンプルとして、備考のモデルで作ってみた。
コメントは結構複数の場合があるけど、備考はたいてい 1 つしか持たないだろう。 1つしかオブジェクトを持たないので、note 属性で値を直接やりとりしたかったのでこんな風に。
Notable を include すればもう備考書き放題。

has_manyの場合

こんどは定番のタグをサンプルにしてみた。 関連テーブルをポリモーフィック関連にするわけですな。

雑感

* xxx_typexxx_id は Rails に任せる。自分では処理しない。
* 関連を定義してくれる module を作っておくと後が便利だと思う。
* t.references :taggable, polymorphic: true とするサンプルが多いけど、null が入ることは設計上ありえないんだから null: false を付けておいたほうがいいと思う。
* save_note では保存のみ実行しているけど、note_store.note が nil の場合はデータサイズ的なことを考えると note_store.destroy としてもいいと思う。(集約されるテーブルなので) まあサンプルなのでシンプルにした。

2014/07/04

ActiveRecord の ! つきメソッドについて

ActiveRecord の更新系メソッドには、!つきのものとそうでないものがあって、一般的には例外を発生させるかどうかの違いという認識だと思う。
しかし!が付いているからといって例外を発生させるメソッドとは限らない。
属性値を変更するだけで、!がつくと同時に保存も実行するというパターンも存在する。 なので、「decrement で減らしているけど、万が一負数になってしまった場合、例外を発生させてトランザクションを失敗させる」場合(バリデーションでやれというのは置いといて)、@user.decrement!(:count) ではなくて @user.decrement(:count).save! と書かなければならない。

あと、 becomesbecomes! という STI(単一テーブル継承)用のメソッドもあるが、これはちょっと別枠ですね。
ちなみに becomes!becomes + type カラムの変更となっていて、変換後に保存するときに便利なのかな。あんまり STI を使ったことないけど。