【Rails】【RSpec】【shoulda-matchers】validate_uniqueness_ofのscoped_toではまったときのまとめ
目的
shoulda-matchersでモデルのバリデーションのテストを書いていたときに、Railsのアトリビュートメソッドのキャッシュの動きで若干はまったため、簡単にまとめる。
shoulda-matchersの簡単な導入
shoulda-matchersはgemで、RSpecやMinitestにvalidate_presence_of
などのワンライナーのテストのシンタックスを追加するもの。
詳しい話、やその他のテストメソッドに関しては、本家のreadme参照。(知らない機能がたくさんある!)
Railsのアトリビュートメソッドのキャッシュ機能
メタプログラミングRubyを読んでいただいた方はわかるかもしれないが、Rails4系・5系のコードはメソッドの定義をキャッシュすることでパフォーマンスの向上を図っている。
詳しい実装には触れないが、具体的には以下のような動作になる(Rails 5.0.0.1, Ruby 2.3.1)
2.3.1 (main):0 > User.method_defined?(:name) => false 2.3.1 (main):0 > User.new => #<User id: nil, name: nil, role_id: nil, created_at: nil, updated_at: nil, email: ""> 2.3.1 (main):0 > User.method_defined?(:name) => true
shoulda-matchersのvalidate_uniqueness_ofメソッドのscoped_toチェーン
モデルでのバリデーションを以下のように書く
class Phone < ActiveRecord::Base validates :phone, uniqueness: true end
shoulda-matchersでテストを書くと以下のように書ける
require 'rails_helper' describe Phone do it { is_expected.to validate_uniqueness_of(:phone) } end
さらに、複数カラムでユニークになるようにバリデーションをすることもでき
class Phone < ActiveRecord::Base validates :phone, uniqueness: { scope: :contact_id } end
shoulda-matchersで書くとこうなる
require 'rails_helper' describe Phone do it { is_expected.to validate_uniqueness_of(:phone).scoped_to(:contact_id) } end
しかし、上記のテストは(他のテストにもよるが、)コケる。
なぜテストが失敗するのか
以下のようなメッセージが出る
:contact_id does not seem to be an attribute on Phone.
ん?カラムあるよ?
これでしばらく進まなかった。
実際は、scoped_toで指定されたカラムは、以下のようにshoulda-matchersのなかでmethod_defined?
を使ってチェックしている。
def attribute_present_on_model? model.method_defined?("#{attribute}=") || model.columns_hash.key?(attribute.to_s) end
試しにbinding.pry
でとめ、contact_id=
を実行してみてからテストを流すとうまくいった。
メソッドのキャッシュのタイミングはバージョンによって異なるので、最新版であれば大丈夫かもしれません。
まとめ
感想
- gemのソースを読むのはなかなか楽しい。
- 今回のがバグなのかわからないが、最新版を使ってバグを踏んで、PR送ろう!
【Rails】権限管理のgem「pundit」を使ってみたときのまとめ
目的
punditというgemを使う際に「Rubyist Magazine - 権限管理のgem、Punditの紹介」を参考に作ったときのまとめ。
railsにapp/policies
ディレクトリって何を入れるのかというところを前回調べたのですが、代表的なものとしてpundit
というgemを使うと出て来るというのが代表的な例なようなので、使ってみました。
【Rails】policiesディレクトリの使い方 - ITの勉強をいろいろやってみたブログ
るびまを参考にして作ったときに考えていたこと等をまとめてみます。
まとめ
ApplicationPolicyのサブクラスの初期化時には、recordではなくclassを渡す
rails g pundit:install
を実行すると、app/policies/application_policy.rb
が作られるのだが、初期実装は以下のようになっている。
class ApplicationPolicy # ... def initialize(user, record) @user = user @record = record end # ... end
これだと、レコードを渡すことで「自分のレコードは更新できる。他の人のレコードは閲覧だけ。」みたいなレコード単位の権限をつけられる。
それはそれで便利なのだけど、社内用の管理画面等はレコード単位ではなくクラス・モデル単位での権限がついていれば十分。なので、以下のようにクラスを受け付ける想定にしておいて、その他も適宜書き換える。(モンキーパッチを当てる必要はなし)
class ApplicationPolicy # ... def initialize(user, klass) @user = user @klass = klass end # ... end
ApplicationController
には以下のようなメソッドを準備しておく。
class ApplicationController < ActionController::Base # ... def pundit_auth authorize controller_name.classify.constantize end end
UsersController
でbefore_action
を呼ぶ
class UsersController < ApplicationController before_action :pundit_auth # ... end
すっきり。
cancancan vs punditはRSpec vs minitestと似ている気がする
cancancanはAbility
というクラスを定義して、その中にDSLをわーっと書いて権限設定を定義します。
それに対しpunditはピュアなObjectを継承したpureなRubyのクラスで定義し、コントローラで呼ぶためのメソッドを提供しているだけです。
このような観点の比較はRSpec vs minitestのときの議論と似ているかなと思っています。
MinitestとRSpec、FixturesとFactoryGirlの良いところ悪いところをコードを書いて比較してみた - give IT a try
RSpec vs minitestのときはテストコードの話なのですが、権限は実際のアプリケーションの設計に関わってくる所なので、妥協したりプラグインに引っ張られた設計になるのは避けたいところです。
複雑な仕様がある場合にはcancancanにモンッキーパッチをあてる等して使うより、自由に書けるpunditを使っておくのも悪くない選択かなと思いました。
TODO
- cancancanで同じことをやる
- 認証はdeviceを使ってやったが、自前で実装してみる
【Rails】policiesディレクトリの使い方
目的
railsにapp/policies
ディレクトリを作るときに、中身には何を入れるのかまとめる。
パーフェクトRuby on Railsの9章を読んでいて、コールバック・バリデーションをモデルから分離して、独立したクラスを作る方法を学びました。
- 作者: すがわらまさのり,前島真一,近藤宇智朗,橋立友宏
- 出版社/メーカー: 技術評論社
- 発売日: 2014/06/06
- メディア: 大型本
- この商品を含むブログ (8件) を見る
callbacks
、validators
という他にも、app配下に置いておくものとしてpolicies
というディレクトリが有るようです。ただし、調べてみると
policies
ディレクトリの使い方は、そこまで一般解があるように思えなかったので、一旦まとめておいて今後検討する材料にしようかと思いました。
policiesディレクトリの使い方概要
- punditを使う(わりと一般的?)
- cancancanのavility.rbを、modelに入れずにpoliciesに入れる(avilitiesでもいいと思う)
- 特にgem等は使わず自分でピュアRubyのオブジェクトを定義して入れる
- policyっていうgemを使う(3をちょっと便利にした感じのもの。メンテされてる??)
punditを使う
app/policies
は以下のようなクラスが入ります
# app/policies/post_policy.rb class PostPolicy attr_reader :user, :post def initialize(user, post) @user = user @post = post end def update? user.admin? or not post.published? end end
punditの使い方に関しては以下を
るびま
参考
Railsを責務に応じてリファクタリングするための9つのパターン | Simplie Post
gemを自然に使うとこうなるため、サンプルには事欠かなさそう。
cancancanのavility.rbを、modelに入れずにpoliciesに入れる
app/models/avility.rb
が肥大化してイヤだ!というときにapp/policies
に入れるという選択肢があるらしい。
中規模Web開発のためのMVC分割とレイヤアーキテクチャ - Qiita
http://blog.thefrontiergroup.com.au/2015/05/separating-abilities-in-cancan/
サンプルがあまり多くないので、上記ふたつをよく読んでから
特にgem等は使わず自分でピュアRubyのオブジェクトを定義して入れる
ちょっと古めの記事ですが、以下の記事で出て来るPolicy Objectのことです。
7 Patterns to Refactor Fat ActiveRecord Models
典型例は以下でしょうか
Policy Object – Steve Robinson – Medium
punditとpolicy objectがおなじpolicyでややこしいから、名前を変えたよという話?
https://sethvargo.com/authorizers-extractors-and-policy-objects/
policyというgemを使う
メンテされてなさそうですが、自分で作ったpolicy objectとcontrollerの橋渡しをしてくれるようなgemのようです
http://eng.joingrouper.com/blog/2014/03/20/rails-the-missing-parts-policies/
まとめ
今後の方針としては以下のような形で使い分けていきたい
- アクションベースのシンプルな権限管理にはpundit
- ロールベースの権限管理はcancancanを使う。
app/policies
は必要になったら作る - 条件が複雑なロジック(アドやレコメンドを出すかどうかみたいな?)がたくさんある場合にはpolicy objectを作る。たくさん無いときはserviceとかmodelに入れれば良さそう
gemの使い方はgemに合わせるわけですが、そうでない場合はcallback、validators同様、そのロジックが業務上重要で複雑な場合は、早めにクラスをわけてメンテし易いように保ちましょう、という話でした。
【パーフェクトRails】バリデーションをクラスに分離する
目的
パーフェクトRuby on Railsの9章の9−2「複雑なバリデーションとコールバックを整理する」を読んでいて 「バリデーションをクラスに分離する」について、知らないことがいくつかあったのでまとめます。
- 作者: すがわらまさのり,前島真一,近藤宇智朗,橋立友宏
- 出版社/メーカー: 技術評論社
- 発売日: 2014/06/06
- メディア: 大型本
- この商品を含むブログ (8件) を見る
バリデーションをクラスに分離するのは必要?
メールとか氏名とか、プロジェクトで要件が共通になると思うので積極的にやって良いと思う。
Open Source Rails でもapp/validators
は結構見かける。
どうやってやるのか?
例によってサンプルを見るのがわかりやすいと思います。
EachValidatorを使う
# app/validators/email_validator.rb class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors.add(attribute, 'is not an email') unless value =~ /.+@.+/ end end
上記の通りActiveModel::EachValidator
を継承してvalidate_each
を定義すればいい。
呼ぶときは以下のように呼ぶ。
class Person include ActiveModel::Validations attr_accessor :email, :name validates :email, email: true end
validates :email, email: true
は「emailというフィールドに対して、EmailValidator#validate_eachでチェックしてください。」というような解釈で良いと思う。EmailValidator
を見ると引数をがどのように渡ってくるかがわかると思う。
Validatorを使う
# app/validators/multi_presence_validator.rb class MultiPresenceValidator < ActiveModel::Validator def validate(record) record.errors.add(:base, 'bust be presence') if options[:attributes].all? { |c| record.send(c).blank? } end end
ActiveModel::Validator
を継承してvalidate
メソッドを定義する。
呼び出し方は以下。
class Person include ActiveModel::Validations attr_accessor :email, :name validates_with MultiPresenceValidator, attributes: [:email, :name] end
validates_with MultiPresenceValidator
は見たとおりMultiPresenceValidator#validateでバリデーションをする。
attributes: [:email, :name]
という引数に関しては、ActiveModel::Validator
のインスタンスからoptions[:attributes]
とすればさわれるようになっている。
validates_with Validator1, Validator2
と書くと両方のバリデーションを付け加えることができる
http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates_with
EachValidatorとValidatorの違い
EachValidator
は、上のサンプルの呼び出し方にある通り、1つのフィールドをチェックするやりかた(もちろん複数のフィールドをチェックするように書くことも出来なくはない)になる。
Validator
は複数のフィールドをチェックするのに使う。(こちらも1つのフィールドだけをチェックチェックするのは問題なく出来る。)
EachValidator
はValidator
のサブクラスになっている。
その他
これらの自作バリデーションや標準のバリデーションに関しては、モデルクラスごとにオブジェクトを持つ。インスタンス単位ではない。したがって、状態を持たないようにすること。
まとめ
app/valiadtors
ディレクトリはわりと一般的になっているので、プロジェクトたちあげのときに追加してバリデーションのクラス化を促すのが良いと思う。1フィールドのときは、
ActiveModel::EachValidator
を継承してvalidate_each
メソッド定義複数フィールドのときは、
ActiveModel::Validator
を継承してvalidate
メソッド定義PresenceValidatorsの実装とか読むといいと思う
【パーフェクトRails】コールバックをクラスに分離する
目的
パーフェクトRuby on Railsの9章の9−2「複雑なバリデーションとコールバックを整理する」を読んでいて コールバックをクラスに分離する場面ややり方がよくわからなかったのでまとめる。
- 作者: すがわらまさのり,前島真一,近藤宇智朗,橋立友宏
- 出版社/メーカー: 技術評論社
- 発売日: 2014/06/06
- メディア: 大型本
- この商品を含むブログ (8件) を見る
はじめにさらっと今回の主張だけ述べて、本編に突入します。
コールバックをクラスに分離するのは必要?
- あってもいいとは思うけど、作る機会は少ないかも。
作る場合はapp/models
かapp/services
に入れておけばいいかな
「コールバックをクラスに分離する」とは何か?
Railsのドキュメントにあったソースをそのまま載せてしまいます。
class BankAccount < ActiveRecord::Base before_save EncryptionWrapper.new("credit_card_number") after_save EncryptionWrapper.new("credit_card_number") after_initialize EncryptionWrapper.new("credit_card_number") end class EncryptionWrapper def initialize(attribute) @attribute = attribute end def before_save(record) record.send("#{@attribute}=", encrypt(record.send("#{@attribute}"))) end def after_save(record) record.send("#{@attribute}=", decrypt(record.send("#{@attribute}"))) end alias_method :after_initialize, :after_save private def encrypt(value) # Secrecy is committed end def decrypt(value) # Secrecy is unveiled end end
before_save EncryptionWrapper.new("credit_card_number")
って書くとbefore_save
のときにEncryptionWrapper.new("credit_card_number").before_save(self)
が呼ばれます。
つまり、渡されたオブジェクトに対してbefore_save
を委譲できるという機能というかapiです。
みんなやっているのか?
ひとまず参考資料をさがしてみることに。
ActiveRecord::Callbacks
9.2 Callbacks | Advanced Active Record in Rails 4 | InformIT
てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ! - Qiita
ruby - Rails: monkey-patching ActiveRecord::Base vs creating a Module - Stack Overflow
以下の、オープンソースのrailsプロジェクトがまとまっているサイトがあるのですが、コールバック用クラスの実装にであえませんでした。
Open Source Rails
GitLabで似たようなことをやっているようなソースも有りましたが、細かいところまでは読めませんでした。
今後どうしていくか
コールバックを別クラスに書くことは悪くはないのだが、そもそもコールバックの処理が肥大化していきそうで、あんまり良くないかなと思っている(ユーザ側に関係ない処理であれば良い)。
加えて、既存のrailsプロジェクトであまり行われていなさそうということも今回の調査でわかった。
よって、app/callbacks
みたいなディレクトリは作らず、もしコールバックの処理が複数のクラスで共通になったり、肥大化していった場合には、いったんapp/models
に作る。
【Ruby】【キャッシュ】dalli(memcachedのクライアントgem)を使ってみました
目的
memcachedとrubyのmemcachedクライアントであるgemのdalliを使ってみたのでまとめ
少し前に、キャッシュについていろいろやらないといけない機会があったのですが
キャッシュを実現するミドルウェアを触ったことがなかったため、手も足も出ないような状況になってしまいました。
これを期にキャッシュに使われるミドルウェアを触っていこうと思い
手始めにmemcachedと、rubyクライアントの中で割りと広く使われているdalliを触ってみました。
基本的な使い方と、覚えておけばどこかで役立ちそうな使い方についてまとめてみます。
基本的な使い方(インストールから)
インストールはgemのreadmeに親切に書いてあるのでその通りにやりましょう
$ brew install memcached $ gem install dalli
使いかたもreadmeと一緒ですが、irbで以下のように叩くと試せます。
irb(main)> require 'dalli' irb(main)> options = { namespace: 'app_v1', compress: true } irb(main)> dc = Dalli::Client.new('localhost:11211', options) irb(main)> dc.set('abc', 123) irb(main)> dc.get('abc') # => 123
deleteしたり、有効期間を指定してsetするとかが出来ます
irb(main)> dc.set('abc', 123) irb(main)> dc.delete('abc') irb(main)> dc.get('abc') # => nil irb(main)> irb(main)> dc.set('abc', 123, 5) irb(main)> dc.get('abc') # => 123 irb(main)> sleep 6 irb(main)> dc.get('abc') # => nil
覚えていて役立ちそうな使い方1(get_multi)
1つのキーに対するget、set、deleteはできるけど、複数キーはどうなの?という話。 memcachedのコマンドだと同じgetでできるみたいです。
dalliの場合はDalli::Client#get_multi
を使います。
irb(main)> dc.set('abc', 123) irb(main)> dc.set('def', 456) dc.get_multi('abc', 'def') # => {"abc"=>123, "def"=>456}
DBと組み合わせて使うときに、N+1対策で使えそうです。
覚えていて役立ちそうな使い方2(cas)
Check And Set (or Compare And Swap)の略で、memcachedで楽観的ロックを実現するための機能です
irb(main)> dc.set('a', 'value1') irb(main)> dc.cas('a') { 'value2' } irb(main)> dc.get('a') # => value2 irb(main)> dc.cas('a') { |v| sleep 10 "#{v}+1" } # sleepの間に別コンソールでdc.set('a', 'value4')を実行する => false irb(main)> dc.get('a') # => value4
cas
メソッドのブロックの中で行う処理に関しては、他者がそのキーの値を変更していない場合のみ書き込みでき
変更している場合にはfalseが返され書き込みに失敗します。
配列みたいなデータ構造があると、ロックと組み合わせてキューみたいな使い方ができるかなと思いました。
cas
メソッドにまかせている色々なことを、自力でやることもできます。詳細は述べませんが、以下のようにやるといろいろメソッドが追加されます。
irb(main)> require 'dalli/cas/client'
詳細は以下
【DB設計】T字型ER入門
目的
T字型ER手法を勉強することになったので、概略とまとめ
データベース設計論 T字形ER―関係モデルとオジブェクト指向の統合をめざして
- 作者: 佐藤正美
- 出版社/メーカー: ソフトリサーチセンター
- 発売日: 2005/09
- メディア: 単行本
- 購入: 3人 クリック: 42回
- この商品を含むブログ (20件) を見る
設計の、特にデータベース設計について勉強しようと考えていた時にT字型ERというワードにたどり着きました。
前からデータベース設計の手順、手法についてはいろいろ模索してはいたのですが、あんまり体型だった手法がわからなかったので一度勉強してみることに。
概略と、現時点での思うことについてまとめます。
T字型ER手法とはなにか
データ正規形を作りながら、同時に、事業過程を分析して、かつ、プログラムのアルゴリズムを「I/O化」する技術。らしい。
mah_labさんのブログによると、Railsのテーブル設計と相性がいい、とのこと。
blog.mah-lab.com
以下のブログでは2000年ごろSIの一部で流行った、とされている。
ozamasa.hatenablog.jp
T字型ER手法の体型
以下のような体型で構成されている。
- エンティティを作る規準を示す
- エンティティを2つのクラス概念に分類する
- リレーションを作る規準を示す
- エンティティが正しい集合になっているかどうか、検証する
- 「多値のOR関係」と「多値のAND関係」を扱う規準を示す
- 「みなしエンティティ」と「概念的スーパーセット」を作る規準をしめす
これは追々記事でまとめていきましょう
最大の特徴
「関係」のモデリングのしかたに重きを置いているようです。R:リソース系、E:イベント系とすると、E-E,E-R,R-R,そして再帰的な関係と、それぞれのパターンに応じてどうテーブルを作っていけばいいのか細かに記載されています。
テーブル設計にまよったときに、やり過ぎにはなるかもしれないけれどとりあえず間違いではないテーブル設計ができるかもなとは思いました。
やり過ぎ禁物
カラムではなくテーブルで表現するというのは、時に複雑さを招いてしまうかもしれません。
例えば「都道府県」と「エリア(関東とか関西とか」というテーブルがあったとして、よくやるのは都道府県レコードにエリアidをもたせるのですが、この手法に従うと、「都道府県エリア関連テーブル」を作ってそこで関係を表す事になりそうです。(ER図書きたい)
これは、リソースの変更と関係の変更が区別できるため、関連の変更が多いテーブル(例えば会社組織とか)にかんしては有効かも知れませんが、そうでないケースもあるかなと思いました。
もう少し読んでみて、適切な場面で使っていければと思います。
まとめ
まだ全然読み切れていないのですが、T字型ER手法の現時点でのまとめでした。
しっかり理解して適切な場面で使いたいです。