ActiveRecordのモデルが1つだとつらい

## Railsあるある

何気ないモデルの変更がアプリケーション全体を傷つけた

## TL;DR

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">最近の趣味アプリではコントローラーごとにモデル生やしてトップレベルのモデル使わない設計で書いていて、コールバックやデフォルトスコープ書き散らかしても影響範囲がコントローラー内だけで済むので便利だしFormオブジェクト書いてグルーしまくる必要もなく快適</p>&mdash; Miyagi (@hanachin_) <a href="https://twitter.com/hanachin_/status/958285661601087488?ref_src=twsrc%5Etfw">2018年1月30日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">アプリ全体で1モデルだとグローバル変数と一緒、モデル全体に影響でる機能がアプリ全体に影響でてつらい。機能ごとにスキップしたり使い分けはできるけどモデルごと全部分けたほうが楽、コントローラーごとに分けると責務が明確になりやすい。みたいな感じです! 詳しい記事はやる気でたら書く...</p>&mdash; Miyagi (@hanachin_) <a href="https://twitter.com/hanachin_/status/961784296347332608?ref_src=twsrc%5Etfw">2018年2月9日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">自分自身ネームスペースどう切るかとかは興味なくてdefault_scopeがつらいのではなくいろんな責務があるのにActiveRecordのモデルが1つなのがつらみの根本的な原因、Formにしてもそれは残るしいっそActiveRecordごと分ける方が便利では既にmigration/micro serviceでやってるしみたいな感じなのでした</p>&mdash; Miyagi (@hanachin_) <a href="https://twitter.com/hanachin_/status/963215822251376640?ref_src=twsrc%5Etfw">2018年2月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

コントローラーごとに分けろといいたいわけではなく、アプリケーション全体で1テーブル1 ActiveRecordモデルをやめて

## 経緯

子どものお世話を記録するウェッブアプリケーションを正月休みあたりから書いています。[^bblog]
https://github.com/hanachin/bblog

[^bblog]: docker-composeで環境を整えたり、SQLでi18nした文字列をjsonとしてrenderしてARインスタンス経由せずに返したり、今回記事にしたARのモデルを分ける設計など普段仕事でやらない実験的なことを趣味でやっています

その中でこういう気持ちが生まれました(Refinements過激派)。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">特定のcontrollerでしかつかわないメソッド、modelに生やさずcontrollerで無名モジュール作ってrefineでよいのでは (過激派</p>&mdash; Miyagi (@hanachin_) <a href="https://twitter.com/hanachin_/status/944015787500609537?ref_src=twsrc%5Etfw">2017年12月22日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

リプライで以下のようなツイートを受け取りました。

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">内部クラスとして HogeController::User を作るのはどうか、って話をこのあいだしてました。</p>&mdash; Takafumi ONAKA (@onk) <a href="https://twitter.com/onk/status/944079047713157120?ref_src=twsrc%5Etfw">2017年12月22日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">モデル消えるのヤバイｗ / HogeController 内では ::User と User とで自然に使い分けられるのが良さそうな感じで、内部クラスの方は find_by_xxx とかのクラスメソッドを置くと良いのかなと。(インスタンス側に何かが欲しいなら decorator という仕組みが既にある</p>&mdash; Takafumi ONAKA (@onk) <a href="https://twitter.com/onk/status/944081650849165312?ref_src=twsrc%5Etfw">2017年12月22日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">良さそうなんですが関連テーブルみたりスコープ生やしたりクラスメソッドはやすとなるとコントローラ下にコントローラ用のモデルあると便利そうですね！(パスがapp/models/hoges_controller/user.rbみたいになりそうなのに若干抵抗あり)</p>&mdash; Miyagi (@hanachin_) <a href="https://twitter.com/hanachin_/status/944084933286412289?ref_src=twsrc%5Etfw">2017年12月22日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

1月のOkinawa.rbで最近試してる話をしたとツイートしたら反響をいただき[^kimihito_][^kazuma_lab]記事にした次第です。

