はじめに
本記事はZeals Advent Calendar 2019の24日目です。メリークリスマスイブ!
本記事では僕が日々開発しているなかで、どうしてもなんとかしなければいけない…という時に活用したtipsをまとめたものです。
タイトルにある通り、エッジケースすぎるのでもし同じような状況で困っている人の為になれたら幸いです。
環境
Rails 5.2.3
Ruby 2.5.5
tips
といっても今回紹介するのは2つです
- カラムにaliasをかける
- caller
カラムにaliasをかける
Railsではcolumnに対してaliasを貼ることができます。
モデル名 | カラム名 |
---|---|
Button | name |
Choice | label |
このようなモデルが2つあるとします。
カラム名はそれぞれ違いますが同じものとして扱いたい時があると思います。
そんな時はalias_attribute が使えます。
class Choice < ActiveRecord::Base
alias_attribute :name, :label
alias_attribute
を使うことでChoiceのlabel
をname
として扱うことができるようになります。
ただname
として扱うことができるようになるだけではなく
Choice.first.name?
=> true
といったActiveRecordの述語メソッドやゲッターやセッターを生成してくれます。
ただ同名として扱いたいだけで、述語メソッドを生成するまでもない場合は以下のようにシンプルに定義するだけで良さそうです。
class Choice < ApplicationRecord
def name
label
end
カラム名を変更したい。変更したほうが良い。でもrename_column
してる時間ないよぉ…
という時に使いました、便利ですねー
caller
モデル名 | カラム名 |
---|---|
User | cliend_id |
モデル名 | カラム名 |
---|---|
Answer | client_id |
モデル名 | カラム名 | カラム名 |
---|---|---|
UserAnswer | user_id | answer_id |
このような中間テーブルがあるとします。
以下はmodel
class UserAnswer < ApplicationRecord
belongs_to :user
belongs_to :answer
validates :should_be_same_client
def self.import_associations!(users, answer)
associations = users.map do |user|
UserAnswer.new(user_id: user.id, answer_id: answer.id)
end
UserAnswer.bulk_import! associations, validate: false
end
def should_be_same_client
return user.client == answer.client
errors[:base] << ' client_id of user and client_id of answer are different'
end
end
定義されている
import_associations!
は引数としてuserの配列と指定のanswer(回答)を受け取ります。
カスタムのvalidationメソッドが定義されており、User
もAnswer
もClient
に対してbelongs_to
の関係で、client_id
が違うものが作成されないようにしています。
should_be_same_client
はUserAnswer単体を生成する時は動いてほしいものですが、bulk import
で作成したい時はvalidationをskipしたい。カスタムvalidationも動いてほしくないとします。
Railsのversionは5.2系なので(早く6に上げたい…)bulk importはactiverecord-importを利用します。
bulk_importはoptionでvalidate: false
を渡すことでmodelのvalidationをskipさせることができます。
しかし今回定義したようなカスタムvalidationはskipしてはくれません。
bulk importする件数が数件であれば都度カスタムvalidationが走ったところで問題はないのですが、件数が多くなればなるほどN+1で都度カスタムvalidationが走るにより実行時間がどんどん長くなるので避けなければなりません。
つまりbulk_import
を呼び出すメソッドのときのみカスタムvalidationをskipする必要があります。
そこでcallerを使いました。
callerはバックトレースの情報を確認することができるので、どのメソッドから呼び出されたのか,どのメソッドを経由してきたのかを確認することができます
callerで呼び出されたバックトレースを正規表現を使って、メソッド名だけを見れるようにします
caller.map { |c| c[/`([^']*)'/, 1] }
=> ["eval",
"evaluate_ruby",
"handle_line",
"block (2 levels) in eval",
"catch",
"block in eval",
"catch",
"eval",
"block in repl",
"loop",
"repl",
"block in start",
"__with_ownership",
"with_ownership",
.......
これでどのメソッドを経由してきたのかがわかります。
取得したバックトレースないにvalidateをskipしたいメソッドが含まれているかを確認するメソッドを追加し、
def call_from_import_associations?
caller.map { |c| c[/`([^']*)'/, 1] }.include?('import_associations!')
end
validates :should_be_same_client, unless: call_from_import_associations?
カスタムvalidationの発火条件として追加し、import_associations!
から呼び出されるときのみカスタムvalidationが発火しないようにすることができます。
まとめ
実際にこのユーザーに対してtagを一括でつける機能は使用頻度の高い機能だったので、早急に修正する必要があり、本当になんとかするためにひねり出した苦肉の策ではあります…w
早急に対応しなければいけない。なんとかしなければいけない。という同じような問題に詰まっている人たち、もしくは同じようなケースで困っているの助けに少しでもなっていれば幸いです!
明日はついにAdvent Calendarの最終日です!
最終日は@pannpersです!
それでは良いクリスマスイブを!