Override Primary key ID trong Rails
Rails được xây dựng trên nguyên tắc Convention over Configuration nghĩa là gần như lập trình viên đã được giảm thiểu tối ta việc tuân thủ convention khi phát triển, thay vào đó bản thân Framework đã làm thay việc đó. Nó bao gồm cả việc cài đặt primary key cho 1 bảng trong database luôn là cột ID. ...
Rails được xây dựng trên nguyên tắc Convention over Configuration nghĩa là gần như lập trình viên đã được giảm thiểu tối ta việc tuân thủ convention khi phát triển, thay vào đó bản thân Framework đã làm thay việc đó. Nó bao gồm cả việc cài đặt primary key cho 1 bảng trong database luôn là cột ID. Tuy nhiên nếu chúng ta muốn sử dụng một cột khác làm primary key thì thế nào ? Trong bài hôm nay mình sẽ hướng dẫn các bạn cách để làm điều đó. Giả sử mình có 1 bảng là Product, mỗi record có 1 chuối ID đặc biệt, mình tạm gọi là sku, chuỗi ID này là unique và thay vì truy vấn theo id như thường lệ thì database sẽ truy vấn dự theo chuỗi này. Mình tạo 1 scaffold như sau
$ rails g scaffold products sku
What is primary key?
Đầu tiên chúng ta sẽ cùng tìm hiểu 1 primary key là gì ? Một cột được gọi là một primary key nếu:
- Tồn tại ràng buộc not NULL
- Tồn tại ràng buộc UNIQUE
- Được đánh INDEX Chúng ta có thể chứng minh điều đó bằng cách kiểm tra trong DB:
$ rails db psql (9.3.4) Type "help" for help. webstote_development=# d products Table "public.products" Column | Type | Modifiers ------------+-----------------------------+------------------------------------------------------- id | integer | not null default nextval('products_id_seq'::regclass) sku | character varying(255) | created_at | timestamp without time zone | updated_at | timestamp without time zone | Indexes: "products_pkey" PRIMARY KEY, btree (id)
Migration
Bây giờ chúng ta cùng xem qua file migration
class CreateProducts < ActiveRecord::Migration def change create_table :products do |t| t.string :sku t.timestamps end end end
Theo Convention thì chúng ta k thể thấy bất cứ thông tin gì về cột id primary key, cột id này sẽ được tự động tạo khi tiến hành tạo bảng. Tuy nhiên chúng ta có thể nói cho Rails hiểu không tạo cột id bằng cách thêm đoạn options id: false như sau:
... create_table :products, id: false do |t| ... end ...
Vây làm thế nào để chỉ cho Rails biết chúng ta chỉ định cột sku là 1 primary key. Sau khi tìm hiểu thì mình tìm được hàm ActiveRecord::ConnectionAdapters::TableDefinitiion#primary_key với code như sau:
# File activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb, line 68 def primary_key(name, type = :primary_key, options = {}) column(name, type, options.merge(:primary_key => true)) end
Và mình đã sử dụng nó trong file migration như sau:
create_table :products, id: false do |t| t.primary_key :sku t.timestamps end
Sau khi chạy migrate, mình kiểm tra lại DB của mình, kết quả như sau:
Table "public.products" Column | Type | Modifiers ------------+-----------------------------+-------------------------------------------------------- sku | integer | not null default nextval('products_sku_seq'::regclass) created_at | timestamp without time zone | updated_at | timestamp without time zone | Indexes: "products_pkey" PRIMARY KEY, btree (sku)
Tuy nhiên cột sku đang có datatype là integer, trong khi mình mong muốn nó là 1 string, vì theo convention thì primary key phải là kiểu integer. Mình đã thử các cách sau:
create_table :products, id: false do |t| t.string :sku, primary: true t.timestamps end
hoặc
create_table :products, id: false do |t| t.string :sku, primary_key: true t.timestamps end
nhưng nó vẫn không hoạt động. Bây giờ chúng ta cùng xem lại, như mình nói từ đầu, 1 primary key phải thỏa các điều kiện, là not NULL, UNIQUE, và được đánh INDEX, ok fine, và đây là cách của mình.
class CreateProducts < ActiveRecord::Migration def change create_table :products, id: false do |t| t.string :sku, null: false t.timestamps end add_index :products, :sku, unique: true end end
kiểm tra lại database thử xem nhé
Table "public.products" Column | Type | Modifiers ------------+-----------------------------+----------- sku | character varying(255) | not null created_at | timestamp without time zone | updated_at | timestamp without time zone | Indexes: "index_products_on_sku" UNIQUE, btree (sku)
Mặc dù cột sku không phải là primary key constraint, nhưng nó tương đương như một primary key vậy. Chúng ta sẽ chỉ rõ cho model Product hiểu sku là primary_key.
class Product < ActiveRecord::Base self.primary_key = 'sku' end
Routing
Mặc định của Rails sẽ tạo ra các URL với reference là :id như sau:
bundle exec rake routes Prefix Verb URI Pattern Controller#Action products GET /products(.:format) products#index POST /products(.:format) products#create new_product GET /products/new(.:format) products#new edit_product GET /products/:id/edit(.:format) products#edit product GET /products/:id(.:format) products#show PATCH /products/:id(.:format) products#update PUT /products/:id(.:format) products#update DELETE /products/:id(.:format) products#destroy
Chúng ta có thể thay đổi nó như sau:
# config/routes.rb resources :products, param: :sku
Và kiểm tra lại lần nữa
bundle exec rake routes Prefix Verb URI Pattern Controller#Action products GET /products(.:format) products#index POST /products(.:format) products#create new_product GET /products/new(.:format) products#new edit_product GET /products/:sku/edit(.:format) products#edit product GET /products/:sku(.:format) products#show PATCH /products/:sku(.:format) products#update PUT /products/:sku(.:format) products#update DELETE /products/:sku(.:format) products#destroy
Cần chú ý răng, chúng ta phải thay đổi params trong ProductsController để tìm kiếm theo params[:sku] thay vì params[:id]
# app/controllers/products_controller.rb def set_product @product = Product.find(params[:sku]) end
URI
Một vấn đề nữa, giả như chúng ta có một mã sku điên điên như 'SKU 001', khi chúng ta acess qua ProductsController#show, chúng ta sẽ có đường dẫn như sau:
http://0.0.0.0:3000/products/SKU%23001
Xem URI có vẻ không thân thiện cho lắm, cho nên mình sẽ override lại method #to_param như sau:
class Product < ActiveRecord::Base ... def to_param sku.parameterize end ... end
và URI lúc này sẽ trả về:
http://0.0.0.0:3000/products/SKU-001
Kết luận
Như mình đã nói từ đầu, Rails được xây dựng trên nguyên tắc Convention over Configuration, vì vậy, chỉ trong trường hợp bắt buộc nào đó mới thực hiện việc override primary key, vì nó đang vi phạm convention của Rails. Chúc các bạn ăn tết vui vẻ