レールから外れたデータベースを扱う
ActiveRecord 外で作られた DB を ActiveRecord から扱うときに知っておくと便利。
table
table_name=
で設定します。
class User < ApplicationRecord
self.table_name = :user
end
PK
id
ではない
primary_key=
で設定します
class User < ApplicationRecord
self.primary_key = :user_id
end
複合主キー
composite_primary_keys gem を使え!
#id
で配列が返ってくるようになるのが使う上で一番の影響かなーと思っているので、特にそこには気を付けてください。
複合主キー苦手でサッサと移行してしまうのであんまり知見ない。
主キーが無いテーブル
そんなのあるのか。あるんだよ。
幸い、さすがに UNIQUE KEY はあったので、unique なら主キー扱いしても良かろうということで primary_key=
で設定する。
親と同じ主キー
こういうヤツ。
+-----------+
| users |
+-----------+
| PK id int |
+-----+-----+
| has_one
+-------+--------+
| user_profiles |
+----------------+
| PK user_id int |
+----------------+
class User < ApplicationRecord
has_one :user_profile, foreign_key: :user_id
end
class UserProfile < ApplicationRecord
self.primary_key = :user_id
belongs_to :user
end
UserProfile が確実に has_one で存在することも期待したい場合、僕は User に after_create :create_user_profile
をよく生やすんだけど、
User#create_user_profile
はレコードを作った後に has_one association の fkey を UPDATE する動きをする。
このとき auto increment を期待して、user_id を指定せずに INSERT 文を発行して ActiveRecord::NotNullViolation
が発生する。
というわけで事前に一瞬謎の unique な id を入れておくというのをやりました。。
class User < ApplicationRecord
has_one :user_profile, foreign_key: :user_id
after_create :create_user_profile
end
class UserProfile < ApplicationRecord
self.primary_key = :user_id
belongs_to :user
after_initialize :init_pk
# ~~~
# BEGIN
# INSERT INTO `users` (`id`) VALUES (747340676579987456)
# INSERT INTO `user_profiles` (`user_id`) VALUES (747340920571039744) # 一時的に謎な id で INSERT される
# SELECT `user_profiles`.* FROM `user_profiles` WHERE `user_profiles`.`user_id` = 747340676579987456 LIMIT 1
# UPDATE `user_profiles` SET `user_profiles`.`user_id` = 747340676579987456 WHERE `user_profiles`.`user_id` = 747340920571039744
# COMMIT
# ~~~
def init_pk
# generate_id は unique id を採番する自作メソッド
self.user_id = UserProfile.generate_id unless persisted?
end
end
主キーを変える
上に書いた内容は「移行時に仕方なくやるもの」であって、徐々にレールに乗せたい。ので以下の手順でやっていく。
- サロゲートキー用のカラム (新 PK) を追加する
- まずは nullable なただのカラム追加
- アプリケーション側から新 PK に値を入れるようにする
- ここで旧 PK が AUTO INCREMENT を期待していたとしたら自前で採番して値を入れるようにしておくと楽
- 上記のように
after_initialize
,unless persisted?
で値を入れるのをよくやります
- 上記のように
- 旧 PK と新 PK が同じ値であるようにしておく
- 最悪、新 PK が unique であれば良いんだけど、関連を考えると新旧が同じ値であると楽ができる
- ここで旧 PK が AUTO INCREMENT を期待していたとしたら自前で採番して値を入れるようにしておくと楽
- 全レコードの新 PK を埋める
- アプリケーション側で
primary_key= 新 PK
に変える - DB の PK 変更
- 同時に旧 PK は nullable にする
- たぶん
change_table
では実現できないのでexecute
になるはず- 旧 PK の AUTO INCREMENT を外す
- 旧 PK の PRIMARY KEY を外す
- 旧 PK の NOT NULL を外す
- 新 PK を NOT NULL にする
- 新 PK を PRIMARY KEY にする
- (場合によっては) 新 PK を AUTO INCREMENT にする
- 旧 PK カラムを落とす
-
ignored_columns
に加えておく - DB から旧 PK カラムを落とす
-
ignored_columns
を消す
-
NULL が入っていない UNIQUE なカラムを用意できたらアプリケーション側からはそれを PK として扱えば普通に動くので、あとはテーブル定義をどう実情に合わせるかという戦いになる。
複合主キーは (AUTO INCREMENT や association が無いと思うので) 割と逃げやすくて、シュッと ALTER TABLE foo DROP PRIMARY KEY, ADD PRIMARY KEY (新PK);
でイケるはず。
それ以外だとちょっと苦労するけど、まぁ頑張りましょう。(普通に動くからやらなくてもいいって話はある)
association
ほとんど foreign_key
, class_name
だけでイケると思うので特に書きません。
timestamps
任意のカラム名を timestamps にする
created_at
, updated_at
というカラムがあると自動的に現在時間を入れてくれる仕組みがあり、ActiveRecord::Timestamp という module がこれを司っている。
デフォルトだと created_at
, created_on
というカラム名があると自動で埋めてくれる。これは Rails 1 の頃から変わっていない。
- https://github.com/rails/rails/blob/v1.0.0/activerecord/lib/active_record/timestamp.rb#L22-L29
- https://github.com/rails/rails/blob/v6.0.3.2/activerecord/lib/active_record/timestamp.rb#L82-L88
テーブルごとにこのカラム名を弄りたかったら以下のように上書きする。
class User < ApplicationRecord
private
def self.timestamp_attributes_for_create
super + ["created"]
end
def self.timestamp_attributes_for_update
super + ["updated"]
end
end
レールに乗せる
-
created_at
,updated_at
を nullable で追加する- この地点から勝手に値が入るようになる
- 全レコードの
created_at
,updated_at
を埋める -
created_at
,updated_at
を NOT NULL にする - 旧側に INDEX が貼ってあるなら同じものを新側にも貼る
add_index
- アプリケーション内で旧カラムを使っているところを新カラムに変える
- 旧 INDEX 削除
remove_index
- 旧 timestamps カラムを落とす
-
ignored_columns
に加えておく - DB から旧 PK カラムを落とす
-
ignored_columns
を消す
-
STI
type
カラムがあると怒られるし、アプリケーション内で STI を使いたいことは結構稀なので
class ApplicationRecord < ActiveRecord::Base
# `type` column が使われているので退避しておく
self.inheritance_column = "sti_type"
end
としておくと楽そう。必要ならモデル単位で type
に戻す。