Rails

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

More than 1 year has passed since last update.

背景

ある日のぼく「ここの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になるしかねぇ!