Sometimes a single column's value isn't enough to uniquely identify every row
of a table, and a combination of two or more columns is required.
This can be the case when using a legacy database schema without a single
column as a primary key, or when altering schemas for sharding or multitenancy.
Composite primary keys increase complexity and can be slower than a single primary key column. Ensure your use-case requires a composite primary key before using one.
You can create a table with a composite primary key by passing the
:primary_key option to
create_table with an array value:
class CreateProducts < ActiveRecord::Migration[7.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
If your table uses a composite primary key, you'll need to pass an array
#find to locate a record:
# 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">
The SQL equivalent of the above is:
SELECT * FROM products WHERE store_id = 3 AND sku = "XYZ12345"
To find multiple records with composite IDs, pass an array of arrays to
# 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"> ]
The SQL equivalent of the above is:
SELECT * FROM products WHERE (store_id = 1 AND sku = 'ABC98765' OR store_id = 7 AND sku = 'ZZZ11111')
Models with composite primary keys will also use the full composite primary key when ordering:
irb> product = Product.first => #<Product store_id: 1, sku: "ABC98765", description: "Red Hat">
The SQL equivalent of the above is:
SELECT * FROM products ORDER BY products.store_id ASC, products.sku ASC LIMIT 1
Hash conditions for
#where may be specified in a tuple-like syntax.
This can be useful for querying composite primary key relations:
Product.where(Product.primary_key => [[1, "ABC98765"], [7, "ZZZ11111"]])
When specifying conditions on methods like
where, the use
id will match against an
:id attribute on the model. This is different
find, where the ID passed in should be a primary key value.
Take caution when using
find_by(id:) on models where
:id is not the primary
key, such as composite primary key models. See the Active Record Querying
guide to learn more.
Rails is often able to infer the primary key - foreign key information between associated models with composite primary keys without needing extra information. Take the following example:
class Order < ApplicationRecord self.primary_key = [:shop_id, :id] has_many :books end class Book < ApplicationRecord belongs_to :order end
Here, Rails assumes that the
:id column should be used as the primary key for
the association between an order and its books, just as with a regular
belongs_to association. It will infer that the foreign key column
books table is
:order_id. Accessing a book's order:
order = Order.create!(id: [1, 2], status: "pending") book = order.books.create!(title: "A Cool Book") book.reload.order
will generate the following SQL to access the order:
SELECT * FROM orders WHERE id = 2
This only works if the model's composite primary key contains the
and the column is unique for all records. In order to use the full composite
primary key in associations, set the
query_constraints option on the
association. This option specifies a composite foreign key on the association,
meaning that all columns in the foreign key will be used to query the
associated record(s). For example:
class Author < ApplicationRecord self.primary_key = [:first_name, :last_name] has_many :books, query_constraints: [:first_name, :last_name] end class Book < ApplicationRecord belongs_to :author, query_constraints: [:author_first_name, :author_last_name] end
Accessing a book's author:
author = Author.create!(first_name: "Jane", last_name: "Doe") book = author.books.create!(title: "A Cool Book") book.reload.author
:last_name in the SQL query:
SELECT * FROM authors WHERE first_name = 'Jane' AND last_name = 'Doe'
Forms may also be built for composite primary key models. See the Form Helpers guide for more information on the form builder syntax.
@book model object with a composite key
@book = Book.find([2, 25]) # => #<Book id: 25, title: "Some book", author_id: 2>
The following form:
<%= 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>
Note the generated URL contains the
id delimited by an
underscore. Once submitted, the controller can extract primary key values from
the parameters and update the record. See the next section for more details.
Composite key parameters contain multiple values in one parameter.
For this reason, we need to be able to extract each value and pass them to
Active Record. We can leverage the
extract_value method for this use-case.
Given the following controller:
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
And the following route:
get '/books/:id', to: 'books#show'
When a user opens the URL
/books/4_2, the controller will extract the
composite key value
["4", "2"] and pass it to
Book.find to render the right
record in the view. The
extract_value method may be used to extract arrays
out of any delimited parameters.
Fixtures for composite primary key tables are fairly similar to normal tables. When using an id column, the column may be omitted as usual:
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"
However, in order to support composite primary key relationships,
you must use the
class BookOrder < ApplicationRecord self.primary_key = [:shop_id, :id] belongs_to :order, query_constraints: [:shop_id, :order_id] belongs_to :book, query_constraints: [: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] %>
You're encouraged to help improve the quality of this guide.
Please contribute if you see any typos or factual errors. To get started, you can read our documentation contributions section.
You may also find incomplete content or stuff that is not up to date. Please do add any missing documentation for main. Make sure to check Edge Guides first to verify if the issues are already fixed or not on the main branch. Check the Ruby on Rails Guides Guidelines for style and conventions.
If for whatever reason you spot something to fix but cannot patch it yourself, please open an issue.
And last but not least, any kind of discussion regarding Ruby on Rails documentation is very welcome on the official Ruby on Rails Forum.