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

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 を使うべきだとは思う。
やってみたらできたというお話

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/07

Rails3とRails4のhas_manyオプションの差分を吸収する

Rails3とRails4の違いの1つに has_many のオプションがあります。 Rails4では orderinclude が使えなくなってるんですね。

しかし、1つのソースでRails3とRails4に対応したい場合どうしようか? とまあこんな感じでどうでしょう?

2015/11/06

本番環境での secret_key_base 設定

本番環境に Rails4.2.4 のアプリケーションをデプロイするとこんなエラーが出た。
App 21863 stderr: [ 2015-11-06 12:37:20.3972 21881/0x007f1632a022e8(Worker 1) utils.rb:84 ]: *** Exception RuntimeError in Rack application object (Missing `secret_token` and `secret_key_base` for 'production' environment, set these value
s in `config/secrets.yml`) (process 21881, thread 0x007f1632a022e8(Worker 1)):
config/secrets.ymlによると、本番環境ではENV["SECRET_KEY_BASE"]を読み込むらしい。
でも、複数の Rails アプリを動かしているし、今後も増やすだろうからpassengerの運用ユーザーに対して設定する訳にはいかない。
ENV["FOO_SECRET_KEY_BASE"]のようにアプリごとに環境変数変えたらいいのかな?とおもって調べてみると、apacheの設定ファイルで SetEnv すればよいらしい。
# /etc/apache2/sites-available/foo-app.conf
<VirtualHost *:80>
  ServerName foo-app.mkt-sys.jp:80
  DocumentRoot /var/lib/rails/foo-app/public
  <Directory /var/lib/rails/foo-app/public/>
    SetEnv SECRET_KEY_BASE abcdefg...xyz       #←ココ
    Options FollowSymLinks
    AllowOverride None
    Require all granted
  </Directory>
  PassengerEnabled on
</VirtualHost>
設定するキーは bunde exec rake secret RAILS_ENV=production で出力されたものをコピペ。(多分 RAILS_ENV=productionはいらないだろうけど)

2014/08/29

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

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

2014/08/28

更新ないけど維持はしてるよ

少し前ですがRails4.0.9と4.1.5がリリースされたので、自作の Rails 関連の gem がちゃんと動くかな?と Travis の Job を動かしてみたら、まあ無事に全部通ったんです。
travis-ci.org に行って、Restart ボタンを押すだけの簡単なお仕事。
それはいいんだけど、Ruby や Rails 界隈では更新のない gem は使わない、使用を控える、同じ機能なら新しい方、開発が活発な方という指針が一部あって(そんな気がする)、せっかく最新バージョンでも動くのにバグフィックスや機能追加がない、つまり更新がないだけで使われない gem も出てくるわけです。
さすがにあまりないだろうけど、更新がないだけで後発の同じ機能を持つ車輪の再発明 gem の方を優先されたら作者としては悔しいわけです。動くのに。
でも「travis-ci.org に行って、Restart ボタンを押すだけの簡単なお仕事」だけだと、Github にも Rubygems.org にも維持してる証拠が残らないんですよね。
gem の選別してる時に、Github で最終更新日や README.md に載ってる CI のバッジでの最終ビルド結果は見ても、そこから CI のサイトで、最新の Rails や Ruby のバージョンでビルドしてることまで見るんだろうか?
自分は毎回そこまで見ていない気がするし、そうしてくれたら嬉しいけど、そこまでを分化としていくのは難しい気がする。
となると、やはり何らかの証拠を作者が残すしかない。

1. バージョン上げる

確実だし、Github にも Rubygems.org にも証拠が残る。
自分の基本的なバージョン指針は「0.0.1 が開発中、たまに 0.5.x とか 0.9.x で Pre-release や RC を経て、最初の想定していた機能ができたら、1.0.0 としてリリースし、その後は、機能追加でマイナーバージョンをあげて、バグフィックスなどの機能を追加しない修正はリビジョン(tiny)を上げる」というものなんだけど、この場合リビジョン上げるていいのかどうか悩む。
ソースコードに変更がないのにいたずらにバージョンを上げるべきじゃないと思うし、バージョンあげてしまうと利用者全員に自分と同じような作業が発生する可能性がある。なのでバージョンは上げたくない。

2. CIを空コミットで回す

