1. 什么是复合主键?
有时,单个列的值不足以唯一标识表的每一行,需要两列或更多列的组合。这在使用没有单个 id 列作为主键的传统数据库模式时可能出现,或者在修改分片或多租户的模式时可能出现。
复合主键会增加复杂性,并且可能比单个主键列慢。在使用复合主键之前,请确保您的用例需要它。
2. 复合主键迁移
您可以通过将 :primary_key 选项以数组值传递给 create_table 来创建具有复合主键的表
class CreateProducts < ActiveRecord::Migration[8.1]
def change
create_table :products, primary_key: [:store_id, :sku] do |t|
t.integer :store_id
t.string :sku
t.text :description
end
end
end
3. 查询模型
3.1. 使用 #find
如果您的表使用复合主键,则在使用 #find 查找记录时需要传递一个数组
# Find the product with store_id 3 and sku "XYZ12345"
irb> product = Product.find([3, "XYZ12345"])
=> #<Product store_id: 3, sku: "XYZ12345", description: "Yellow socks">
上述内容的 SQL 等效项是
SELECT * FROM products WHERE store_id = 3 AND sku = "XYZ12345"
要查找具有复合 ID 的多条记录,请将一个数组的数组传递给 #find
# Find the products with primary keys [1, "ABC98765"] and [7, "ZZZ11111"]
irb> products = Product.find([[1, "ABC98765"], [7, "ZZZ11111"]])
=> [
#<Product store_id: 1, sku: "ABC98765", description: "Red Hat">,
#<Product store_id: 7, sku: "ZZZ11111", description: "Green Pants">
]
上述内容的 SQL 等效项是
SELECT * FROM products WHERE (store_id = 1 AND sku = 'ABC98765' OR store_id = 7 AND sku = 'ZZZ11111')
具有复合主键的模型在排序时也将使用完整的复合主键
irb> product = Product.first
=> #<Product store_id: 1, sku: "ABC98765", description: "Red Hat">
上述内容的 SQL 等效项是
SELECT * FROM products ORDER BY products.store_id ASC, products.sku ASC LIMIT 1
3.2. 使用 #where
#where 的哈希条件可以用元组式语法指定。这对于查询复合主键关系很有用
Product.where(Product.primary_key => [[1, "ABC98765"], [7, "ZZZ11111"]])
3.2.1. 带有 :id 的条件
在指定 find_by 和 where 等方法的条件时,使用 id 将与模型上的 :id 属性匹配。这与 find 不同,其中传入的 ID 应该是一个主键值。
在 :id 不是主键的模型(例如复合主键模型)上使用 find_by(id:) 时要小心。请参阅 Active Record 查询指南以了解更多信息。
4. 具有复合主键的模型之间的关联
Rails 通常可以推断关联模型之间主键-外键关系。然而,在处理复合主键时,Rails 通常默认只使用复合键的一部分,通常是 id 列,除非明确指示。此默认行为仅在模型的复合主键包含 :id 列,并且该列对所有记录都是唯一的情况下才有效。
考虑以下示例
class Order < ApplicationRecord
self.primary_key = [:shop_id, :id]
has_many :books
end
class Book < ApplicationRecord
belongs_to :order
end
在此设置中,Order 具有由 [:shop_id, :id] 组成的复合主键,并且 Book 属于 Order。Rails 将假定 :id 列应用作订单与其图书之间关联的主键。它将推断图书表上的外键列是 :order_id。
下面我们创建一个 Order 和一本与之关联的 Book
order = Order.create!(id: [1, 2], status: "pending")
book = order.books.create!(title: "A Cool Book")
要访问图书的订单,我们重新加载关联
book.reload.order
这样做时,Rails 将生成以下 SQL 来访问订单
SELECT * FROM orders WHERE id = 2
您可以看到 Rails 在其查询中使用了订单的 id,而不是 shop_id 和 id。在这种情况下,id 就足够了,因为模型的复合主键确实包含 :id 列,并且该列对所有记录都是唯一的。
但是,如果上述要求未满足或您想在关联中使用完整的复合主键,您可以在关联上设置 foreign_key: 选项。此选项指定关联上的复合外键;查询相关记录时将使用外键中的所有列。例如
class Author < ApplicationRecord
self.primary_key = [:first_name, :last_name]
has_many :books, foreign_key: [:first_name, :last_name]
end
class Book < ApplicationRecord
belongs_to :author, foreign_key: [:author_first_name, :author_last_name]
end
在此设置中,Author 具有由 [:first_name, :last_name] 组成的复合主键,并且 Book 属于 Author,具有复合外键 [:author_first_name, :author_last_name]。
创建一名 Author 和一本与之关联的 Book
author = Author.create!(first_name: "Jane", last_name: "Doe")
book = author.books.create!(title: "A Cool Book", author_first_name: "Jane", author_last_name: "Doe")
要访问图书的作者,我们重新加载关联
book.reload.author
Rails 现在将在 SQL 查询中使用复合主键中的 :first_name 和 :last_name
SELECT * FROM authors WHERE first_name = 'Jane' AND last_name = 'Doe'
5. 复合主键模型的表单
也可以为复合主键模型构建表单。有关表单构建器语法的更多信息,请参阅 表单助手指南。
给定一个具有复合键 [:author_id, :id] 的 @book 模型对象
@book = Book.find([2, 25])
# => #<Book id: 25, title: "Some book", author_id: 2>
以下表单
<%= form_with model: @book do |form| %>
<%= form.text_field :title %>
<%= form.submit %>
<% end %>
输出
<form action="/books/2_25" method="post" accept-charset="UTF-8" >
<input name="authenticity_token" type="hidden" value="..." />
<input type="text" name="book[title]" id="book_title" value="My book" />
<input type="submit" name="commit" value="Update Book" data-disable-with="Update Book">
</form>
请注意,生成的 URL 包含用下划线分隔的 author_id 和 id。提交后,控制器可以从参数中提取主键值并更新记录。有关更多详细信息,请参阅下一节。
6. 复合键参数
复合键参数在一个参数中包含多个值。因此,我们需要能够提取每个值并将其传递给 Active Record。我们可以利用 extract_value 方法来处理这种情况。
给定以下控制器
class BooksController < ApplicationController
def show
# Extract the composite ID value from URL parameters.
id = params.extract_value(:id)
# Find the book using the composite ID.
@book = Book.find(id)
# use the default rendering behavior to render the show view.
end
end
和以下路由
get "/books/:id", to: "books#show"
当用户打开 URL /books/4_2 时,控制器将提取复合键值 ["4", "2"] 并将其传递给 Book.find 以在视图中渲染正确的记录。extract_value 方法可用于从任何分隔参数中提取数组。
7. 复合主键夹具
复合主键表的夹具与普通表非常相似。当使用 id 列时,可以像往常一样省略该列
class Book < ApplicationRecord
self.primary_key = [:author_id, :id]
belongs_to :author
end
# books.yml
alices_adventure_in_wonderland:
author_id: <%= ActiveRecord::FixtureSet.identify(:lewis_carroll) %>
title: "Alice's Adventures in Wonderland"
但是,为了支持复合主键关系,您必须使用 composite_identify 方法
class BookOrder < ApplicationRecord
self.primary_key = [:shop_id, :id]
belongs_to :order, foreign_key: [:shop_id, :order_id]
belongs_to :book, foreign_key: [:author_id, :book_id]
end
# book_orders.yml
alices_adventure_in_wonderland_in_books:
author: lewis_carroll
book_id: <%= ActiveRecord::FixtureSet.composite_identify(
:alices_adventure_in_wonderland, Book.primary_key)[:id] %>
shop: book_store
order_id: <%= ActiveRecord::FixtureSet.composite_identify(
:books, Order.primary_key)[:id] %>