Enum và sự khác biệt khi sử dụng giữa rails 4.x và rails 5.x

1. Enum là gì ?

  • Có thể nói nôm na: Enum là trường được định nghĩa là enum thì sẽ lưu trong DB là số nhưng mà lại có thể được truy xuất bằng chữ
  • Ví dụ : Model User có trường role: [:member, :admin] thì được lưu trong DB tương ứng là 0, 1 nhưng có thể được truy xuất bằng chữ : User.member, User.admin
  • Cú pháp:
class User < ActiveRecord::Base
    enum role: [:member, :admin]
end

2. Một số lưu ý khi sử dụng enum với rails 4 và rails 5

  • Nếu trường dùng làm enum nhận giá trị nil và mọi hàm gán cho giá trị này đều trả về False

  • Vì vậy: Trường lưu trữ enum nên để nullable: false và có giá trị mặc định(default):

class CreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      t.integer :role, null: false, default: 0

      t.timestamps
    end
  end
end
  • Như ta thấy : Ứng với các function admin, member là các scope, nên chúng ta có thể nối thêm các scope khác để truy xuất thuận tiện hơn. Thay vì viết:
User.where(role: 0)

Thì ta viết:

User.admin

sẽ trực quan và ý nghĩa hơn nhiều. Bên cạnh đó có thể nối thêm các scope hay mệnh đề khác:

User.admin.where(id: 1)
  • Đến đây vẫn chưa thấy được sự khác biệt của enum trong rails 4 và rails 5, phần tiếp theo mình xin trình bày sự khác biệt đó. Thử một ví dụ trước nhé để xem sự khác biệt đó.: Ở rails 4:

  • Như chúng ta thấy, cả 2 trường hợp đều cho kết quả không mong muốn, vì bản chất role được lưu vào DB là số, nên khi sử dụng mệnh đề where thì nó convert về dạng int: +) "member".to_i = 0 nên thành ra nó tìm các bản ghi có role là 0 (ứng với admin). +) :member là symbol không có hàm to_i nên nó so sánh role với nil. *Vì vậy lưu ý cho mình : Với rails 4 tuyệt đối không so sánh các trường enum với string hoặc symbol. Còn với rails 5:

  • Như chúng ta thấy, vẫn với câu lệnh như bên kia nhưng kết quả trả về khác hẳn, đúng như những kết quả ta mong muốn. Đây cũng chính là điểm khác biệt mình muốn đề cập tới, những cái gọi là còn "tồn đọng", "khuyết điểm" ở rails 4 tới đây đã được giải quyết.

  • Còn 1 điều khá là nguy hiểm nên mình tách riêng ra ở đây: Vì bản chất rails 4 convert giá trị so sánh về dạng integer trước khi đem so sánh nên sẽ có thể xảy ra trường hợp sau:

  • Rõ ràng là không có thằng nào có role : "adminnn" và cũng không tồn tại "adminnn" trong enum nhưng vẫn select ra được, do "adminnn".to_i = 0.

  • Với rails 5 thì kết quả khác hẳn:

  • Vẫn với lý do như trên bản chất rails 4 convert cái mình truyền vào thành integer sau đó so sánh với 0,1,... Còn rails 5 đã update có cơ chế convert ngược nên không còn xảy ra tình trạng trên.

3. Nguyên tắc khi thêm function mới với enum

  • Thêm vào ngoài cùng bên phía tay phải
class User < ApplicationRecord
  enum role: [:admin, :member, :mod]
end
  • Khi đó theo logic lưu vào DB: mod ứng với 2, và role các bản ghi trước đó không bị ảnh hưởng. Nhưng giả sử mình thêm :mod vào ngoài cùng tay trái.
class User < ApplicationRecord
  enum role: [:mod, :admin, :member]
end
  • Khi đó :admin từ 0 thành 1, :member từ 1 thành 2 và các bản ghi trước sẽ bị ảnh hưởng
  • Vậy câu hỏi đặt ra là : "Tôi muốn đặt "mod" ở đầu tiên mà không làm ảnh hưởng tới các bản ghi trước có được không? "
  • Câu trả lời là được vì rails hỗ trợ xét giá trị role theo kiểu Hash
class User < ApplicationRecord
  enum role: {
    mod: 2,
    admin: 0,
    member: 1}
end
  • Và khi đó kết quả lại trở về như cũ và không bị ảnh hưởng gì
  • User đầu tiên lại trở về role như ban đầu là "member"

Tổng kết

  1. Enum hỗ trợ lưu trữ trong DB là số nhưng có thể truy xuất qua chữ nên nó rất trực quan.
  2. Trường lưu trữ Enum không nên để nullable và luôn có giá trị mặc định
  3. Khi sử dụng rails 4 nên chú ý không được so sánh trực tiếp trường có kiểu enum với string hoặc symbol
  4. Các key của enum trong cùng 1 class không được trùng nhau và trùng với từ khóa
  5. Có thể biểu diễn enum dưới dạng Hash

Tài liệu tham khảo

  1. https://viblo.asia/ta.duy.anh/posts/nwmGyElDGoW
  2. http://api.rubyonrails.org/classes/ActiveRecord/Enum.html