18
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ZealsAdvent Calendar 2019

Day 24

世の中の1%の人の為になるかもしれないRails tips

Posted at

はじめに

本記事は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のlabelnameとして扱うことができるようになります。
ただ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メソッドが定義されており、UserAnswerClientに対して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です!
それでは良いクリスマスイブを!

18
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?