12/08/2018, 15:31

Kết hợp Primary keys cho ActiveRecords

1. Giới thiệu Trong một vài trường hợp, khi cần thao tác với bảng trung gian chứa khóa ngoại đến các bảng khác, chúng ta có thể không để primary_key id. Nguyên nhân là do số lượng record trong bảng này tăng rất nhanh nên giá trị của id sẽ sớm vượt giới hạn lưu trữ, nên thông thường, bảng trung ...

1. Giới thiệu

Trong một vài trường hợp, khi cần thao tác với bảng trung gian chứa khóa ngoại đến các bảng khác, chúng ta có thể không để primary_key id. Nguyên nhân là do số lượng record trong bảng này tăng rất nhanh nên giá trị của id sẽ sớm vượt giới hạn lưu trữ, nên thông thường, bảng trung gian như này sẽ bỏ cột id và các record được xác định thông qua các khóa ngoại.

Ví dụ: ta tạo các bảng sau:

# tạo bảng users
  def up
    create_table :users do |t|
    	t.string :name
    end
  end
  
  # tạo bảng products 
    def up
    create_table :products do |t|
    	t.string :name 
    end
  end

giả sử ta có bảng trung gian orders

  def up
    create_table :orders, id: false do |t|
    	t.references :product 
        t.references :user
        t.integer :status 
        
        t.index [:product_id, :user_id], unique: true
    end
  end

Nếu muốn tìm kiếm trong bảng orders, ta có thể sử dụng lệnh where

Order.where(product_id: 1, user_id: 1).first

khi muốn update 1 record thì phải sử dụng update_all

Order.where(product_id: 1, user_id: 1).update_all status: 1

Tuy nhiên, có một vài project sẽ hạn chế không cho dùng update_all, ví dụ chạy gem rubocop chẳng hạn, bởi vì như ta đã biết update_all sẽ không chạy callback. Nếu sử dụng update_attributes thì sẽ gây ra lỗi

Order.where(product_id: 1, user_id: 1).first.update_attributes status: 1
=> TypeError: nil is not a symbol nor a string

Sử dụng hàm destroy cũng gây ra lỗi tương tự. Nguyên nhân là do update_attributes và destroy yêu cầu phải có cột primary_key để xác định record , nhưng ActiveRecord hiện không hỗ trợ kết hợp primary keys

Ngoài ra, việc tạo quan hệ với các bảng khác sẽ khó khăn, vì bảng orders được định nghĩa thông qua 2 khóa ngoại.

Gem composite_primary_keys hỗ trợ khả năng kết hợp 2 khóa để tạo thành primary_key cho table.

2. Cài đặt

Chúng ta cài gem bằng lệnh

gem install composite_primary_keys

hay thêm vào trong file Gemfile

gem 'composite_primary_keys'

và chạy bundle install

Ta thêm cài đặt primary_key trong model order

class Order < ApplicationRecord
  self.primary_key = :user_id, :product_id

  belongs_to :user
  belongs_to :product
end

primary_key sẽ được định nghĩa theo 2 khóa ngoại self.primary_key = :user_id, :product_id

Ta chạy lệnh

Order.primary_key  # => ["user_id", "product_id"]
Order.primary_key.to_s # => "user_id,product_id"

Khi đó, để tìm 1 bản ghi order ta truyền mảng khóa ngoại vào hàm find

Order.find([1,1])
# lệnh trên sẽ tạo ra query 
SELECT  `orders`.* FROM `orders` WHERE (`orders`.`user_id` = 1 AND `orders`.`product_id` = 1) LIMIT 1

Thực hiện update và destroy

Order.find([1,1]).update_attributes status: 1
UPDATE `orders` SET `status` = 1 WHERE (`orders`.`user_id` = 1 AND `orders`.`product_id` = 1)

Order.find([1,1]).destroy 
DELETE FROM `orders` WHERE `orders`.`user_id` = 1 AND `orders`.`product_id` = 1

Tạo quan hệ với bảng khác:

Ta thêm quan hệ với bảng notifications

  def change
    create_table :notifications do |t|
    	t.references :user
    	t.references :product
    	t.datetime :nofified_date
    end
  end

để định nghĩa quan hệ has_many của order với notifications, ta dùng:

# order.rb
has_many :notifications, foreign_key: [:user_id, :product_id]

# notification.rb
class Notification < ApplicationRecord
	belongs_to :order, foreign_key: [:user_id, :product_id]
end

Join giữa các bảng như table bình thường

Order.joins(:notifications)
SELECT `orders`.* FROM `orders` INNER JOIN `notifications` ON `notifications`.`user_id` = `orders`.`user_id` AND `notifications`.`product_id` = `orders`.`product_id`

Lưu ý Chỉ nên dùng gem composite_primary_keys với các bảng có khóa ngoại unique. Tài liệu hướng dẫn: composite_primary_keys

0