このガイドでは、データベーステーブルで利用できる複合主キー(composite primary keys)について紹介します。
このガイドの内容:
テーブルのすべての行を一意に識別するために単一のカラム値だけでは不十分な場合、2つ以上のカラムの組み合わせが必要になることがあります。このような状況は、主キーとして単一のid
カラムを持たないレガシーなデータベーススキーマを使わなければならない場合や、シャーディング/マルチテナンシー向けにスキーマを変更する場合に該当します。
複合主キーを導入すると複雑になり、単一の主キーカラムよりも遅くなる可能性があります。複合主キーを使う前に、そのユースケースでどうしても必要であることを確認しておきましょう。
create_table
に:primary_key
オプションで配列の値を渡すことで、複合主キーを持つテーブルを作成できます。
class CreateProducts < ActiveRecord::Migration[7.2] 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
#find
の場合テーブルで複合主キーを使っている場合は、レコードを#find
で検索するときに配列を渡す必要があります。
# productを「store_id 3」と「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
に「配列の配列」を渡します。
# productsを主キー「[1, "ABC98765"]と[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')
複合主キーを持つモデルは、ORDER BY(順序付け)でも複合主キー全体を使います。
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
#where
の場合#where
では、以下のようにタプル的な構文でハッシュ条件を指定できます。
これは、複合主キーのリレーションでクエリを実行するときに便利です。
Product.where(Product.primary_key => [[1, "ABC98765"], [7, "ZZZ11111"]])
:id
を指定する場合find_by
やwhere
などのメソッドで条件を指定するときにid
を使うと、モデルの:id
属性と一致します(これは、渡すIDが主キーでなければならないfind
と異なります)。
:id
が主キーでないモデル(複合主キーを使っているモデルなど)でfind_by(id:)
を使う場合は注意が必要です。詳しくはActive Recordクエリガイドを参照してください。
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
カラムを使う必要があると推測し、books
テーブルの外部キーカラムは:order_id
であると推測します。
以下は、Order
とそれに関連付けられたBook
を作成します。
order = Order.create!(id: [1, 2], status: "pending") book = order.books.create!(title: "A Cool Book")
このbook
のorder
にアクセスするために、以下のように関連付けをreload
します。
book.reload.order
このとき、Railsは以下のSQLを生成してorders
にアクセスします。
SELECT * FROM orders WHERE id = 2
このクエリでは、shop_id
とid
の両方を使うのではなく、orderの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_first_name, :author_last_name]
を持つAuthor
モデルに属します。
以下は、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
のauthor
にアクセスするために、以下のように関連付けをreload
します。
book.reload.author
これでRailsは、SQLクエリの複合主キーの:first_name
と:last_name
の両方を使うようになりました。
SELECT * FROM authors WHERE first_name = 'Jane' AND last_name = 'Doe'
複合主キーを持つモデルでもフォームを作成できます。フォームビルダー構文について詳しくは、フォームヘルパーガイドを参照してください。
複合主キー[: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
がアンダースコア区切りの形で含まれていることにご注目ください。
送信後、コントローラーはパラメータから主キーの値を抽出して、単一の主キーと同様にレコードを更新できます。詳しくは次のセクションを参照してください。
れているため、各値を抽出してActive Recordに渡す必要があります。このユースケースでは、extract_value
メソッドを活用できます。
以下のコントローラがあるとします。
class BooksController < ApplicationController def show # URLパラメータから複合ID値を抽出する id = params.extract_value(:id) # この複合IDでbookを検索する @book = Book.find(id) # デフォルトのレンダリング動作でビューを表示する end end
ルーティングは以下のようになっているとします。
get '/books/:id', to: 'books#show'
ユーザーがURL /books/4_2
を開くと、コントローラは複合主キーの値["4", "2"]
を抽出してBook.find
に渡し、ビューで正しいレコードを表示します。extract_value
メソッドは、区切られた任意のパラメータから配列を抽出するのに利用できます。
複合主キーテーブル用のフィクスチャは、通常のテーブルとかなり似ています。 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] %>
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。