読者です 読者をやめる 読者になる 読者になる

ActiveRecord で複合プライマリーキーのテーブルを扱う

Rails

Rails 移行検証の過程で既存のテーブルを操作してみようとしたら,一部のテーブルで .find がエラーになってハマったのでメモ程度に残しておく.原因としては id と別のカラムの複合プライマリーキーになっていることで,Rails は複合プライマリーキーを許容していなかった.そんなこと知らないよー!って感じ.

単一プライマリーキーの場合

サンプルとして id をプライマリーキーとした teams テーブルを作成する.スキーマが雑だけど許して!

CREATE TABLE `teams` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

適当にデータを投入して ActiveRecord から .find で取得してみる.当たり前だけど取得できる.

pry(main)> Team.find(1)
  Team Load (0.8ms)  SELECT  `teams`.* FROM `teams` WHERE `teams`.`id` = 1 LIMIT 1
#<Team:0x007fee2ddc8d18> {
  :id         => 1,
  :user_id    => 1,
  :name       => "A",
  :created_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00,
  :updated_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00
}

複合プライマリーキーの場合

次に iduser_id をプライマリーキーとした teams テーブルを作成する.

CREATE TABLE `teams` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`, `user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

複合プライマリーキーだと ActiveRecord.find が使えないことがわかる.

pry(main)> Team.find(1)
ActiveRecord::UnknownPrimaryKey: Unknown primary key for table teams in model Team.

.where(id: 1).first で回避することもできるけど微妙過ぎる.

pry(main)> Team.where(id: 1).first
  Team Load (0.8ms)  SELECT  `teams`.* FROM `teams` WHERE `teams`.`id` = 1 LIMIT 1
#<Team:0x007fee3018efb8> {
  :id         => 1,
  :user_id    => 1,
  :name       => "A",
  :created_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00,
  :updated_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00
}

解決策1 : composite_primary_keys

composite_primary_keys を使って複合プライマリーキーに対応できた.ただコードに違和感があってメリットが感じられなかった.

お決まりのように Gem を追加する.

gem 'composite_primary_keys'

モデルに primary_keys を定義する.

class Team < ActiveRecord::Base
  self.primary_keys = :id, :user_id
end

.find([:key1, :key2]) という構文で使えるようになる.嫌だー!絶対に嫌だー!!!

[16] pry(main)> Team.find([1, 1])
  Team Load (0.7ms)  SELECT  `teams`.* FROM `teams` WHERE (`teams`.`id` = 1 AND `teams`.`user_id` = 1) LIMIT 1
#<Team:0x007fee29b6a020> {
  :id         => [
    [0] [
      [0] 1,
      [1] 1
    ],
    [1] 1
  ],
  :user_id    => 1,
  :name       => "A",
  :created_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00,
  :updated_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00
}

解決策2 : スキーマリファクタリングして単一プライマリーキーにしちゃう

技術的負債を抱えながら開発を進めるぐらいなら先に解消しちゃおう!っていう考え方.

プライマリーキーを削除しようとすると auto_increment だから消せないよって怒られた.

mysql> ALTER TABLE teams DROP PRIMARY KEY;
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key

プライマリーキーの削除と追加を1個の SQL で書けばできる.

mysql> ALTER TABLE teams DROP PRIMARY KEY, ADD PRIMARY KEY(id);
Query OK, 2 rows affected (0.02 sec)
Records: 2  Duplicates: 0  Warnings: 0

これで幸せになれそう!

pry(main)> Team.find(1)
  Team Load (0.6ms)  SELECT  `teams`.* FROM `teams` WHERE `teams`.`id` = 1 LIMIT 1
#<Team:0x007fee2d951140> {
  :id         => 1,
  :user_id    => 1,
  :name       => "A",
  :created_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00,
  :updated_at => Thu, 01 Jan 2015 00:00:00 UTC +00:00
}

複合プライマリーキーを持ったテーブル一覧を検索する

参考情報だけど,こんな SQL で調べられるはず!

mysql> SELECT TABLE_NAME, COUNT(*)
    -> FROM information_schema.KEY_COLUMN_USAGE
    -> WHERE
    ->   CONSTRAINT_SCHEMA = 'xxx' AND
    ->   CONSTRAINT_NAME = 'PRIMARY'
    -> GROUP BY TABLE_NAME
    -> HAVING COUNT(*) > 1;