ビット演算を活用してRailsのモデルにColumn一本だけで複数のタグを付ける

# TL;DR
## 権限判断
```rb
admin    = 1 # 0001
editor   = 2 # 0010
observer = 4 # 0100

my_roles = 3 # 0011

my_roles & admin    == admin    # true
my_roles & editor   == editor   # true
my_roles & observer == observer # false

# So `my_roles` Has `admin` and `editor`
```

## Scopeで検索
```user.rb
class User < ApplicationRecord
  # ...
  scope :has_role, lambda { |role|
    where('roles_code & ? = ?', role, role)
  }
  scope :roles_equal, lambda { |roles|
    where(roles_code: roles.reduce(:|))
  }
  # ...
end

# Eg:
# User.has_role('0001'.to_i(2)).has_role('0100'.to_i(2))
# > User Load (0.6ms)  SELECT "users".* FROM "users" WHERE (roles_code & 1 = 1) AND (roles_code & 4 = 4)
#
# User.roles_equal([1, 2])
# > User Load (4.9ms)  SELECT "users".* FROM "users" WHERE "users"."roles_code" = $1  [["roles_code", 3]]
```

# はじめに
Railsでは、ユーザーに複数の権限や商品に複数のタグを付けたい時、`has_and_belongs_to_many`を使って多対多のリレーションを作成することが多いと思います。
しかし多対多リレーションは中間テーブルを必要で、DBに必要以上多量なデータを保存してしまい、検索する際も非効率です。
そこでヒントになったのはUnix/Linuxの権限の持ち方でした。
*nix系システムでは、`755`, `600`など、8進数3個で`オーナ－`、`グループ `、`その他`のロールにそれぞれ8種類の権限を表示しています。
例えば権限`600`のファイルは「オーナのみ読み書きができる(`rw-------`)」を意味しています。
そしてIPアドレスも同じ考え方が用いられています。例えばサブネットを設定する時、`255.255.255.0`のようなサブネットマスクとオリジンのIPとビット演算することで、通信を制御しています。

ちなみにこれらの手法は基本情報技術者試験に出題されるので、ちょうど復習にもなれます。

# 方法
## Modelの作成
整数型の`roles_code`に、権限情報を保存するようにします。

```rb
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string  :name,           null: false, default: ""
      t.integer :roles_code,     null: false, default: 0
    end
    add_index :users, :name
    add_index :users, :roles_code
  end
end
```

```user.rb
class User < ApplicationRecord
end
```

## 権限の定義
例として権限を下記の風に定義します。各権限はそれぞれ独立です。
下記のテーブルが示したように、16進数(4ビット)と照らし合わしてみると、
最下位のビットは`admin`の権限の有無を表記、右から第2のビット、第３のビットは`editor`と`observer`の有無を記載しています。最上位は使用していません。今後新たに権限が追加できるように、最下位から上の桁に追加するのは重要です。

```
権限名:  (未使用)  observer   editor   admin
ビット:      0        1         1        1
```

## 権限の認証
サブネットマスクのように、対象コードと指定の[ビットマスク](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%82%B9%E3%82%AF_(%E6%83%85%E5%A0%B1%E5%B7%A5%E5%AD%A6))とAND演算(論理積演算)を行うと、もし結果がマスクと同じであれば、該当マスクに「通過した」考え、該当マスクの権限を所有すると判断します。

### 例題
```ruby

admin_mask    = 1 # 0001
editor_mask   = 2 # 0010
observer_mask = 4 # 0100

my_roles = 3 # 0011

#### 権限の認証 ####
my_roles & admin_mask    == admin_mask    # true
# 0011 & 0001 == 0001
my_roles & editor_mask   == editor_mask   # true
# 0011 & 0010 == 0010
my_roles & observer_mask == observer_mask # false
# 0011 & 0100 => 0000 != 0100
# 故に`my_roles` に `admin` と `editor` の権限があることがわかる


#### 権限の付与 ####
# `my_roles_2` に `editor` と `observer` の権限を付与したい
# 観察すると、`0110` にするとわかった。
# `editor_mask` と `observer_mask` のOR演算(論理和)をとればいい
my_roles_2 = editor_mask | observer_mask
# 0110 = 0010 | 0010
my_roles_2
# 6 => 0110

```

### マスクの設定
```user.rb
class User < ApplicationRecord
  def self.role_masks
    {
      admin:    1, # 0001
      editor:   2, # 0010
      observer: 4, # 0100
    }
  end
end
```

