LoginSignup
1

More than 5 years have passed since last update.

Railsでもう消したいassociationが使用されたら「それはDeprecatedである」と警告したい

Last updated at Posted at 2017-09-15

背景

ある日のぼく「ここのhas_manyとかもういらないよね〜!それに今は1:1なここの関係は、1:多にしないと今後ヤバそうだよな〜!よーし、変えるか〜!!!!」
(この後、数時間に及ぶ試行錯誤を行う)
ぼく「この関連、使用箇所多すぎ!!テスト通らねぇ!!寿命で死ぬ!!こんなもんに時間かけてられっか!!!!(ブチギレ)」
(自分ができるだけ手を動かさずに済む方法を考える)
ぼく「せや!もう使って欲しくない関連付けを使ってる場所で『この関連付けはそのうち消す(Deprecated)』ということを皆に知らせて実装を変えてもらおう!レッツ人海戦術!」

というわけで、タイトルのことをやってみました。

やったこと

時間がない人向け

こういうmoduleを作成しました。

deprecatable.rb
# Deprecatedなassociationを設定したい場合ははこれを使用する

module Deprecatable
  extend ActiveSupport::Concern

  module ClassMethods
    def deprecated_associations(*names)
      names.each do |name|
        reflection = self.reflections[name]
        if reflection.collection?
          deprecated_collection_association(name)
        else
          deprecated_single_association(name)
        end
      end
    end

    private

    def deprecated_single_association(name)
      single_method_name_to_association_method_name_hash = {'association' => 'reader',
                                                            'association=' => 'writer'}.freeze

      deprecated_association(name, single_method_name_to_association_method_name_hash)
    end

    def deprecated_collection_association(name)
      collection_method_name_to_association_method_name_hash = {'collection' => 'reader',
                                                                'collection=' => 'writer',
                                                                'collection_singular_ids' => 'ids_reader',
                                                                'collection_singular_ids=' => 'ids_writer'}.freeze

      deprecated_association(name, collection_method_name_to_association_method_name_hash)
    end

    def deprecated_association(_name, _method_hash)
      name = _name.to_s

      _method_hash.keys.each do |_method_name|
        method_name = _method_name.gsub(/association|collection_singular/, name.singularize).gsub(/collection/, name.pluralize)
        association_method_name = _method_hash[_method_name]

        # 元のメソッドは利用不可にする
        alias_method "orig_#{method_name}".to_sym , method_name.to_sym
        private "orig_#{method_name}".to_sym

        # 元のメソッド名で新たなメソッドを定義
        define_method(method_name) do |*args|
          ActiveSupport::Deprecation.warn "#{name} association is being removed", caller(2,1) if Rails.env != 'test'
          association(_name).send(association_method_name.to_sym ,*args)
        end
      end
    end
  end
end

このモジュールをDeprecatedとしたい関連付けのあるクラスにincludeし、deprecated_assosiations :hogesのように記述すると、その関連付けが使用された時に警告がコンソールに表示されます。

user.rb
class User < ActiveRecord::Base
  has_one :foo
  has_many :bars

  include Deprecatable
  deprecated_associations :foo, :bars
end
console
[1] pry(main)> User.first.foo
DEPRECATION WARNING: foo association is being removed. (called from eval at 使用しているパス )
# => foo
### 中略 ###
[2] pry(main)> User.first.bars
DEPRECATION WARNING: bars association is being removed. (called from eval at 使用しているパス )
# => bars
### 後略 ###

まだ時間がある人向けの解説

重要キーワード

  • ActiveRecord::Reflectionクラス
  • ActiveRecord::Associationsクラス
  • メタプログラミング(define_methodメソッド、privateメソッド)

ActiveRecord::ReflectionクラスとActiveRecord::Associationsについてはこちらの記事がよくまとまっていて、非常に参考になりました。ありがとうございます。
Railsのコードを読む アソシエーションについて

動作の流れ

  1. deprecated_associationsメソッドの引数で与えられた名称の関連付けがcollection? == trueかを見てメソッドを振り分ける
  2. メソッドの対応表を用意する
  3. 既存のメソッドを置き換える

以下で各項の解説を行います。

collection? == trueかを見てメソッドを振り分ける

self.reflections[:name]:nameの関連付けの情報を取得できます。

懸念点

  • カバーしていないメソッドが大いにありそう

あとがき

  • 例えば has_one :foo, deprecated: true のように関連付けメソッドのオプションにdeprecated: trueみたいなオプションを付けられる様にしたかった
    • そうなると元のコードに手を加える必要がありそうな気しかしない
    • Contributorになるしかねぇ!

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
1