LoginSignup
13
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-06

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

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

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

13
8
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
13
8