[^kazuma_lab]: https://twitter.com/kazumalab/status/961824048052281345
[^kimihito_]: https://twitter.com/kimihito_/status/960332669954359297


## 何気ないモデルの変更がアプリケーション全体を傷つける原因

アプリケーションの規模がどんなに大きく成長しても1つのテーブルに対応するActiveRecordのモデルは常に1つ。アプリケーション全体が1つのモデルに依存!
なのでモデルの振る舞いに影響が出る機能を使うとアプリケーション全体に影響がでる。

## 具体的にどういう機能で問題が出やすいか

Railsあるある[^anti_pattern]を参考にいくつか挙げてみます。

- default_scope
- validation
- callbacks
- as_json

どれもモデルの振る舞いに影響が出るものばかりですね。

特にdefault_scopeに関しては地雷メソッド[^mine]とか撒いてはいけない種[^pain_in_the_neck]とかevil[^default_scope_is_evil]など過激な呼ばれ方をされています。

[^anti_pattern]: https://www.slideshare.net/tricknotes/rails-possiblestory
[^mine]: https://qiita.com/sinsoku/items/9cbdc5304aa3ede4a178
[^pain_in_the_neck]: https://qiita.com/juntetsu_tei/items/a1b641f7f3b10d3ae6e1
[^default_scope_is_evil]: https://rails-bestpractices.com/posts/2013/06/15/default_scope-is-evil/

## 逃れ方

一応それぞれ色々な方法で外したりスキップ出来ます。

### default_scope

- unscopedでスコープを外す
- reorder, rewhereなどで条件を変更する

### validation

- validationがかからないAPIや`save(validate: false)`を使う[^skipping_validations]
- contextを設定して特定の場合だけ実行されるようにする`save(context: :account_setup)`[^ar_validation_on]
- 特定の条件に合致する場合しかバリデーションしない[^conditional_validation]

[^skipping_validations]: http://guides.rubyonrails.org/active_record_validations.html#skipping-validations
[^ar_validation_on]: http://guides.rubyonrails.org/active_record_validations.html#on
[^conditional_validation]: http://guides.rubyonrails.org/active_record_validations.html#conditional-validation

### callbacks

- callbackが実行されないAPIを使う[^skipping_callbacks]
- 特定の条件に合致する場合しかバリデーションしない[^conditional_callbacks]

[^skipping_callbacks]: http://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
[^conditional_callbacks]: http://guides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks

### as_json

ActiveRecordの`:only`, `:except`, `:methods`, `:include`のオプションを指定するとある程度カスタマイズできる[^as_json]

[^as_json]: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json

## 逃れるの無理説

アプリケーション全体でモデル1つだとモデルが大きくなるにつれ複雑に組み合わさる。モデルを使う場所全部でこれらの機能を意識しながら書くの無理では...。
ということで最初から使わないほうがよいのでは、みたいな結論になりがちです。

単純にConcernに切り出してファイルを分けてもモデルが1つのままではモデルの変更がアプリケーション全体に影響します。

## モデル全体に影響する機能を使わない場合、代わりに何を使うの?

レールの伸ばし方[^extend_the_rail]ではモデルの責務をPORO[^poro], FormObject, ServiceObjectに分ける方法が紹介されています。

[^extend_the_rail]: https://speakerdeck.com/willnet/rerufalseshen-basifang
[^poro]: Plain Old Ruby Object、継承元なしのObjectを継承してるただのRubyのオブジェクト、`class PORO; end`こういうやつ。

### ActiveRecord以外の層つくると意外と面倒

ActiveRecordを使うとparamsで受け取った文字列を渡すだけでいい感じに型変換してくれてべんりです。
Form Objectなどを分けた場合、このあたりの型変換のコードでかなり記述量が増えたりします。[^active_record_attributes_is_awesome]
なのでForm Objectを作りやすくするためのまた別のgemを導入することが多いです。[^ganbari]

