モチベーション

Railsでテーブルのidカラム以外にプライマリキーを設定したい。インターネットに転がっている情報で色々やってみたが上手く行かなったので自分なりに調べてまとめる。

背景

Railsはその設計からidをテーブルのプライマリキーに自動的に設定してくれる。しかし、時にはid以外のカラムにプライマリキーの設定をしたい場合もあるだろう。公式ドキュメントやブログ情報によると、そういう時はオプションを指定することで任意のカラムにプライマリキーの設定が可能とのこと。

当初は下に挙げたドキュメントやブログを参考にテーブルのプライマリキーを設定しようとした。しかし、idが自動でプライマリキーに設定されることはなくなったが、 肝心の任意のキーをプライマリキーにするという要件を満たせなかった。

  1. Ruby on Rails 4.1.8
  2. RailsGuides Active Record Basics
  3. 「ActiveRecord」の基本とデータの参照
  4. Railsで規約に沿わない古いデータを扱う
  5. Ruby on Rails - ActiveRecord で規約外のプライマリキーを使用する方法!

どうやら、公式ドキュメント通りだと思った通りの動作をしないようだ。公式ドキュメントが信頼出来ないとなると、どのように記述すればこちらの希望する処理になるかはフレームワークのコードを読まないとわからない。そうなると、フレームワークの意味が無いのだが・・・ただ、これだけ世で使われているフレームワークなので自分の勘違いの可能性もある。動作検証の結果と追加調査をを以下にまとめる。

検証してみた

環境

  • MacOSX 10.9.4
  • Ruby 2.1.2
  • Ruby on Rails 4.18

作りたいテーブル

MySQLで以下のクエリを実行した時に作成されるテーブルと同様のものをrake db:migrateで作成する。

  CREATE TABLE books(
      book_id INTEGER NOT NULL,
      title VARCHAR(250) NOT NULL,
      created_at DATETIME NOT NULL,
      updated_at DATETIME NOT NULL,
      PRIMARY KEY(book_id)
);

通常の手順で作成したスキーマの確認

まずは、何も考えずに普通の手順で作成したテーブルスキーマを確認する。 手順は以下のとおり。

$ rails new book -d mysql
$ rails g model book book_id:integer title:string
$ rake db:create
$ rake db:migrate

作成されたテーブルのスキーマは以下の通り。

mysql>describe books;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| book_id    | int(11)      | YES  |     | NULL    |                |
| title      | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

これを見ると、自動的にidカラムが作成されており、idカラムのみプライマリキーになっている。

公式ドキュメントの設定を検証する

Ruby on Rails 4.1.8には以下のように記述されている。

The options hash can include the following keys:
:id
Whether to automatically add a primary key column. Defaults to true. Join tables for has_and_belongs_to_many should set it to false.
:primary_key
The name of the primary key, if one is to be added automatically. Defaults to id. If :id is false this option is ignored.
Note that Active Record models will automatically detect their primary key. This can be avoided by using self.primary_key= on the model to define the key explicitly.

最後の2行に、”modelを定義しているファイルでself.primary_keyを設定とすると自動でActiveRecordがプライマリキーを検知する”と書かれているので、 以下のようにしてrake db:migrateを実行する。

class Book < ActiveRecord::Base
        self.primary_key = "book_id"
end

primary_keyオプションは任意のプライマリキーを指定するためにあり、デフォルトだとidがプライマリキーになる。 また、id: falseの設定が入っているとこのオプションは無視されるらしい。なので、idオプションをfalseにしないで、primary_keyの値を変更し、Modelの定義でself.primary_key = :book_idを設定しrake db:migrateを実行した。

➤  rake db:migrate
== 20141128014807 CreateBooks: migrating ======================================
-- create_table(:books, {:primary_key=>"book_id"})
rake aborted!
StandardError: An error has occurred, all later migrations canceled:

you can't redefine the primary key column 'book_id'. To define a custom primary key, pass { id: false } to create_table./Users/takayuki/Dropbox/002_Study/ruby_on_rails_study/test/books3/db/migrate/20141128014807_create_books.rb:4:in `block in change'

エラーがでてきた。なになに、idをfalseにしてください・・・? idをfalseにしたらprimary_keyオプション無視するんじゃないの?英語の解釈間違えているのだろうか?

マイグレーションファイルを書き換えて、

class CreateBooks  describe books;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| book_id    | int(11)      | YES  |     | NULL    |       |
| title      | varchar(255) | YES  |     | NULL    |       |
| created_at | datetime     | YES  |     | NULL    |       |
| updated_at | datetime     | YES  |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

プライマリーキーが設定されてない!!!

プライマリキーを設定する方法の調査2

公式ドキュメントの方法では任意のカラムにプライマリキーを設定できなかった。 多くのドキュメントやブログでプライマリキーの設定方法を載せているが、ほとんど上で試した設定方法が紹介されている。

一方、以下の情報サイトでは違う方法でプライマリキーを設定している。

その中から、プライマリキーの設定が正常にできたものを2つ挙げる。

プライマリキーの設定が上手く行った例1

このパターンではexecuteによりSQL文を発行してプライマリーキーの設定を行っている。 試していないが、他のSQLの発行にも使えると思うのでSQL書くのに慣れている人はこの形式が良いのではないだろうか。

Migrationファイル

class CreateBooks < ActiveRecord::Migration
  def change
    create_table :books, id: false do |t|
      t.integer :book_id
      t.string :title

      t.timestamps
    end
    execute "ALTER TABLE books ADD PRIMARY KEY (book_id);"
  end
end

テーブルスキーマの確認

mysql&gt; describe books;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| book_id    | int(11)      | NO   | PRI | 0       |       |
| title      | varchar(255) | YES  |     | NULL    |       |
| created_at | datetime     | YES  |     | NULL    |       |
| updated_at | datetime     | YES  |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+

プライマリキーの設定が上手く行った例2

このパターンではt.columnの後にカラム名とオプションを指定することで、 任意のカラムにプライマリーキーを紐付けている。しかし、直感的にわかりにくいので、個人的にはexecuteの後にプライマリキーを指定する方法を採用したい。

Migrationファイル

class CreateBooks < ActiveRecord::Migration
  def change
    create_table :books, id: false do |t|
      t.column :book_id, 'INTEGER PRIMARY KEY AUTO_INCREMENT'
      t.string :title

      t.timestamps
    end
  end
end

テーブルスキーマの確認

mysql&gt; describe books;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| book_id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

注意すべきなのはデータベースごとにSQLの書き方が異なるので、'INTEGER PRIMARY KEY AUTO_INCREMENT'のように直接指定するときは意識すること。 例えば、オートインクリメントオプションを付けるときは、AUTOINCREMENT(sqlite3)なのかAUTO_INCREMENT(MySQL)なのかを意識すること。

結論

プライマリキーやauto_incrementの設定などはexecute "ALTER TABLE books ADD PRIMARY KEY (book_id);"のようにSQLを実行させる方法を採用した方が間違いがない。 ちょっと後でコードを読んで処理内容を確認しよう。