## 所有する全ての権限を表示
簡単に言えば、`roles`という[attr_accessor](https://ref.xaio.jp/ruby/classes/module/attr_accessor)を自前で設定します。

```user.rb
class User < ApplicationRecord
  @roles

  def roles
    @roles = []
    User.role_masks.each do |role, mask|
      if roles_code & mask == mask
        @roles << role_text
      end
    end
    @roles
  end

  def self.role_masks
    {
      admin:    1, # 0001
      editor:   2, # 0010
      observer: 4, # 0100
    }
  end
end
```

```rb
user = User.new(name: 'user1', roles_code: 1)
user.roles
# [:admin]
```

## 所有する権限の変更

```user.rb
class User < ApplicationRecord
  @roles

  def roles
    @roles = []
    User.role_masks.each do |role, mask|
      if roles_code & mask == mask
        @roles << role
      end
    end
    @roles
  end

  def roles=(roles)
    self.roles_code = roles
      .map{|role| User.role_masks[role]}
      .compact.reduce(:|)
  end

  def self.role_masks
    {
      admin:    1, # 0001
      editor:   2, # 0010
      observer: 4, # 0100
    }
  end
end
```

```rb
user = User.new(name: 'user1', roles_code: 1)
user.roles
# [:admin]
user.roles = [:editor]
user.roles_code
# 2 => '0010'
user.roles = user.roles << :observer
user.roles_code
# 6 => '0110'
```

## 所有する権限で検索
SQLでは、`WHERE`文に簡単な計算が行うこともできるので、下記のscopeを設定すれば検索出来ます。

```user.rb
class User < ApplicationRecord
  @roles

  scope :has_role, lambda { |role|
    role_code = User.role_masks[role]
    where('roles_code & ? = ?', role_code, role_code)
  }

  def roles
    @roles = []
    User.role_masks.each do |role, mask|
      if roles_code & mask == mask
        @roles << role
      end
    end
    @roles
  end

  def roles=(roles)
    self.roles_code = roles
      .map{|role| User.role_masks[role]}
      .compact.reduce(:|)
  end

  def self.role_masks
    {
      admin:    1, # 0001
      editor:   2, # 0010
      observer: 4, # 0100
    }
  end
end
```

```ruby
User.has_role(:admin).has_role(:editor)
# User Load (0.6ms)  SELECT "users".* FROM "users" WHERE (roles_code & 1 = 1) AND (roles_code & 2 = 2)
```

## enum風に`role?`のようなメソッドの実装
[メタプログラミング](https://qiita.com/iguchi1124/items/a2a64f27e6dabbac1301)の手法を活用すれば、簡単にできます

```user.rb
class User < ApplicationRecord
  @roles

  scope :has_role, lambda { |role|
    role_code = User.role_masks[role]
    where('roles_code & ? = ?', role_code, role_code)
  }

  role_masks.keys.each do |role|
    # `user.admin?` のようなメソッドの生成
    method_name = role.to_s + "?"
    define_method(method_name.to_sym) {
      self.roles.include?(role)
    }
  end

  def roles
    @roles = []
    User.role_masks.each do |role, mask|
      if roles_code & mask == mask
        @roles << role
      end
    end
    @roles
  end

  def roles=(roles)
    self.roles_code = roles
      .map{|role| User.role_masks[role]}
      .compact.reduce(:|)
  end

  def self.role_masks
    {
      admin:    1, # 0001
      editor:   2, # 0010
      observer: 4, # 0100
    }
  end
end
```

```ruby
user = User.new(name: 'user1', roles_code: 3) # [:admin, :editor]
user.admin?    # true
user.editor?   # true
user.observer? # false
```

どうせなら、Enum風に[Scopeも自動生成](https://stackoverflow.com/a/14061680/11263491)しましょう。

```user.rb
class User < ApplicationRecord
  @roles

  scope :has_role, lambda { |role|
    role_code = User.role_masks[role]
    where('roles_code & ? = ?', role_code, role_code)
  }

  role_masks.keys.each do |role|
    # `user.admin?` のようなメソッドの生成
    method_name = role.to_s + "?"
    define_method(method_name.to_sym) {
      self.roles.include?(role)
    }
    # `User.admin` のようなスコープの生成
    scope role, lambda {
      role_code = User.role_masks[role]
      where('roles_code & ? = ?', role_code, role_code)
    }
  end

  def roles
    @roles = []
    User.role_masks.each do |role, mask|
      if roles_code & mask == mask
        @roles << role
      end
    end
    @roles
  end

  def roles=(roles)
    self.roles_code = roles
      .map{|role| User.role_masks[role]}
      .compact.reduce(:|)
  end

  def self.role_masks
    {
      admin:    1, # 0001
      editor:   2, # 0010
      observer: 4, # 0100
    }
  end
end
```

```ruby
User.admin.first
# User Load (2.6ms)  SELECT  "users".* FROM "users" WHERE (roles_code & 1 = 1) ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
```

## Ransackで検索可能にする(2019-07-03更新)
Ransackで検索したい時は、`ransackable_scopes`を活用すれば簡単にできます。
詳しくは[こちらの記事](https://qiita.com/hirotakasasaki/items/0578dd637e2bb19f5c97)を参考してください。

```user.rb
class User < ApplicationRecord
  @roles

  scope :has_role, lambda { |role|
    role_code = User.role_masks[role]
    where('roles_code & ? = ?', role_code, role_code)
  }

  # ...省略...

  def self.ransackable_scopes(auth_object = nil)
    %i(has_role)
  end

  def self.role_masks
    {
      admin:    1, # 0001
      editor:   2, # 0010
      observer: 4, # 0100
    }
  end
end
```

```rb
User.ransack({has_role: 'admin'}).result
=> User Load (5.3ms)  SELECT "users".* FROM "users" WHERE (roles_code & 1 = 1)
```

# 参考
https://qiita.com/koara-local/items/185838ea3fa37d9007f7
https://qiita.com/lnznt/items/d2d18e45bcab48cf107d
https://qiita.com/okamos/items/724a4a162dfa9e27754a
https://db.just4fun.biz/?SQL/SELECT句で四則演算を行う
https://stackoverflow.com/a/14061680/11263491