このガイドでは、データベーステーブルで利用できる複合主キー(composite primary keys)について紹介します。
このガイドの内容:
テーブルのすべての行を一意に識別するために単一のカラム値だけでは不十分な場合、2つ以上のカラムの組み合わせが必要になることがあります。このような状況は、主キーとして単一のid
カラムを持たないレガシーなデータベーススキーマを使わなければならない場合や、シャーディング/マルチテナンシー向けにスキーマを変更する場合に該当します。
複合主キーを導入すると複雑になり、単一の主キーカラムよりも遅くなる可能性があります。複合主キーを使う前に、そのユースケースでどうしても必要であることを確認しておきましょう。
create_table
に:primary_key
オプションで配列の値を渡すことで、複合主キーを持つテーブルを作成できます。
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
#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は多くの場合、追加情報を必要とせずに、複合主キーをモデル間の関連付けで「主キー〜外部キー」情報を推論できます。以下の例をご覧ください。
class Order < ApplicationRecord self.primary_key = [:shop_id, :id] has_many :books end class Book < ApplicationRecord belongs_to :order end
ここでRailsは、1件のorder(注文)とそのbooks(本)の関連付けの主キーに:id
カラムが使われると仮定します。これは、通常のhas_many
関連付けやbelongs_to
関連付けと同様です。Railsは、books
テーブル上の外部キーカラムが:order_id
であると推測します。
ある本の注文に、以下のようにアクセスするとします。
order = Order.create!(id: [1, 2], status: "pending") book = order.books.create!(title: "A Cool Book") book.reload.order
この場合、以下のSQLを生成してorderにアクセスします。
SELECT * FROM orders WHERE id = 2
これが期待通りに動作するのは、このモデルの複合主キーに:id
カラムが含まれており、かつ:id
カラムがすべてのレコードで一意である場合だけです。関連付けで完全な複合主キーを使うには、その関連付けでquery_constraints
オプションを設定してください。このオプションは、関連付けられるレコードをクエリするときに複合外部キーを指定します。例:
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
以下のように、ある本のauthor(著者)にアクセスするとします。
author = Author.create!(first_name: "Jane", last_name: "Doe") book = author.books.create!(title: "A Cool Book") book.reload.author
この場合、以下のように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, 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] %>
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。