1 什么是复合主键?
有时,单个列的值不足以唯一标识表的每一行,需要组合两个或多个列。当使用没有单个 id
列作为主键的旧版数据库模式时,或者当为分片或多租户修改模式时,可能会出现这种情况。
复合主键会增加复杂性,并且可能比单个主键列速度更慢。在使用复合主键之前,请确保您的用例需要复合主键。
2 复合主键迁移
您可以通过将 :primary_key
选项传递给 create_table
并使用数组值来创建一个带有复合主键的表
class CreateProducts < ActiveRecord::Migration[8.0]
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
不同,在 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 behaviour to render the show view.
end
end
以及以下路由
get "/books/:id", to: "books#show"
当用户打开 URL /books/4_2
时,控制器将提取复合键值 ["4", "2"]
并将其传递给 Book.find
以在视图中渲染正确的记录。extract_value
方法可用于从任何分隔参数中提取数组。
7 复合主键 Fixture
复合主键表的 Fixture 与普通表非常相似。使用 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] %>