Ridgepole本番運用における懸念点と解決方法の検討


[Ridgepole](https://github.com/winebarrel/ridgepole)は、rails標準のmigrateに代わるDBスキーマ管理ツールです。

[Ridgepoleを使ってみる](http://qiita.com/akishin/items/4612ae2a6f7a5173ff7f)
[クックパッドにおける最近のActiveRecord運用事情](http://techlife.cookpad.com/entry/2014/08/28/194147)

現在弊社で随時開発中のRails製新規サービスにおいて、DBのスキーマ管理ツールとして、Ridgepoleを導入することが決定しました。そこでRidgepoleを導入するにあたって、悩んだ点やハマった点などをまとめてみました。

## 環境

- Ruby 2.3.1
- Rails 5
- MySQL 5.7

# Point 1. インストール

~~普通にインストールしようとしたら、Rails5環境には入りませんでした。そこで、正式リリース前の最新ブランチでインストールすると、うまく入りました。~~

**2017/3/18追記**
最新のバージョンがリリースされたことで、Rails5でも普通に入るようになりました。

```rb
# Gemfileに記述してbundle install
gem 'ridgepole'
```

# Point 2. 意図しないDROP TABLEのリスク回避

## migrateでは起きない問題

Rails標準のmigrateでテーブルの削除を行うには、テーブルの削除を行うためのmigrationスクリプトを作成し、migrateする必要があります。具体的には以下のようなファイルを作成します。

```rb
class DropEvent < ActiveRecord::Migration
  def change
    drop_table :event
  end
end
```

**テーブルを削除するという作業**を明示的に行うため、意図しない形でテーブルが削除されてしまうリスクは少ないです。

## Ridgepoleでは意図しないDROP TABLEのリスクが・・・

しかし、Ridgepleでは違います。テーブル定義を行っている箇所を削除した状態で、スキーマをDBに反映させてしまうと、該当のテーブルが消えてしまいます。つまり、スキーマファイル上からcreate_table文を間違えて消してしまうと、意図しない形でテーブルが消えて大変なことになってしまいます(ほぼあり得ないとは思いますが・・・)。

```rb
# これを消してしまうと、DROP TABLEが実行される
create_table :events do |t|
  ~
  略
  ~
  t.timestamps
end
```

本番運用中に、消えちゃいけないテーブルがなくなってしまうと大変なので、万が一に備えて対策を考えます。

## Rakeタスク内でリスクを担保する

スキーマをDBに反映させるrakeタスクを自作し、その中で、DROP TABLEのリスクを担保します。

```rb
namespace :db do
  desc 'apply Schemafile and update schema.rb'
  task apply: :environment do
    ENV['ALLOW_DROP_TABLE'] ||= '0'
    ENV['ALLOW_REMOVE_COLUMN'] ||= '0'
    ENV['RAILS_ENV'] ||= 'development'
    task_return = `ridgepole -E #{ENV['RAILS_ENV']} --diff config/database.yml db/schemas/Schemafile`
    column_condition = task_return.include?('remove_column') && ENV['ALLOW_REMOVE_COLUMN'] == '0'
    table_condition = task_return.include?('drop_table') && ENV['ALLOW_DROP_TABLE'] == '0'
    if column_condition || table_condition
      puts '[Warning]this task contains some risks: "remove_column" or "drop_table"'
    else
      sh "ridgepole -E #{ENV['RAILS_ENV']} -c config/database.yml --apply -f db/schemas/Schemafile"
      sh 'rake db:schema:dump'
    end
  end
end
```

このrakeタスクではまず、DBの状態とスキーマファイルを比較します。そして、DROP TABLEもしくはREMOVE COLUMNが発行される場合は、処理を中断して警告を表示します。

```
> rails db:apply
[Warning]this task contains some lisks: "remove_column" or "drop_table"
```

テーブル削除あるいはカラム削除の差分反映を通すには、"ALLOW_DROP_TABLE=1" あるいは、"ALLOW_REMOVE_COLUMN=1" をオプションとして指定します。

```
> rails db:apply ALLOW_REMOVE_COLUMN=1 # カラムを削除・リネームする場合
```

```
> rails db:apply ALLOW_DROP_TABLE=1 # テーブルを削除する場合
```

不十分かもしれませんが、意図しない形でテーブルやカラムが削除されるリスクをある程度担保できると思います。

# Point 3. 外部キーの定義

referencesが使えません。明示的に宣言します。

### migrate

```rb
create_table :events do |t|
  t.references :tag, index: true # ここが無くなって
  ~
  略
  ~
  t.timestamps
end
```

#### ridgepole

```rb
create_table :events do |t|
  t.integer :tag_id # ここと
  t.index [:tag_id], name: 'index_events_on_tag_id' # ここが追加
  ~
  略
  ~
  t.timestamps
end
```

# Point 4. 外部キー制約の削除

## 作成

外部キー制約の作成時は、name属性を指定する必要があります。

```rb
add_foreign_key :events, :tags, name: 'events_on_tag_id'
```

問題は削除です

## 削除する順番が重要

例えば、下記のようなテーブル定義があります。

```rb
create_table :events do |t|
  t.integer :tag_id
  t.index [:tag_id], name: 'index_events_on_tag_id'
  ~
  略
  ~
  t.timestamps
end

add_foreign_key :events, :tags, name: 'events_on_tag_id'　# 外部キー制約
```

eventテーブルとtagテーブルの関連が必要なくなったので、外部キーと外部キー制約を削除したいとします。

ここで、不要になった箇所を取り除いて下記のように修正します。

```rb
create_table :events do |t|
  ~
  略
  ~
  t.timestamps
end
```

しかしこれだと、

remove_column

↓

remove_index

↓

remove_foreign_key

の順に実行されてしまいます。外部キー制約が存在する状態のカラムを削除することはできないので、最初のremove_columnの時点でエラーとなってしまいます。

解決方法としては、主に2通り存在します。

## 1. 二回に分けてスキーマをDBに反映させる

まず、add_foreign_keyの行だけを削除したスキーマファイルをDBに反映させ、改めて、カラムとインデックスを宣言している行を削除し、DBに反映させます。

ちょっと面倒ですね。

### 2. MySQL上でALTER TABLEを実行する

まず、MySQL上で外部キー制約を解除するためにALTER TABLEを実行します。具体的なSQL文は、ridgepoleコマンドでdiffオプションを使えば確認することができます。そして、スキーマファイルから不要な行を削除した上で、改めてDBに反映させます。

# 最後に

ridgepoleを導入するにおいて、注意が必要な点をまとめてみました。もちろん今回上げたもの以外にも、細かい注意点や、ちょっとした制限などはいくつかあります。しかし、それを補ってもなお利点が勝ると感じたため、今回導入に踏み切りました。スタンダードな手法を捨てることはリスクも伴いますが、得られるものも多い気と思うので、今後もチャレンジしていきたいです。