更多内容请访问 rubyonrails.org:

复合主键

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

阅读本指南后,您将能够

  • 创建带有复合主键的表
  • 查询带有复合主键的模型
  • 使您的模型能够使用复合主键进行查询和关联
  • 为使用复合主键的模型创建表单
  • 从控制器参数中提取复合主键
  • 对带有复合主键的表使用数据库 fixture

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_bywhere 等方法指定条件时,使用 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_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 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] %>


返回顶部