[Rails]CSV生成処理をモデルで書いてるけどそれって正しいの?


概要

CSV生成処理をモデル側で書いてる案件をよく見ます。
※実際にはエスケープ処理なども必要ですが、今回は簡略化してます。

app/models/user.rb
class User < ApplicationRecord
  def self.build_csv
    csv = ["氏名,ニックネーム,電話番号"]
    all.map do |user|
      row = "#{user.full_name},#{user.nickname},#{user.phone_number}"
      csv << row
    end

    csv.join('\n')
  end
end

今回は↑の設計に疑問を投げかける投稿です。

これの何処が問題なの?

自分なりに、上記の様なCSV生成処理をモデル側で書く設計の違和感について解説していきます。

MVC の観点から見た違和感

Rails は MVC モデルのフレームワークです。
MVC において、クライアントへの最終的な出力はViewの役割であり、今回のケースで言うと、CSVの中身がクライアントへの最終的な出力( == Viewの責務)であると考えられます。
このことから、MVC の観点から言うとCSV生成処理はView層でやるのが妥当ではないかと考えられます。

この設計で実際のところどんな問題が起きるの?

設計の話はどうしても抽象的でイメージがしづらいので、「こういうときに困るだろうな」という例を上げてみます。

例えば、以下のような要望が来たとしましょう
・管理画面でユーザー一覧のCSVをダウンロードしたい
・出力内容は 氏名,ニックネーム,電話番号

そして、モデル側にCSV生成処理を書きます。(記事最上部のコードと全く一緒です。)

app/models/user.rb
class User < ApplicationRecord
  def self.build_csv
    csv = ["氏名,ニックネーム,電話番号"]
    all.map do |user|
      row = "#{user.full_name},#{user.nickname},#{user.phone_number}"
      csv << row
    end

    csv.join('\n')
  end
end

そして、後から以下のような要望が来たとします。
・一般ユーザーもユーザー一覧CSVをダウンロードできるようにしたい
・CSVダウンロードボタンは、一般ユーザー向けのユーザー一覧画面に設置したい
・個人情報は漏らしてはいけないので氏名は「○○」、電話番号は最初の2桁だけ表示してのりは伏せ字にしたい。
・ニックネームはそのまま表示したい

↑こういった要望が追加されると、修正方法としては
1. self.build_csv メソッドに for_admin の様なフラグ変数を引数として渡して self.build_csv メソッド内で分岐する
2. self.build_csv_for_general のようなメソッドを追加して、一般向けCSVの生成はこちらのメソッドに任せる

のどちらか2択になるかと思います。

1は、フラグ変数で表示を分岐してるので、内部の処理が複雑化したり、一般向けCSVに個人情報が漏れ出す不具合が起きそうで不安です。

また、2は User モデルのCSV種類が増えるたびにメソッドが増えていき、fat model になりそうです。

モデルで書くのが良くないなら、何処で書けばよいの?

自分はCSV生成処理はView側で書くようにしてます。

勝手に こちらの記事 を参考にさせていただきました。ありがとうございます。

まずは、上記要望の管理者向け CSV を実装していきます。
コントローラ側では、CSV出力用のViewを呼び出して、 send_data でファイルを返す処理を書きます。

app/controllers/admins/users_controller.rb
class Admins::UsersController < ApplicationController
  def download
    @users = User.all
    send_data render_to_string, filename: "users.csv", type: :csv
  end
end

そして、CSVの内容はView側で行います。

app/views/admins/users/download.csv.ruby

csv = ["氏名,ニックネーム,電話番号"]
@users.map do |user|
  row = "#{user.full_name},#{user.nickname},#{user.phone_number}"
  csv << row
end

csv.join('\n')

次に、上記一般ユーザー向け CSV を実装していきます。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def download
    @users = User.all
    send_data render_to_string, filename: "users.csv", type: :csv
  end
end

そして、CSVの内容はView側で行います。
一般向けなので本名、電話番号は伏せて出力します。

app/views/users/download.csv.ruby

csv = ["氏名,ニックネーム,電話番号"]
@users.map do |user|
  row = "○○,#{user.nickname},#{user.phone_number[0..2]}X-XXX-XXXX"
  csv << row
end

csv.join('\n')

これで一般向け、管理者向けCSV生成処理をView側で実装できました。

最後に

今回は設計寄りの話をしました。
設計には正解がないので、自分の実装案が必ずしも正解とは限りませんし、案件によってはモデルに実装したほうが良いケースももしかしたらあるかもしれません。

大事なのは 一度立ち止まって「この処理はこの箇所で実装するべきか」をしっかり考えること だと思います。
考えた上で「CSV生成処理をモデルで書こう」という結論が出たなら、それはそれで一つの回答ではないかと思います。