enjoy Railsway!! 第6回 Rails 7.1: 複合主キーサポート、とこれまでの主キー

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

Railsでの主キーの設定方法に留め、データベースの設計そのものについては扱いません

目次

基本 (サロゲートキー)

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_idbook_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技術者認定試験運営委員会 テクニカルアドバイザー
・神奈川工科大学 情報工学科 客員研究員
・鹿児島県 喜界島出身。10歳の頃N88-BASICに触り、コンピューティングにのめり込む
・興味の赴くまま様々なプラットフォーム、言語を楽しみつつ10数年来に渡りRuby on Railsでの開発業務に従事する
・Webサイト https://9uelle.jp/

お知らせ

当委員会ではRails試験を全国300か所で一年中実施をしています。興味がある方は以下をご覧の上、是非受験ください。https://railsce.com/exam

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次