🛋️

ActiveRecordの属性を文字列としてフォーマットして羅列するスクリプトで生産性がそこそこ上がった話

Leaner Technologies 開発チームの RKTM です。

Ruby on Rails を使って開発をしていく上で、ActiveRecord で定義したモデルの属性を羅列することってありますよね?

例えば下記のようなテーブルとそれに対応するモデルがあるとしましょう。

create_table "books" do |t|
    t.string "title" null: false
    t.string "author"
    t.string "category_name"
    ...
end

その場合、以下のようなコードを書くことはよくあります。

# JSON 化する際に、as_json で必要な属性のみを指定する
book.as_json(only: %i[id title author category_name ...])
# RSpec で属性の値を検証する
expect(book).to have_attributes(
  title: 'Leaner Book',
  author: '李衣名 太郎',
  category_name: '産業'
  ...
)

また、開発しているプロダクトのフロント側では、JSON のキーは lowerCamelCase で受け取るため(過去記事)、JSON に変換された結果を確認する際には以下のように記述します。

# RSpecでJSONに変換された値を検証する
expect(book_as_json).to have_attributes(
  title: 'Leaner Book',
  author: '李衣名 太郎',
  categoryName: '産業' # lowerCamelCase
  ...
)

問題の定義:毎回手書きで属性を羅列するのはしんどい!

多くの機能開発においてモデルの属性を(全てではないにしても)羅列する作業が発生します。

そしてその作業は楽しいものでなく、かつ、手で入力すると typo もあり、心を削られます。
特に属性が 5 個以上に増えてくるときついですね。漏れも発生します。

解決案: 面倒なことはスクリプトにやらせよう!

ということで、モデルの属性をいい感じにフォーマットして吐き出すスクリプトを作成しました。
ポイントは、「8 割ぐらいいい感じに吐き出せれば OK!残り 2 割は手で修正」という割り切りです。

まずは属性のコレクションを取得する処理。

# スクリプトの第一引数に `Book` のようなモデル名をもらう想定
klass = ARGV[0].constantize

# ActiveRecordのcolumnsメソッド経由で、定義されたカラム情報を取得
# created_atなどの使わないカラムはここで除去
target_columns = klass.columns.reject { |c| c.name == 'created_at' || c.name == 'updated_at' }

これで該当モデルが持つカラムの情報にアクセスできます。

columns の定義はこちら。

https://github.com/rails/rails/blob/decafbd9cf22c8e17e6b3545b852225dfb064992/activerecord/lib/active_record/model_schema.rb#L424-L427

columns の戻り値は ConnectionAdapters::Column(を継承したクラス)の配列です。

https://github.com/rails/rails/blob/e8175f4e235b9b6a0b6fd553d29790e3353dd3f7/activerecord/lib/active_record/connection_adapters/column.rb

カラムのコメントも取得できます。

コードをgenerateする

あとは欲しい情報に応じて色々と吐き出してみましょう。

属性の羅列

target_columns.map(&:name).join(",\n")

# output

id,
title,
author,
category_name

as_json のoption生成

<<~"OPTION_TEMPLATE"
    JSON_OPTIONS = {
      only: %i[#{target_columns.map(&:name).join(' ')}]
    }.freeze
  OPTION_TEMPLATE

# output

JSON_OPTIONS = {
  only: %i[id title author category_name]
}.freeze

これを使って、前述の通り、book.as_json(JSON_OPTIONS) と書いたりします。

lowerCamelCaseのキーにして羅列

RSpec で JSON の比較用。
*snake_case のままなら、model.create model.update や RSpec の to have_attributes にも使えます。

target_columns.map do |c|
  "#{c.name.camelize(:lower)}: xxx.#{c.name}"
end.join(",\n")

# output

id: xxx.id,
title: xxx.title,
author: xxx.author,
categoryName: xxx.category_name,

末尾のカンマは手で削ってください。あと xxx も手で置換してください。

フロントエンドのtest用

target_columns.map do |c|
  "expect(screen.getByLabelText('#{c.comment || 'TODO'}') as HTMLInputElement).toBe(xxx.#{c.name.camelize(:lower)})"
end.join("\n")

# output

expect(screen.getByLabelText('TODO') as HTMLInputElement).toBe(xxx.id)
expect(screen.getByLabelText('タイトル') as HTMLInputElement).toBe(xxx.title)
expect(screen.getByLabelText('著者') as HTMLInputElement).toBe(xxx.author)
expect(screen.getByLabelText('カテゴリー') as HTMLInputElement).toBe(xxx.categoryName)

こちらも xxx は手で置換してください。

フロントのテストは React Testing Library を想定しています。
https://testing-library.com/docs/react-testing-library/intro/

ざっくりしたコードジェネレーターでも十分生産性の向上に寄与

上記のスクリプトを開発チームに共有したところ、以下のようなフィードバックももらえました。ありがたいです。

  • 手を動かす量が減った!typo も減った!
  • 属性羅列の漏れが減った!

手作業をなるべく減らし(とはいえ実装を頑張りすぎないのも大事)、生産性・品質・気分を上げていきたいですね。

宣伝

Leaner では開発生産性を小さなところから改善したいエンジニアを募集しています!

https://careers.leaner.co.jp/engineering

リーナーテックブログ

Discussion