Edited at

RailsでIDを露出させないようにする②

More than 3 years have passed since last update.

以前の記事obfuscatableというgemを作ったのですが、ハッシュ風にエンコードしたIDをデコードしつつレコードを取得する時に、ActiveRecordfindをうまくoverrideできずにやむなくfind_obfuscatedという別のメソッドを定義していました。

(理由は後述

最近、hashids.rbという別の方法でIDを変換するgemを発見したため、今回はfindをオーバーライドして完全に透過的にIDをエンコードするgemを作りました。

https://github.com/dtaniwaki/acts_as_hashids

xxx.png


使い方

使い方はいたって簡単。

gem "acts_as_hashids"

Gemをインストールして、以下のようにacts_as_hashidsApplicationRecordに書くだけです。

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になります。

https://github.com/dtaniwaki/acts_as_hashids