我們有一個User model,每個user會有不同的角色(admin, manager, member),而且一個user可以同時擁有多個角色,那要怎麼儲存user的角色資訊呢?
user_id
與role
這兩個欄位,則一個user會有多個user_role。這是標準使用model association的做法,如果你需要查詢哪一些user有某個role,或是之後想要動態增加新的角色,其實這是最正確的做法。但如果沒有這些需求,也就是說角色已經固定,而且只想確定某個user的角色是什麼,那麼為了要儲存role而多建了一個model似乎有點殺雞牛刀的感覺。另外,因為role是儲存在另一個table,意味著每次要做role的判斷都要同時讀取users與user_roles這兩個table,顯然在效能上會受到一些影響。roles
的欄位。但如果要存多個值,第一個會想到的就是用serialize data,也就是在roles
中儲存的是[:admin, :member]
之類的值,這樣可以不用開另一個table來儲存role的資訊。但如果只是存role,卻要用到serialize data似乎有點殺雞牛刀的感覺(你到底有多少把牛刀啊),serialize data本身也有沒辦法判斷dirty而有強迫更新的問題。角色 | 所在的bit位置(由左到右) | 用bitwise的方式表示 | 實際會儲存在roles 的值 |
---|---|---|---|
admin | 1 | 001 | 1 |
manager | 2 | 010 | 2 |
member | 3 | 100 | 4 |
如果某個user有admin又有member的角色,那在roles
中儲存的值就會像下面所表示的:
角色 | 用bitwise的方式表示 | 實際會儲存在roles 的值 |
---|---|---|
[:admin, :member] | 101 | 4 + 1 = 5 |
看起來是個不錯的方法,可是要怎麼實作呢?
db/migrate/20151008032103_add_roles_to_users.rb
class AddRolesToUsers < ActiveRecord::Migration
def change
add_column :users, :roles, :integer, null: false, default: 0
end
end
ROLES
用來定義有哪些角色。另外再加上roles_bitwise
與roles_bitwise=
這兩個method。app/models/user.rb
class User < ActiveRecord::Base
ROLES = {
admin: 1,
manager: 2,
member: 3,
}
attr_accessor :role_bitwise
# ...
def roles_bitwise
result = []
ROLES.each do |bit_key, bit_value|
result << bit_key if (self.roles & 2**(bit_value - 1)) != 0
end
result
end
def roles_bitwise=(bitwise_val)
result = 0
bitwise_val.each do |bv|
result = result | 2**(ROLES[bv.to_sym] - 1) if ROLES[bv.to_sym]
end
self.roles = result
self.roles
end
end
有了roles_bitwise
與roles_bitwise=
就可以用Array的方式設定角色,而roles_bitwise會自動將array轉成integer並assign到roles中。
u = User.new
u.roles_bitwise = [:admin, :member]
puts u.roles # 5
u.roles = 1
puts u.roles_bitwise # [:admin]
roles_bitwise
來取代roles
,使用roles_bitwise
的方式就跟一般的欄位一樣,但要注意的是strong parameter中的roles_bitwise
需要設成Array的方式傳入。app/controllers/user/roles_controller.rb
class User::RolesController < ApplicationController
def update
if @user.update_attributes(user_params)
# ...
end
end
# ...
private
def user_params
params.require(:user).permit(roles_bitwise:[])
end
end
app/views/user/roles/edit.html.erb
<%= simple_form_for(@user) do |f| %>
<%= f.input :roles_bitwise,
collection: User::ROLES,
as: :check_boxes,
label_method: -> (r){ t("user.role.#{r.first}") },
value_method: :first %>
<%= f.button :submit %>
<% end %>
這裡介紹了如何使用bitwise的方式儲存multiple value的值,我覺得很重要的兩點就是:
roles_bitwise
與roles_bitwise=
這兩個method簡化了roles的計算與處理,我們不直接使用roles
而是藉由roles_bitwise
來處理roles
是一個重要的技巧。