ものぐさツールを作って rails アプリ開発のめんどう事を減らそう

この記事は SmartHR Advent Calendar 2019 6日目の記事です。

開発作業をしているとめんどくさいな〜、忘れそうだな〜、と思うことが時々あります。
たとえば rails アプリ開発の際に migration ファイルを追加したときは migrate 済みの schema.rb をコミットし忘れないようにしないと、といったことです。

そういった、ある時に何かしなければいけない、そしてそれを覚えておく、ということが自分はとても苦手です。帰宅前に牛乳を買ってきて、と会社にいる時におつかいを頼まれることがありますが、大体帰宅時には忘れています。何の対策もせずに買って帰れたためしがありません。それでは困るので、自分はリマインダーをセットするなどの対策を取るようにしています。
対策によって面倒なことから開放されたり、苦手なことが普通のことになったりします。

この記事では日々の開発の中で見つけた面倒事と、その対策として作ったツールの紹介をします。
これらのツールは今まで特に紹介してこなかったので、この機会になぜ作ったのかとツールの概要とを書き残しておこうと思います。

作った理由がどれも面倒だから、大変だから、という動機からばかりなので、これらを ものぐさツール とでも呼んでおきます。

## :gem: db_schema_checker

- [https://github.com/kufu/db_schema_checker](https://github.com/kufu/db_schema_checker)

### これは何

実行することで schema.rb および migration ファイルが正しくコミットされていることを確認できる便利な rake task を追加するものです。


### なぜ作ったか

CI 上でどう DB にテーブルを作っているかにもよりますが、次のことが発生した覚えがある人は多いと思います。

- レビューの時に migration ファイル または schema.rb が更新されてないことを指摘した/された
- 同じテーブルへのカラム追加のマイグレーションを含む Pull Request が並行して作られ、マージされたところ、カラムの順番が schema.rb と稼働中の DB とでズレた
- 最新の master ブランチで DB のマイグレーションを実行したところ schema.rb が変わり、 schema.rb の更新に気づいた
- 反対に、 schema.rb だけコミットされていたために `ActiveRecord::PendingMigrationError` になった

これらはマイグレーションファイルとその生成物である schema.rb とが master ブランチ上で不整合な状態ために発生するものです。その整合性を自分で確認するのが面倒なため、作りました。

### 使い方

```console
$ RAILS_ENV=test bin/rails db:migrate:reset:check
Dropped database 'blog_test'
Created database 'blog_test'
ok

$ rm db/migrate/20180202012345_create_comments.rb

$ RAILS_ENV=test bin/rails db:migrate:reset:check
Dropped database 'blog_test'
Created database 'blog_test'
ERROR: Generated schema is not consistent with db/schema.rb
```

- マイグレーションファイルを削除した結果、マイグレーションファイルから `schema.rb` が生成できるかどうかの検証に失敗し、エラーとなっている

### 仕組み

- `RAILS_ENV=test` で `db:migrate:reset task` を実行し、その前後で `db/schema.rb` とが一致することを確認している
- マイグレーションタスクの実行時は [`SCHEMA` 環境変数にパスをセットしていればそこに `schema.rb` が出力される](https://github.com/rails/rails/blob/675f967a314c3029137d1ed6a306c85d5a4a958f/activerecord/lib/active_record/tasks/database_tasks.rb#L410)ことを利用して、`schema.rb` をテンポラリファイルに出力して、現在の `schema.rb` との差分を検出している
  - これによって git の diff を使わないため、必要なファイルのみ存在するコンテナ上など .git が存在しない状態でも実行可能にしている

## :gem: ignored_removed_columns

 - [https://github.com/meganemura/ignored_removed_columns](https://github.com/meganemura/ignored_removed_columns)

### これは何

これは ignored_columns に不要なカラム名が書かれていないかを確認できるものです。

ignored_columns は、ActiveRecord のモデルで利用できるクラスメソッドで ignored_columns に定義されたカラムはモデル内で無視され、クエリでも参照されなくなります。
無視された状態でアプリが動くことを確認できるため、安全なカラムの削除に便利な機能です。

### なぜ作ったか

ある日 ignored_columns に書いたカラム名がタイポしていたことに気づきました。
ignored_columns は既に存在していないカラム名が指定されていても何も起こりません。
これは rails アプリの稼働中にカラムを消したいという目的からしても正しい挙動です。
ただ、タイポに気づかずに、カラムを ignore できていると勘違いした時は危険です。

ignored_columns は安全のためのものなので、ここでタイポしていると、実はカラムを使っている箇所があった場合に事故になります。カラム名が合っているかどうかを人の目でレビューすることは大変です。実際失敗してタイポがマージされていました。これを原因とする事故を防ぎたいと思い、作りました。

また、 ignored_columns からいつ消すのかは開発者に任せられているので、何もしなければ ignored_columns にどの環境にも存在しないカラム名が残り続けることになります。
残ること自体はそこまで害はないのですが、消したこと・消していないことを忘れないようにするのも面倒です。

### 使い方

```console
$ rake ignored_removed_columns:check
Found removed or typo columns defined in ignored_columns
--------------------------------------------------------
Post:
  - published
  - archivedd
$ echo $?
1
```

- `Post` のテーブルに書いてある `ignored_columns` のうち `published`, `archivedd` が存在していないカラムだというメッセージ
- 実際のカラムと見比べることで `published` は消えたカラムで、`archivedd` は typo しているカラム、だとわかることを期待している
- この結果を元に修正したり削除したりといったアクションにつなげられる

### 仕組み

- `ActiveRecord::Base` を継承するものを取得してすべてのモデルの一覧を作る
- 各モデルに対して ignored_columns と実際の DB 上のカラムとを比較することで既に消えているカラムを検出する
- 存在しないカラムは typo してる、または実際に消えているので何か対応するアクションが取れる


## :gem: ghost_schema

 - [https://github.com/meganemura/ghost_schema](https://github.com/meganemura/ghost_schema)

### これは何か

現在のブランチに存在しないマイグレーションファイルの rollback を実行可能にするものです。

### なぜ作ったか

Pull Request のレビューをするとき、コードレビューと Heroku Review Apps での PR ごとの検証環境での動作確認で完了することもあれば、ローカルのマシンで動作確認することもあります。

ローカルマシンで動作確認をする場合は、 

1. Pull Request のブランチをチェックアウトし、 
2. DB のスキーマに変更があれば手元でマイグレーションを実行して、
3. 動作確認をする

ことになります。

動作確認を終えた後、自分の作業ブランチに戻ってきた後、 `db:migrate` を実行するとレビューしたブランチで追加されたカラムが `schema.rb` に追加されます。
普通この時に `rails db:rollback` してなかったことに気付きます。

しかも作業ブランチにはそのマイグレーションファイルが無いから rollback できません。
もとのブランチを探し、チェックアウトし、 rollback する必要があり、面倒です。

注意深い人なら、レビューが終わったあとでブランチを戻すときに、さっき migration で変更があったから rollback しなきゃ、みたいに思い出せるかもしれません。
しかし忘れっぽい自分にとっては、忘れないようにすることが面倒なので、どうにか rollback したい...と考えて、作りました。

### 使い方

```console
$ bin/rails db:ghost:migrate:down VERSION=20180330010101
== 20180330010101 AddAwesomeColumns: reverting ==============
(snip)
```

### 仕組み

- `db:migrate` task を拡張し、実行ごとに tmp ディレクトリに今回実行した migration ファイルのコピーを保存していく
- `db:ghost:migrate:down` task を実行するとそのコピーの入った tmp ディレクトリのファイルを使ってスキーマを元に戻す
- `db:migrate:down` に対応する `db:ghost:migrate:down` のみを定義している
- 現時点では `db:rollback` 相当のものはない
  - (`db:rollback` に対応しなかった理由は忘れた)

### ちなみに

メンテしてないです。
これを作ってた時の自分のメインプロジェクトでは全部作り直して開発用データ流し込めばいいや、という環境だったので作ったもののほとんど使いませんでした。かわいそうですね。Pull Request お待ちしております。

## :gem: method_publisher

 - [https://github.com/meganemura/method_publisher](https://github.com/meganemura/method_publisher)

### なぜ作ったか

binding.pry で止めたとき、よくこのエラーに当たります。

```
NoMethodError: private method `client' called for SugoiApi:Class
```

そういう時はやむなく次のように書いて一時をしのぎます。

```
api.send(:client)
```

しかしその後もいろんなメソッドを呼びたいわけですが、その度にこれは private だから `send` だ、と書いていくのが面倒になります。

決め手としてこのツイートを見て、この気持ちめっちゃわかるしなんとかしたい、と思い作りました。

<blockquote class="twitter-tweet"><a href="https://twitter.com/pink_bangbi/status/1026642799402217474"/></blockquote>

### 使い方

```ruby
irb(main):001:0> class Card
irb(main):002:1>   private
irb(main):003:1>   def pin
irb(main):004:2>     1234
irb(main):005:2>   end
irb(main):006:1> end
=> :pin
irb(main):007:0> card = Card.new
=> #<Card:0x00007fb4af00d8c0>

# pin は private method なのでエラーが出る
irb(main):008:0> card.pin
Traceback (most recent call last):
(snip)
NoMethodError (private method `pin' called for #<Card:0x00007fb4af00d8c0>)
```

```ruby
# private メソッドを公開する!
irb(main):009:0> card.publish_private_methods; nil

# #pin メソッドを呼び出せる
irb(main):010:0> card.pin
=> 1234
```

- Object にインスタンスメソッドの `#publish_private_methods` を生やしている
- irb/pry 実行中に対象のオブジェクトで呼ぶことで全て public に変更する

### 仕組み

- [https://github.com/meganemura/method_publisher/blob/master/lib/method_publisher/core_ext/object/public.rb](https://github.com/meganemura/method_publisher/blob/master/lib/method_publisher/core_ext/object/public.rb)
- このコードの通りなのですが、そのオブジェクトの特異クラスに対して `private_instance_methods` を順繰りに public に変更しています

## :wrench: めんどうじゃないツールの作り方

以上の4つのライブラリはどれも自分の経験した面倒事を解消しようと思い、知恵を振り絞って作ってみたものです。
何度かこういったツールを作ってみた経験から得た、アイデアの試し方を伝えたいと思います。

### アイデアは気軽に試そう

アプリケーション開発をしている際に面倒だな、前にも同じような作業したな、と思った時に、仕方ないよねとは思わずに、心に留めておくか、メモしておくか、Slack でボヤいておきましょう。
何度か同じ経験をしたり、同じ経験をした人が何人もいるなら、それはものぐさツールを作るときかもしれません。

ふと、こういうアイデアによって解決できるかも? と思ったら、まず `bundle gem xxx` ...とライブラリを作り始めるのではなく、自分の携わっているアプリケーション自体に組み込んでしまう方法が手っ取り早いのでおすすめです。

始めから gem として作ると変更のための面倒事が増えるので、避けた方が良いです。
rails アプリのためのツールなら、生きたコードに直接書いていった方が環境構築の手間が省けますし簡単に実行できます。サンプルアプリを作る必要もありません。

(ローカルの gem を見るように bundler を設定するも可能ですが、これもやはり bundle update する手間が必要になります。)

特に開発初期は何度も不具合や考慮漏れが見つかるので、すぐに試せる環境で初速が落ちないようにするのはとても大切です。

何より自分の困りごとを最小限の手順で解決できます。自分の面倒だと感じるものを自分で解決するのは楽しいものです。

また、開発のためのものぐさツールは `Rails.env` による判定で開発やテストでのみ実行されるように作っておくことで、プロダクションコードに影響を与えずに、気楽に導入することができます。gem にしたあとは development/test group に追加しましょう。

作ってみたツールが複数プロジェクトで使えそうだなと思ったときに初めて gem にして他のプロダクトで使えるようにすると良いです。gem にしておけば Gemfile に1行追加すればいいのでコードをコピーする手間や重複してメンテする必要もなくなります。

## まとめ

- 日常の開発の中で面倒なことがあったとき、仕方ないとは思わずに、その対策となる方法を考えてみると楽になる何かが見つかるかも知れない
- 対策のアイデアを思いついたらまずは自分のプロジェクト内で試すのが確実で楽で、洗練しやすい
- 対策がうまくいって汎用的なものならライブラリにしておくと良い

SmartHR ではどのプロジェクトも rails アプリなので、rails アプリ開発での面倒事を解決できると複数のプロジェクトで楽できます。やりましたね。