レールから外れたデータベースを扱う

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

主キーを変える

上に書いた内容は「移行時に仕方なくやるもの」であって、徐々にレールに乗せたい。ので以下の手順でやっていく。

  1. サロゲートキー用のカラム (新 PK) を追加する
    • まずは nullable なただのカラム追加
  2. アプリケーション側から新 PK に値を入れるようにする
    • ここで旧 PK が AUTO INCREMENT を期待していたとしたら自前で採番して値を入れるようにしておくと楽
      • 上記のように after_initialize, unless persisted? で値を入れるのをよくやります
    • 旧 PK と新 PK が同じ値であるようにしておく
      • 最悪、新 PK が unique であれば良いんだけど、関連を考えると新旧が同じ値であると楽ができる
  3. 全レコードの新 PK を埋める
  4. アプリケーション側で primary_key= 新 PK に変える
  5. 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 にする
  6. 旧 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 の頃から変わっていない。

テーブルごとにこのカラム名を弄りたかったら以下のように上書きする。

class User < ApplicationRecord
  private

    def self.timestamp_attributes_for_create
      super + ["created"]
    end

    def self.timestamp_attributes_for_update
      super + ["updated"]
    end
end

レールに乗せる

  1. created_at, updated_at を nullable で追加する
    • この地点から勝手に値が入るようになる
  2. 全レコードの created_at, updated_at を埋める
  3. created_at, updated_at を NOT NULL にする
  4. 旧側に INDEX が貼ってあるなら同じものを新側にも貼る
    • add_index
  5. アプリケーション内で旧カラムを使っているところを新カラムに変える
  6. 旧 INDEX 削除
    • remove_index
  7. 旧 timestamps カラムを落とす
    • ignored_columns に加えておく
    • DB から旧 PK カラムを落とす
    • ignored_columns を消す

STI

type カラムがあると怒られるし、アプリケーション内で STI を使いたいことは結構稀なので

class ApplicationRecord < ActiveRecord::Base
  # `type` column が使われているので退避しておく
  self.inheritance_column = "sti_type"
end

としておくと楽そう。必要ならモデル単位で type に戻す。