Railsで開発を行うときに考えていること

# はじめに

この記事は [VR法人HIKKY Advent Calendar 2023](https://qiita.com/advent-calendar/2023/hikky) の14日目の記事です。
前日は [VRイベントサイト制作の振り返り2023 Nuxt3対応とWebアニメーションに寄せて](https://qiita.com/rohitotsubakura/items/938ec3d6703b4656b500) でした。


# 書くこと
普段 Rails 開発で意識していることやコーディングの仕方について基本的なことから宗教的なことまでだらだらと書き出してみる。
普段意識していないことは書かないかもしれない。
フレームワークによらず考えていることも含む。

# controller / routing
## routes.rb
member, collection 等、見通しよく整理できるメソッドが意外とある。

## データの保存失敗処理
```ruby
  def update
      ...
      save!
      ...
  rescue ActiveRecord::RecordInvalid => e
    render json: { errors: e.record.errors }, status: :unprocessable_entity
  end
```

```ruby
...
  rescue_from ActionController::BadRequest, with: :_render_400
...
```

rescue を利用することで、見通しよくエラー処理を書くことが出来る。

## 悪意あるリクエストを受け付けない
コードを書き終えたあとに、このコードやデータを悪意を持って壊すにはどのようなリクエストを送ればよいか？ということを一度考え、悪意あるユーザーからのリクエストの対策を行う。
他人のデータのIDを指定したり、脆弱性をつくなど。

## レスポンスステータスは適切に返す
外部からはレスポンスステータスの情報は重要な情報であるので、適切なステータスコードはどれであるかを都度検討する。とりあえず全部500みたいな返し方は行わない。

## よくある書き方が存在する
Rails の根底の考え方に `設定より規約` という考え方があり、それによって開発効率の向上を生み出している。このため、 `rails g` を行ったときに生成される初期のコードがあり、それを踏襲することでより開発効率の向上が見込めることや、キャッチアップコストの低下を見込めることが出来る。

例) `#show, #update, #destroy` の対象レコードの取得は、 `before_action :set_xxx` で行うなど
```ruby
  before_action :set_user
...
  def show
    render json: { user: @user }
  end
...
  private

  def set_user
    @user ||= User.find(params[:id])
  end
```

## fat controller と向き合う
業務ロジックが複雑になればなるほど、複数のモデルをまたいだデータの更新処理が行われる。
複数のデータの更新処理は一つのモデルに書くのはやや不適切な場合があるため、 controller に書かれがちになる。
以下でよく見る対策について考える。

#### バリデーション処理を model に切り出す
`validate` を有効活用することで、controller にバリデーションの処理を書かなくて済む。
`if` も併用し、より見通しよくコードを書くことが出来る。

#### private メソッドへの切り出し
private メソッドへの処理の切り出しはあくまで、処理のまとまりに名前をつけただけにとどまるため、private メソッドが乱立するようであれば、service 層などの rails 標準ではない処理の集約層の導入の検討を行う。
ただし、 rails 標準ではない層の導入を行う場合、rails way から外れるため開発効率の低下につながる。
複雑になりすぎる場合はより適切な言語やフレームワークの検討を行いたい。

## 外部システムとの連携
外部システムとの連携を行う際は、どのようなデータをやり取りしたか、失敗時は出力可能な範囲でログを残すようにする。外部システムとの連携は不整合が発生しやすく、問題が起きた際の調査や復旧が困難になりやすいため、ログ出力を可能な限り行う。

## as_json
使いこなすと便利。

## 出力するデータ
active_record のインスタンスをそのまま `render json` することでいい感じにデータの出力が可能となる。
データのサイズと相談し、生産性やメンテナンス性とのバランスの取れた出力を目指していく。


# Model (Active Record)
## as_json
使いこなすとDRYに出来る。
便利すぎるがゆえに不要なデータの出力が行われないか注意する。

## enum
勝手に生える便利メソッドを有効活用する。
`_prefix: true` などを活用することで、複数の enum もわかりやすく管理できる。

## relation
primary キー以外でも join する書き方が出来るので、特殊な join の SQL を自ら書くことを減らせる。


# DB (mysql)
## N+1 を出さない
N+1 のチェックを忘れない。
bullet 等を使って賢く検知する。

## index ちゃんと貼る
検索条件にあった index をちゃんと貼る。
複雑な検索条件になってきた際に、カーディナリティの意識と ElasticSearch 等の検索に特化したミドルウェアを利用する検討を行う。
漢(オトコ)のコンピュータ道をちゃんと理解するまで読む。

## 複数のテーブルの更新
transaction をしっかり貼る。

## transaction のロールバック
DB の変更を伴う処理に関連して不可逆な操作が含まれる場合、必ず DB の処理を先に行い、 transaction の最後で不可逆な処理を行う。
不可逆な処理中にエラーが発生した際に、raise エラーにより自動的に transaction がロールバックされ、安全にDBも含めてロールバックされる。
不可逆な操作を複数回行うような transaction は、不可逆な操作を非同期にすることでリスクヘッジしたり、失敗時のオペレーションの検討を行う。

例) 外部システムのデータの登録など
```ruby
# NG
ActiveRecord::Base.transaction do
  ExternalSystem.update_user(@user)
  @user.update!(update_params)
end

# OK
ActiveRecord::Base.transaction do
  @user.update!(update_params)
  ExternalSystem.update_user(@user)
end
```