[^active_record_attributes_is_awesome]: `ActiveModel::Attributes`が使えるようになればPOROでも同じように型変換できるので問題なくなるかも https://qiita.com/alpaca_taichou/items/bebace92f06af3f32898
[^ganbari]: 学習コスト💪

## 例えばモデルのクラスを分ける

モデルの振る舞いの影響範囲がアプリケーション全体に及んでしまうのがつらみの原因なら、責務ごとにモデルごと別々に分けると疎になって便利では?
以下で普段の開発の中でActiveRecordのクラスを分ける例を挙げます。

## 例: マイグレーション実行時に使うモデル

マイグレーション作成時のチェックポイント[^migration_checkpoint]から引用します。

> app/models 下のモデルクラスなど、マイグレーションファイルの外部に定義している、将来実装を変更する可能性のあるクラスを直接利用することは禁じ手と考えた方がいいでしょう。
> なぜかというと、マイグレーションファイルというのは、未来にわたって末永く、書いたときの意図どおりに動く 必要があるからです。言い換えれば、マイグレーションファイルのコードは、マイグレーションファイル内で閉じていて、凍結されていることが望ましい のです。

[^migration_checkpoint]: https://qiita.com/nay3/items/ef773006cd7f815a07cd

### 問題: 1つのクラスに2つの責務

```rb
# app/models/user.rb
class User < ApplicationRecord
  UNKNOWN_BIRTHDAY = Date.new(9999, 1, 1)
end
```

```rb
require 'date'

class AddBirthdayDateToUsers < ActiveRecord::Migration[5.1]
  def up
    add_column :users, :birthday_date, :date
    User.reset_column_information
    User.find_each do |u|
      birthday_date = Date.new(u.year, u.month, u.day) rescue nil
      u.update(birthday_date: birthday_date || User::UNKNOWN_BIRTHDAY)
    end
    change_column_null :users, :birthday_date, false
  end

end
```

上記のようにマイグレーション実行時にアプリケーションで定義したActiveRecordのクラスを参照すると、アプリケーションコードにマイグレーションのコードが依存し、1つのクラスに2つの責務が生まれます💪

- アプリケーションを実行するための責務
- マイグレーションを実行するための責務

この場合、アプリケーションを実行するための修正がマイグレーション実行に影響を及ぼす可能性があります。
具体的な例をRails で信頼性の高い Migration を書くには[^reliable_migration]から引用すると以下のような感じです。

> 特に Model を使ってデータの移行を行う場合は注意が必要です。create, update, where など一部のメソッドしか使わないつもりでついついそのまま使ってしまいがちですが、hook や default_scope、validation などの変化によって知らぬうちに挙動が変わってしまいます。Migration 毎に専用の Model を作りましょう。

[^reliable_migration]: https://qiita.com/shuhei/items/c0a6c3e29c87de6dff63#migration-%E6%AF%8E%E3%81%AB%E5%B0%82%E7%94%A8%E3%81%AE-model-%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B


### 解決策: マイグレーション用のモデルをつくる

マイグレーションファイル中でマイグレーションの実行に必要な責務だけを持ったActiveRecordのクラスを宣言します。アプリケーションコードの変更がマイグレーションに影響することはありません。[^why_reset_column_info]


```rb
require 'date'

class AddBirthdayDateToUsers < ActiveRecord::Migration[5.1]
  class User < ActiveRecord::Base
    UNKNOWN_BIRTHDAY = Date.new(9999, 1, 1)
  end

  def up
    add_column :users, :birthday_date, :date
    User.find_each do |u|
      birthday_date = Date.new(u.year, u.month, u.day) rescue nil
      u.update(birthday_date: birthday_date || User::UNKNOWN_BIRTHDAY)
    end
    change_column_null :users, :birthday_date, false
    User.reset_column_information
  end

  def down
    remove_column :users, :birthday_date
  end
end
```

