Sort kết quả trả ra của ActiveRecord bằng các giá trị enum

Đăng bởi Lưu Đại vào ngày 18-12-2022

Rails enum là gì? 

Rails enum là gì? Rails enum là một alias của rails có thể gọi là một tính năng cũng được. Nó sinh ra nhằm giúp code Rails gọn gàng dễ hiểu hơn =))) ngoài ra khi khai báo một enum trong models nó cũng tạo ra cơ số những hàm rất tiện dụng. 

Tiện ích của enum? 

Tiện ích của enum? Như đã đề cập ở trên ngoài việc làm cho code dễ hiểu ngắn gọn hơn. Enum còn sinh sẵn một số hàm hỗ trợ trong models. Để giải thích rõ hơn thì mình đi qua ví dụ sau:
class User < ActiveRecord::Base
	enum role: %i(admin employee) # Lưu ý %i(admin employee) sẽ tạo ra một mảng các symbol [:admin, :employee]
end

Đối với khai báo như trên Rails sẽ tự động tạo ra các hàm: admin! : Hàm này dùng để cập nhật role cho một user đã được query ra trước đó: #user.admin! ⇒ user này sẽ được cập nhật trạng thái thành admin 

#admin? : Hàm này để kiểm tra xem một user 
class Inventory < ActiveRecord::Base 
 enum product_status: { interim_product: 0, working_product: 1, final_product: 2, fail_product: 3 }
end

Inventory.order(:product_status)
class Inventory < ActiveRecord::Base 
 enum product_status: { interim_product: 0, working_product: 1, final_product: 2, fail_product: 3 }
end

Inventory.order(:product_status)
class Inventory < ActiveRecord::Base 
 enum product_status: { interim_product: 0, working_product: 1, final_product: 2, fail_product: 3 }
end

Inventory.order(:product_status) có phải role là admin không
user = User.create email: ‘abc_admin@luudai.com’, role: :admin
user.admin? ⇒ true
user = User.create email: ‘abc_employee@luudai.com’, role: :employee
user.admin? ⇒ false

.admin : Hàm này lấy ra các bản ghi có role là admin.
User.admin ⇒ Truy vấn ra các bản ghi có role admin

Ngoài ra enum còn cung cấp validate cho enum. Ví dụ trên khi ta tạo user = User.create email: ‘abc@luudai.com’, role: ‘invalid’ sẽ raise lỗi ArgumentError
Vào trong source code đọc thì thấy:
def assert_valid_value(value)
  unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
    raise ArgumentError, "'#{value}' is not a valid #{name}"
  end
end

Enum trong Rails 7 

Vậy trong Rails 7 enum có thêm tính năng gì mới: 
  • in_order_of: Hàm này cho phép ta sort các giá trị trong database theo một order nhất định.

    Để cho dễ hiểu hơn mình sẽ lấy một vấn đề nhỏ có thể gặp phải: Mình có một bảng quản lý hàng tồn kho (inventories) với một trường enum trạng thái tồn kho product_status . Trường product_status có thể nhận 3 giá trị interim_product, working_product, final_product, fail_product với ý nghĩa là iterim_product là hàng được nhập về và chưa trải qua công đoạn sản xuất nào cả, working_product là hàng đang được sản xuất cuối cùng là final_product là hàng đã được sản xuất thành công và sẵn sàng để bán ngoài thị trường trong khi fail_product là hàng trong quá trình sản xuất bị lỗi và không thể bán ngoài thị trường. 

    Ban đầu khách hàng yêu cầu màn hình list inventories được sắp xếp theo thứ tự các interim_product, working_product, final_product, fail_product nên mình quyết định đặt enum như sau để làm order cho đơn giản: 

class Inventory < ActiveRecord::Base 
	enum product_status: { interim_product: 0, working_product: 1, final_product: 2, fail_product: 3 }
end

Inventory.order(:product_status)
Sau khi dự án chạy được một thời gian khách hàng dùng thử sản phẩm khách hàng quyết định đưa fail_product lên vị trí ngay dưới interim_product vì các sản phẩm fail có thể được tái sử dụng để sản xuất lại nên đặt sp này lên trước :’( interim_product → fail_product → working_product →final_product. 
Vậy là từ một câu query đơn giản giờ mình phải viết lại một scope dài ngoằng (Ở đây mình không dùng cách sắp xếp lại cho fail_product lên giá trị 2 vì hiện đã có data cũ trong database rồi nếu muốn đảo fail_product lên thì phải chạy rake update hàng loạt dữ liệu cũ + mai sau khách hàng còn có thể thay đổi thứ tự trên màn hình này nhiều mỗi lần chạy rake với lượng dữ liệu lớn sẽ mất rất nhiều thời gian). 

sql = <<-SQL.squish
      CASE status
        WHEN 'draft' THEN 1
        WHEN 'publish' THEN 2
      END ASC,
      created_at DESC
    SQL

order(Arel.sql(sql)) 
Updated: 27/01/2023

Tuy nhiên từ rails 7 đã có hỗ trợ cho trường hợp này: Inventory.in_order_of(:status, %w[interim_product fail_product working_product final_prouduct]). Câu lệnh này sẽ gen ra câu query giống hệt với scope bên trên. Tuy nhiên từ rails 7 đã có hỗ trợ cho trường hợp này: Inventory.in_order_of(:status, %w[interim_product fail_product working_product final_prouduct]). 

Lưu ý khi sử dụng enum trong Rails 7 

Nên dùng enum với 1 hash thay vì dùng dạng %i[a b c]. Khi dùng %i[a b c] Rails sẽ tự động đánh value theo index của mảng nên sau này thêm mới một giá trị vào rất dễ bị lỗi ảnh hưởng data cũ.