Github には証拠が残る。Rubygems.org はそのまんま。
コミットメッセージに「Railsバージョンアップによる確認のための空コミットだよ」みたいなコメントを残しておけばわかりやすい。

3. README.mdにログを残す

Github には証拠が残る。Rubygems.org はそのまんま。
基本的に Changelog は書いてるので、そこに追記するか、Change してねーだろってんなら Activity log みたいなセクションを設けて記述する。
Github まで行って README.md 読まない奴はあまりおらんだろうという想定。
ファイルの最終更新まで変更されるし自動的にコミットも伴うから、空コミットより訪問者にわかりやすい。
しかし、普通そういうログは確認後に残すものだから Travis 回して確認 → ログ書いて commit, push はめんどくさい。

まあ、普通に考えたら 2 かな?というわけで、空コミットするお仕事に戻ります。

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/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 を使ったことないけど。

2014/06/13

Redmine のプラグインを RSpec3 でテスト出来ない

pinzolo/redmine_persist_wfmt を RSpec3 に対応しようとしてみたが実行すら出来なかった。
/Users/pinzolo/projects/redmine/persist_wfmt/redmine-2.5.1/vendor/bundle/ruby/2.1.0/gems/rspec-support-3.0.0/lib/rspec/support/version_checker.rb:28:in `raise_too_low_error': You are using capybara 2.1.0. RSpec requires version >= 2.2.0. 
(RSpec::Support::LibraryVersionTooLowError)
        from /Users/pinzolo/projects/redmine/persist_wfmt/redmine-2.5.1/vendor/bundle/ruby/2.1.0/gems/rspec-support-3.0.0/lib/rspec/support/version_checker.rb:18:in `check_version!'
        from /Users/pinzolo/projects/redmine/persist_wfmt/redmine-2.5.1/vendor/bundle/ruby/2.1.0/gems/rspec-rails-3.0.1/lib/rspec/rails/vendor/capybara.rb:13:in `'
        from /Users/pinzolo/projects/redmine/persist_wfmt/redmine-2.5.1/vendor/bundle/ruby/2.1.0/gems/activesupport-3.2.17/lib/active_support/dependencies.rb:251:in `require'
        from /Users/pinzolo/projects/redmine/persist_wfmt/redmine-2.5.1/vendor/bundle/ruby/2.1.0/gems/activesupport-3.2.17/lib/active_support/dependencies.rb:251:in `block in require'
        from /Users/pinzolo/projects/redmine/persist_wfmt/redmine-2.5.1/vendor/bundle/ruby/2.1.0/gems/activesupport-3.2.17/lib/active_support/dependencies.rb:236:in `load_dependency'
        from /Users/pinzolo/projects/redmine/persist_wfmt/redmine-2.5.1/vendor/bundle/ruby/2.1.0/gems/activesupport-3.2.17/lib/active_support/dependencies.rb:251:in `require'
        .... and more
Redmine 本体の Gemfile で gem "capybara", "~> 2.1.0" なのだが、RSpec3 で Capybara を使用するには 2.2.0 以上を要求される。
このエラーは実行時でなく事前チェックで投げられているようで、 Capybara をプラグインのテストで使用していなくても出る。
さすがに本体のバージョンを無視するわけにはいかないので、当分は gem "rspec", "~> 2.14.0" で行こうと思う。
というか、やっぱりこういうのを避けるためには ActiveSupport::TestCase で書いたほうがいいんだろうな。

2014/06/10

rails-render_hooks という gem を作った

Redmine のプラグインなんかを作っていると、アクションの処理が終了してレンダリングされる前にちょっと手を入れたくなる。
実際そういう需要はそれなりにあるようで、'rails before_render' とかで検索するとチョロチョロ出てくる。
しかし、Rails3.2〜Rails4.1で動くぐらいにメンテされている gem がない。
  • shell/rails3_before_render
    Rails4 で動かないし、Rails4で動くように修正されたプルリクエストも放置
  • nilesh/before_render
    上記から fork して Rails4 で動くけど、逆に Rails3.2 で動かない
  • codepodu/rails3_before_render
    上記のプルリクエスト元なのでどっちでも動くけどRails4.1だとテスト落ちる。そもそも Travisこけてるけど放置。プルリクエスト用だから gem になってない
というわけで、自分で作った ついでに、before だけでなく、after と around も足しておいた。使うかどうかは知らないけど。