更多内容请访问 rubyonrails.org:

复合主键

本指南介绍了数据库表的复合主键。

阅读本指南后,您将能够

  • 创建具有复合主键的表
  • 查询具有复合主键的模型
  • 使您的模型能够将复合主键用于查询和关联
  • 为使用复合主键的模型创建表单
  • 从控制器参数中提取复合主键
  • 为具有复合主键的表使用数据库夹具

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_bywhere 等方法的条件时,使用 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_idid。在这种情况下,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_idid。提交后,控制器可以从参数中提取主键值并更新记录。有关更多详细信息,请参阅下一节。

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] %>


回到顶部