2023年10月5日、Rails 7.1がリリースされました。
(その後、10月11日に7.1.1がリリースされています)
前回のコラムでは追加された機能の概要と、そのうちアプリケーション構築に直接影響がありそうなところ、「独自の認証機能構築の改善」についてご紹介しました。
今回はもう一つ直接影響がありそうなところ、「複合主キーサポート」について見ていきたいと思います。
その前に、まずはRails(Active Record)における主キーについて振り返ってみましょう。
※このコラムのプログラム等は以下の環境で動作確認しています
(2023/10/31 最終確認)
ruby: ruby 3.2.2 [arm64-darwin22]
rails: rails (7.1.1)
trilogy 2.6.0
annotate 3.2.0
MariaDB 11.0.2
基本 (サロゲートキー)
Active Recordはマイグレーションを通してデータベーススキーマを操作します。
もっとも基本的なマイグレーションファイルは次のようになります。
db/migrate/create_books.rb
class CreateBooks < ActiveRecord::Migration[7.1]
def change
create_table :books do |t|
t.string :title
t.timestamps
end
end
end
このようにcreate_table
にオプションを特に指定しない場合、自動的にidカラムが主キーとして追加されます。
このidは自動的に採番され、通常意味を持ちません。
こういった主キーは「サロゲートキー(代理キー)」と呼ばれます。
Gem annotateを利用すると、スキーマを次のような形で確認できます。
# == Schema Information
#
# Table name: books
#
# id :bigint not null, primary key
# title :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
ランダムな値を設定する
「基本」で自動的に採番されるidは通常連番になります。
自然にRailsアプリケーションを構築した場合、このidは様々なところに現れます。もっともわかりやすい例はURLでしょう。
これは「idが予測可能である」「レコードの件数を推測可能である」といった不都合となる場合があります。
対策のひとつとして、idにランダムな値を設定する方法があります。
ランダムな値を設定するとしても、主キーは一意にする必要があります。ここでは重複を考慮しなくて良いSecureRandom.uuidを使ってみましょう。
マイグレーションファイルは次のようになります。
db/migrate/create_books.rb
class CreateBooks < ActiveRecord::Migration[7.1]
def change
create_table :books, id: :string do |t|
t.string :name
t.timestamps
end
end
end
create_table
のオプションid
で主キーの型:string
を設定し、UUIDを保存できるようにします。
スキーマは次のようになります。
# == Schema Information
#
# Table name: books
#
# id :string(255) not null, primary key
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
詳細なカラム設定
ハイフンを含むUUIDは36桁です。id: :string
と指定した場合、カラムの大きさはデフォルトの255文字となり大きすぎるとも言えます。
これらを詳細に設定したい場合、次のようなマイグレーションファイルで実現できます。
db/migrate/create_books.rb
class CreateBooks < ActiveRecord::Migration[7.1]
def change
create_table :books, id: false do |t|
t.string :id, primary_key: true, limit: 36
t.string :name
t.timestamps
end
end
end
create_table
のオプションにid: false
を指定すると、idカラムが自動生成されなくなります。
ブロック内に自身でidカラムの定義を記述することで、詳細なカラム設定ができます。primary_key: true
を忘れないようにしてください。
スキーマを確認すると、idの長さが設定されていることがわかります。
# == Schema Information
#
# Table name: books
#
# id :string(36) not null, primary key
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
UUIDを自動設定する
「基本」の場合idは自動採番されますが、自身で主キーを設定した場合はその値も自身で設定する必要があります。
これはモデル内でbefore_create
を利用することで自動化することができます。
次のコードはその一例です。
app/models/book.rb
class Book < ApplicationRecord
before_create :set_id
private
def set_id
self.id = SecureRandom.uuid
end
end
このように任意のプログラムで値を設定できますので、素のUUIDを設定するほかにもidの値を自在にコントロールすることができます。
その際も「必ず一意になるようにする」という制約には注意しましょう。
任意のカラムを設定する (ナチュラルキー)
データベース設計をおこなう際に主キーとしてサロゲートキーでは無く意味のある値、「ナチュラルキー(自然キー)」を採用したい状況があります。
例えば「出版されている本」の場合、ISBNという一意の値を持っているため、これを主キーとする選択があります。
こういった場合、マイグレーションファイルは次のような形になります。
db/migrate/create_books.rb
class CreateBooks < ActiveRecord::Migration[7.1]
def change
create_table :books, id: :int, primary_key: :isbn do |t|
t.string :name
t.timestamps
end
end
end
create_table
のオプションid
で主キーの型:int
を設定し、さらにprimary_key
で主キーカラムの名前isbn
を指定しています。
スキーマは次のようになります。
# == Schema Information
#
# Table name: books
#
# isbn :integer not null, primary key
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
複合主キー
主キーについて振り返ったところで、Rails 7.1で利用できるようになった複合主キーを見ていきましょう。
ここまでにあげた主キーは「ひとつのカラムによってひとつのレコードが一意に定まる」ものですが、複合主キーは「複数のカラムの組み合わせによって一つのレコードが一意に定まる」ものです。
データベースの設計次第では、複合主キーを採用することになる状況があります。
また、テーブル同士の多対多の関係を表す中間テーブルでは複合主キーを活用する状況があります。
「注文伝票Order」と「本Book」、「注文詳細OrderDetail」について考えてみます。
このとき、OrderDetailはorder_id
とbook_id
で一意になります。
これをマイグレーションファイルで表すと、次のようになります。
db/migrate/create_order_details.rb
class CreateOrderDetails < ActiveRecord::Migration[7.1]
def change
create_table :order_details, primary_key: [:order_id, :book_id] do |t|
t.integer :order_id
t.integer :book_id
t.integer :quantity, null: false
t.timestamps
end
end
end
スキーマは次のようになります。
primary keyが複数存在することが確認できます。
# == Schema Information
#
# Table name: order_details
#
# quantity :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# book_id :integer not null, primary key
# order_id :integer not null, primary key
#
#find
複合主キーを設定したレコードをfind
メソッドで検索する場合、検索する値の配列を渡す必要があります。
この配列はprimary_key
で指定した順番通りに設定する必要があります。
# primary_key: [:order_id, :book_id]
# order_id: 1, book_id: 2で検索
OrderDetail.find([1, 2])
まとめ
Rails 7.1の複合主キーサポートに加え、Rails(Active Record)でのこれまでの主キーについても見ていきました。
Railsの標準機能の範囲でも、主キーについてはいくつか選択肢があることをご理解いただけたかと思います。
主キーをどのように設定するか、は奥深い議論がありここでは取り上げませんが、状況に応じた選択肢を知ることは重要でしょう。
とはいえ、Railsの基本は「omakase」です。
必要以上に複雑化させないように気をつけましょう。
お知らせ
当委員会ではRails試験を全国300か所で一年中実施をしています。興味がある方は以下をご覧の上、是非受験ください。https://railsce.com/exam