以前の記事でobfuscatableというgemを作ったのですが、ハッシュ風にエンコードしたIDをデコードしつつレコードを取得する時に、ActiveRecord
のfind
をうまくoverrideできずにやむなくfind_obfuscated
という別のメソッドを定義していました。
(理由は後述)
最近、hashids.rbという別の方法でIDを変換するgemを発見したため、今回はfind
をオーバーライドして完全に透過的にIDをエンコードするgemを作りました。

使い方
使い方はいたって簡単。
gem "acts_as_hashids"
Gemをインストールして、以下のようにacts_as_hashids
とApplicationRecord
に書くだけです。
class ApplicationRecord < ActiveRecord::Base
acts_as_hashids
end
class Foo < ApplicartionRecord
end
foo = Foo.create
# => #<Foo:0x007feb5978a7c0 id: 3>
foo.to_param
# => "ePQgabdg"
Foo.find(3)
# => #<Foo:0x007feb5978a7c0 id: 3>
Foo.find("ePQgabdg")
# => #<Foo:0x007feb5978a7c0 id: 3>
Foo.with_hashids("ePQgabdg").first
# => #<Foo:0x007feb5978a7c0 id: 3>
こんな感じでエンコードしたIDにもfind
がそのまま使えます。
findオーバーライドの課題
find
をオーバーライドしようとした時に最初に思いつくのは、
class Foo < ActiveRecord::Base
def self.find(ids)
# Do whatever
puts 'Yay'
end
end
Foo.find('something')
# => "Yay"
しかし、このオーバーライドは以下のような場合に失敗します。
# `bar.foos`は`has_many`でfooを取得すると仮定
bar.foos.find('something')
# ActiveRecord::RecordNotFound: Couldn't find User with 'id'="something"
Foo.where(nil).find('something')
# ActiveRecord::RecordNotFound: Couldn't find User with 'id'="something"
なぜかというと、同じfind
を使っているように見えて、実は関数の定義されている場所が違います。
bar.foos.class
# => Foo::ActiveRecord_Associations_CollectionProxy
Foo.where(nil).class
# => Foo::ActiveRecord_Relation
これらのクラスにmixinされているモジュールをMonkeyパッチすれば簡単なのですが、そこを弄ってしまうと汎用性がなくなってしまいます。
解決策
そこで、オーバーライドしたいロジックのfind
を含んだモジュールFinderMethods
を作成し、各所に動的にextend
してみます。
ActiveRecord_Associations_CollectionProxy
これはhas_many
の定義時に生成されるため、
class Foo < ActiveRecord::Base
def self.has_many(*args, &block)
options = args.extract_options!
options[:extend] = (options[:extend] || []).concat([FinderMethods])
super(*args, options, &block)
end
end
Base.foos.find('something')
# => "Yay"
成功です!
ActiveRecord_Relation
これはActiveRecord::Base.relation
として動的に生成されるため、
class Foo < ActiveRecord::Base
def self.relation
r = super
r.extend FinderMethods
r
end
end
Foo.where(nil).find('something')
# => "Yay"
成功です!
ということで、このあたりをうまく吸収して、既存のコードに一切手を入れずにIDをエンコードできるgemがacts_as_hashids
になります。