## 誰がどのデータを更新するのか意識する
他人のデータを更新できてはいけないということを意識する。
複数人が同時に一つのデータを更新できる可能性があるとき、排他制御の必要性やデッドロックの可能性について考慮する。

## パーティション
データ量が多く見込めるテーブルはちゃんとパーティションを切る。
primary キーの設計を意識する。


## reader, writer
負荷分散のためにDBのアクセスを切り替えている際に、データ更新の際に今どちらにアクセスしているかということを意識しながらコードを書く。
reader 側のデータは transaction 内の変更を反映してくれない。

# DB (NoSQL)
ほんとに NoSQL が必要か、導入前に一回立ち止まって考える。
RDBMS みたいに簡単にテーブル構造の変更ができない。

# Cache
## TTL
キャッシュのTTLによってはデータの不整合があるため、どのタイミングで誰が更新するのか？ということを意識する。

## キャッシングレイヤー
どのレイヤーでのキャッシュが必要であるか考える。
バックエンドアプリケーションは単体では起動せず、必ずインフラが存在するため、インフラを意識して効率的なキャッシングレイヤーの検討を行う。

# テスト
## RSpec を書く
rspec (テスト)は必ず書く。
rspec が無いコードは修正するときの影響範囲が分かりづらく、正しい挙動の確証を得るのが難しい。

## matcher を使う
結構便利な matcher がいっぱいあるのでこういうことしたいと思ったときに調べてみると、大体ある。
配列や hash の特定の情報のみチェックしたり、完全一致のチェックしたりなど、かゆいところに手が届く。

## factory
### Faker を使う
Faker によるランダムネスの提供をフル活用する。

### 別名 factory を作りすぎない
特定の model に対して、一部のデータのみを変えた factory を作りすぎない。
特定のフィールドによって性質が変わり、その他の複数のフィールドが暗黙的に連動しているようなデータは factory を作っても良いかもしれない。
その場合はそもそもテーブル構造に負債を抱えていそうであるので、リファクタリングを検討したい。

## 外部システムとの連携
外部システムとの連携はテストしきれないところもある。
少なくとも、期待する引数で外部システムとの連携をコールするのを確認するためのテストを、モック機能などを用いてテストしておく。

## テストデータ
Request Spec での一覧APIのテストを行う際に、複数のデータを一気に作らない。もしくは、複数のデータを作ることを明示した context 内で、複数のデータに関する振る舞いのチェックのために作成する。
1件でのテストの振る舞いを確認し、各種条件によって挙動が変わることを確認し、その後に複数件数での振る舞いを確認する。
複数の挙動を一気に確認するのではなく、一つずつ順番に条件を積み上げていく。

## Time
created_at 等の時間に関するデータの確認はCIで秒単位のズレが発生したりするため、 freeze_time をしっかり行う。

## openapi
[こういうgem](https://github.com/interagent/committee) を使って、openapi でスキーマチェックするとフィールドの漏れなどが確認できて楽になる。

# gem
## 選定
メンテナンスされているか？issuesやPRが放置されていないかを確認する。
脆弱性のリスクや、バージョンアップデートの際の障害につながる。

## carrierwave
### remote_xxx_url, identifier
便利メソッド/パラメータがある。
README にサラッと書いているので README をじっくり読むとよい。

### CarrierWave::Downloader::Base
minio の利用時など、 `skip_ssrf_protection?` というメソッドを利用して、ローカルでのテストに対応することが出来る。

# インフラ
## rack
インフラ層にあった rack の設定のチューニングを行う。

## ssl
復号化を行っているのは誰なのかを確認する。インフラの設定次第では rails では `force_ssl` の設定の調整を行う必要がある。

## body サイズ
インフラの制約やコスト増を意識する。

## ログ出力
インフラにあったログ出力の設定をする。json のフォーマットで出したり、標準出力で出力したりなど。


# おわりに
根底の考え方の言語化は上手く出来ないけど、きれいなコードの書き方やベストプラクティスを漁った結果培った考え方です。
ここはどうなの？や表現の方法など編集リクエスト大歓迎です。
読んでくれてありがとうございました。


明日のアドベントカレンダーは
@iori_yuki さんの Nuxt3のメタバースサービスにPWAを導入した話 です。