[^why_reset_column_info]: ActiveRecordではテーブルごとにスキーマの情報をキャッシュしておりクラスが分かれていても影響が出る場合があります。`reset_column_information`を呼んでいるのはキャッシュをクリアするためです。詳しくはonkさんの記事を読みましょう。 https://blog.onk.ninja/2017/10/18/use_reset_column_information

## 責務に応じてモデルを分けるとよいのでは

上記の例ではActiveRecordのモデルをわけた例を紹介しました。
モデルを分けた結果、モデルが単一責任になり、モデルへの変更が別のモデルやアプリケーションコードに影響しなくなりました。
ふつうのアプリケーションのコードも無理して1つのモデルに全部詰め込まず、マイグレーションのようにActiveRecordのモデルを分けるとよいのでは?

### 影響範囲が狭くなる

目的ごとにモデルを定義すると影響範囲がアプリケーション全体から狭まります。
`default_scope`や`validation`や`callbacks`や`as_json`を書き散らかしても、他のモデルに影響しないので便利そうです。

### Railsの機能がそのまま使えて便利

ふつうのActiveRecordのクラスなので型変換や慣れ親しんだAPIをそのまま使えます。
他のgemの使い方を覚える必要はありません。

## やりかた

### 例: 登録が完了したときメールを送りたい

```app/models/signup_user.rb
class SignupUser < User
  after_save :send_signup_email

  private

  def send_signup_email
    UserMailer.signup(self).deliver_later
  end
end
```

```app/controllers/signup_controller.rb
class SignupController < ApplicationController
  def create
    user = SignupUser.new(params)
    if user.save
      redirect_to root_path
    else
      render 'new'
    end
  end
end
```

### 例: 公開されている記事だけを表示したい

```app/models/article.rb
class Article < ApplicationRecord
end
```

```app/models/published_article.rb
class PublishedArticle < ApplicationRecord
  self.table_name = "articles"
  default_scope -> { where(published: true) }
end
```

```app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    # 一覧用
    @articles = PublishedArticle.order(published_at: :desc)

    # 新規作成用
    @new_article = Article.new
  end
end
```

### 例: 作るときだけ関連レコードのpresenceを確認したい

```app/models/article.rb
class Article < ApplicationRecord
  belongs_to :author, optional: true
end
```

```app/models/new_article.rb
class NewArticle < Article
  self.table_name = "articles"
  validates :author, presence: true
end
```

```app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def create
    @new_article = NewArticle.new(params)

    if @new_article.save
      redirect_to @new_article
    else
      render 'new'
    end
  end
end
```

## まとめ

アプリケーション全体で1つのテーブルに対応するActiveRecordのモデルが1つだとモデル全体に影響でる機能がアプリケーション全体に影響でてつらい。
`default_scope`や`validation`や`callbacks`や`as_json`などモデル全体に影響が出るメソッドでつらみが生まれるのは、それらの機能自体が悪いわけではなく、アプリケーション全体で1モデルを共有しているのが原因ではないか?
アプリケーションの様々な場面での責務を1つのActiveRecordのモデルに詰め込むとつらいので、責務に応じて同じテーブルを参照するActiveRecordのモデルを分けるとよいのでは、影響範囲が狭まるしActiveRecordの機能がそのまま使えて便利!というご提案でした。

1つのテーブルに対応するActiveRecordのモデルを分けるのはマイグレーションやマイクロサービスなどで既にやっている人も多いと思いますが、アプリケーションコードでも分けてこ💪

## 懸念

最近の趣味のアプリケーションでちょっと試した感じよさそうでしたが大きいアプリケーションになるとまた別のつらみが発生しそう。[^tsurami]

[^tsurami]: 複雑な実業務でやると影響範囲がアプリケーション全体から特定のモデルを使う機能に変わるだけで結局モデルクラスが増えた分メンテコスト増えたり、同じテーブルに対する操作が複数のモデルに散らばってしまいそう(concernでまとめてあげれば再利用できそうですが)、これはFormオブジェクトに分けても同じかな。