Skip to content

Modelとは

RailsにおけるModelは、Webアプリケーションの「データ」と「頭脳」を司る重要な存在です。データベースのテーブルと直接対話するだけでなく、アプリケーションのビジネスロジックを管理する役割を担っています。

⚠️ Modelは「シェフ」以上の存在である

Section titled “⚠️ Modelは「シェフ」以上の存在である”

Modelを単なる「データベース担当」と捉えると、ビジネスロジックの置き場に困ります。

重要な理解: RailsのModelは、単なるデータの箱ではなく、**「アプリケーションの全てのルール」**を知っている場所です。

: 「18歳未満は購入不可」というルールは、ウェイター(Controller)ではなく、シェフ(Model)がレシピの一部として持っているべき知識です。

# ❌ 悪い例: ビジネスロジックがControllerにある
class OrdersController < ApplicationController
def create
if current_user.age < 18
flash[:error] = "18歳未満は購入できません"
redirect_to products_path
return
end
# 注文処理...
end
end
# ✅ 良い例: ビジネスロジックがModelにある
class Order < ApplicationRecord
belongs_to :user
validate :user_must_be_adult
private
def user_must_be_adult
if user.age < 18
errors.add(:base, "18歳未満は購入できません")
end
end
end
# Controllerはシンプルに
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
if @order.save
redirect_to @order
else
render :new
end
end
end

改善案: **「Model = データベース + ビジネスルール」**と定義し直すと、より堅牢な設計になります。これにより、アプリケーションのルールが一箇所に集約され、変更時の影響範囲が明確になります。

RailsのModelの心臓部にあるのが、Active Recordという強力なORM(オブジェクト関係マッピング)ライブラリです。この「魔法のツール」を使うことで、私たちはSQL文を一切書くことなく、Rubyのオブジェクトとしてデータベースを操作できます。

例えば、usersというデータベーステーブルがあれば、Railsは自動的にUserというModelクラスを紐づけてくれます。

# Userクラスのインスタンスは
# データベースのusersテーブルの1つの行に対応します
user = User.new(name: "Alice")
user.save # 魔法!データベースに新しい行が追加される

Modelは単なるデータ置き場ではありません。データを扱う上で欠かせない3つの強力な機能を持っています。

1. バリデーション(Validation)✅

Section titled “1. バリデーション(Validation)✅”

Modelは、データをデータベースに保存する前に、それが「有効なデータか?」を厳しくチェックします。これにより、誤ったデータが入り込むのを防ぎ、データの品質を保ちます。

class User < ApplicationRecord
# 名前が空でないか、メールアドレスが一意であるかチェック!
validates :name, presence: true
validates :email, uniqueness: true
end

⚠️ バリデーションは「フロントエンド」との二重管理

Section titled “⚠️ バリデーションは「フロントエンド」との二重管理”

React側でもZodなどでバリデーションをしている場合、Modelのバリデーションとどう向き合うべきか。

重要な理解: Modelのバリデーションは「最後の砦」ですが、DBレベルの制約(Not Null制約など)の代わりにはなりません。

問題点: validates :name, presence: trueと書いても、DB側にnull: falseがなければ、Railsを通さない操作(直接SQLを実行する、別のアプリケーションからアクセスするなど)で不正データが入り込みます。

# ❌ 不十分: Modelのバリデーションだけ
class User < ApplicationRecord
validates :name, presence: true
end
# マイグレーションファイル
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name # null: falseがない!
t.timestamps
end
end
end
# 問題: Railsを通さずに直接SQLを実行すると、nameがnullのデータが入る
# INSERT INTO users (name) VALUES (NULL); # これが実行できてしまう

改善案: 「Modelでのバリデーション」と「DBレベルの制約」は常にセットで行うべきである、という境界防御の視点を強調しましょう。

# ✅ 良い例: ModelとDBの両方で制約
class User < ApplicationRecord
validates :name, presence: true # アプリケーションレベルの防御
end
# マイグレーションファイル
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false # DBレベルの防御
t.string :email, null: false, unique: true # 一意制約もDBレベルで
t.timestamps
end
end
end

フロントエンドとの関係:

  • フロントエンド(React/Zod): UX向上のため、ユーザー入力時に即座にエラーを表示
  • Modelのバリデーション: アプリケーションレベルの防御(Railsを通す場合)
  • DBレベルの制約: 最後の砦(Railsを通さない場合も含む)

この3層の防御により、データの整合性を確実に保てます。

複数のModelが互いにどのように関係しているかを定義します。これにより、複雑なデータ間のつながりを簡単に扱えるようになります。

関連付け意味合い具体例
has_many1対多ユーザーは多くの投稿を持つ
belongs_to多対1投稿は1人のユーザーに属する
has_one1対1ユーザーは1つのプロフィールを持つ
has_and_belongs_to_many多対多投稿は多くのタグを持ち、タグは多くの投稿を持つ

関連付けを定義しておけば、user.postsのように直感的なコードで関連データを取得できます。

⚠️ ORMの魔法が生む「N+1問題」

Section titled “⚠️ ORMの魔法が生む「N+1問題」”

「user.postsのように直感的に取得できる」という魔法には、恐ろしい代償があります。

問題点: 関連付け(Association)を何も考えずにループ内で使うと、発行されるSQLの数が爆発し、サーバーが死にます。

具体例: 100人のユーザーの投稿を表示する際、1回で済むはずのSQLが101回(1 + 100)発行されるのがN+1問題です。

# ❌ 悪い例: N+1問題が発生
users = User.all # SQL: SELECT * FROM users (1回)
users.each do |user|
puts user.posts.count # SQL: SELECT * FROM posts WHERE user_id = ? (100回)
end
# 合計: 101回のSQLが発行される
# ✅ 良い例: Eager Loadingを使用
users = User.includes(:posts).all # SQL: 2回のみ
# 1. SELECT * FROM users
# 2. SELECT * FROM posts WHERE user_id IN (1, 2, 3, ..., 100)
users.each do |user|
puts user.posts.count # 追加のSQLは発行されない
end
# 合計: 2回のSQLのみ

改善案: 「魔法」には必ずincludespreloadという**「一括取得(Eager Loading)」**の杖をセットで使うべきです。

# includes: 関連データを事前に読み込む
User.includes(:posts, :profile).all
# preload: includesと似ているが、JOINを使わない
User.preload(:posts).all
# eager_load: JOINを使って一度に取得
User.eager_load(:posts).all

⚠️ includesの「過剰読み込み」に注意

Section titled “⚠️ includesの「過剰読み込み」に注意”

includesはN+1問題を解決しますが、必要なデータだけを読み込むことを意識しないと、メモリ使用量が不必要に増大します。

重要な理解: ループ内での.last.title取得にincludesを使用していますが、効率が悪いです。includes(:posts)はそのユーザーの全投稿をメモリに読み込みますが、必要なのは「最新の1件」だけです。

問題点: 投稿数が多いユーザーがいる場合、メモリ使用量が不必要に増大します。

# ❌ 非効率: 全投稿を読み込んでから最新の1件を取得
users = User.includes(:posts).all
users.each do |user|
puts user.posts.last.title # 問題: 全投稿をメモリに読み込んでいる
end
# 問題: 100件の投稿を持つユーザーがいる場合、100件すべてをメモリに読み込む
# 問題: 必要なのは最新の1件だけなのに、全投稿を読み込んでいる

改善案: has_one :latest_postという関連をモデルに定義し、includes(:latest_post)とすることで、読み込むデータ量を最小限に抑えてください。

# ✅ 効率的: 最新の1件だけを読み込む関連を定義
class User < ApplicationRecord
has_many :posts
has_one :latest_post, -> { order(created_at: :desc) }, class_name: 'Post'
end
# 使用例
users = User.includes(:latest_post).all
users.each do |user|
puts user.latest_post.title # 最新の1件だけが読み込まれている
end
# 利点: 各ユーザーに対して最新の1件だけが読み込まれる
# 利点: メモリ使用量が大幅に削減される

SQLの違い:

# ❌ 非効率な方法
User.includes(:posts).all
# SQL: SELECT * FROM users
# SQL: SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)
# → 全投稿が読み込まれる
# ✅ 効率的な方法
User.includes(:latest_post).all
# SQL: SELECT * FROM users
# SQL: SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...) ORDER BY created_at DESC LIMIT 1
# → 各ユーザーに対して最新の1件だけが読み込まれる

推奨されるアプローチ:

  1. 必要なデータを特定: ループ内で実際に使用されるデータを確認する
  2. 関連を最適化: 必要なデータだけを取得する関連を定義する
  3. メモリ使用量を意識: includesを使う際は、読み込むデータ量を最小限に抑える

この最適化により、メモリ使用量を削減し、パフォーマンスを向上させられます。

開発時の対策:

  • Bullet Gem: 開発中にN+1問題を自動検知してくれるGemを使用する
  • ログの確認: log/development.logで発行されるSQLを常にチェックする習慣をつける

⚠️ selectメソッドの「見落としがちな罠」

Section titled “⚠️ selectメソッドの「見落としがちな罠」”

パフォーマンス改善のためにselectを使って必要なカラムだけを取得しようとすることがありますが、これには注意が必要です。

重要な理解: User.select(:id, :name)だけでは不十分なケースがあります。後続のロジックやシリアライザーでuser.emailを参照していた場合、selectから漏れていると追加のSQLが発行される(N+1)か、エラーになります。

問題点: パフォーマンス改善のつもりが、バグや逆にクエリ増を招きます。

# ❌ 危険: selectから必要なカラムが漏れている
users = User.select(:id, :name).all
users.each do |user|
# 問題: シリアライザーでuser.emailを参照している
UserSerializer.new(user).as_json # user.emailが呼ばれる
# → 追加のSQLが発行される: SELECT email FROM users WHERE id = ?
end
# 結果: パフォーマンス改善のつもりが、逆にクエリが増える
# ❌ 危険: selectから必要なカラムが漏れている(エラーになるケース)
users = User.select(:id, :name).includes(:posts).all
users.each do |user|
user.posts.each do |post|
# 問題: post.user.emailを参照しているが、userにemailが含まれていない
puts post.user.email # ActiveModel::MissingAttributeError
end
end

改善案: シリアライザーが必要とする全属性を把握した上でselectを使うか、まずはN+1解消を優先してください。

# ✅ 安全: 必要なカラムをすべて含める
users = User.select(:id, :name, :email).all
users.each do |user|
UserSerializer.new(user).as_json # 追加のSQLは発行されない
end
# ✅ より安全: まずはN+1解消を優先
users = User.includes(:posts).all # selectを使わず、まずはN+1を解消
users.each do |user|
UserSerializer.new(user).as_json
end
# ✅ 最適: シリアライザーが必要とする属性を把握してからselect
# シリアライザーで使用される属性: id, name, email, created_at
users = User.select(:id, :name, :email, :created_at).includes(:posts).all
users.each do |user|
UserSerializer.new(user).as_json # すべての属性が揃っている
end

推奨されるアプローチ:

  1. まずはN+1解消: selectを使う前に、includesなどでN+1問題を解消する
  2. シリアライザーの確認: シリアライザーや後続のロジックで使用される属性をすべて把握する
  3. 段階的な最適化: パフォーマンスが本当に問題になったら、その時点でselectを検討する

この順序により、パフォーマンス改善のつもりがバグを生むことを防げます。

データのライフサイクル(作成、更新、削除など)の特定のタイミングで、自動的に実行される処理を定義します。

class Post < ApplicationRecord
# 投稿が保存される前に実行される
before_save :set_default_title
private
def set_default_title
# タイトルが空なら「Untitled」を設定
self.title = "Untitled" if self.title.blank?
end
end

⚠️ コールバックは「副作用の温床」

Section titled “⚠️ コールバックは「副作用の温床」”

「自動的に実行される」コールバックは、一見便利ですが、アプリが大きくなると最大のバグ発生源になります。

問題点: before_saveなどに複雑なロジックを書くと、「ただ保存したいだけなのに、予期せぬメールが飛ぶ」「テストが異常に遅くなる」という事態を招きます。

Reactで「副作用の局所化」を学んだ通り、モデルを保存するだけで世界中のデータが変わってしまう設計は「安全」ではありません。

# ❌ 悪い例: コールバックに副作用を書く
class User < ApplicationRecord
after_create :send_welcome_email
after_save :update_analytics
after_update :notify_admin
private
def send_welcome_email
UserMailer.welcome_email(self).deliver_now # メール送信
end
def update_analytics
AnalyticsService.track_user_creation(self) # 外部サービスへの通知
end
def notify_admin
AdminNotificationService.notify(self) # 管理者への通知
end
end
# 問題: テストでUser.createするだけで、メールが送信され、外部サービスに通知される
# 問題: バッチ処理で大量のUserを作成すると、メールが大量に送信される

改善案: コールバックは「そのモデル自身のデータの整形(タイトルをUntitledにするなど)」だけに留め、外部への通知などはServiceクラスなどに追い出すのがプロの設計です。

# ✅ 良い例: コールバックは自己完結型の処理のみ
class Post < ApplicationRecord
before_save :set_default_title # 自分自身のデータを整形するだけ
private
def set_default_title
self.title = "Untitled" if self.title.blank?
end
end
# 外部への通知はServiceクラスで
class PostCreationService
def self.call(post_params)
post = Post.new(post_params)
if post.save
PostNotificationService.notify(post) # 通知は別のServiceで
true
else
false
end
end
end

これにより、副作用が局所化され、テストが容易になり、予期せぬ動作を防げます。

Modelのファイルは、app/models ディレクトリに配置されます。慣習として、ファイル名はクラス名を単数形(例:Userクラスならuser.rb)にします。

⚠️ 隠れたメモリ消費:大量データのロード

Section titled “⚠️ 隠れたメモリ消費:大量データのロード”

Reactの物理制約で「1万件のDOMは重い」と学びましたが、Railsでも同じです。

問題点: User.all.eachと書くと、1万人のユーザーデータをすべて一気にRubyのメモリに載せようとします。これでサーバーのメモリが枯渇し、プロセスがクラッシュ(OOM Killer)します。

# ❌ 危険: 全データを一度にメモリに読み込む
User.all.each do |user|
puts user.name
end
# 1万人のユーザーがすべてメモリに載る → メモリ不足でクラッシュ
# ✅ 安全: バッチ処理で小分けに読み込む
User.find_each do |user| # デフォルトで1000件ずつ処理
puts user.name
end
# 1000件ずつ処理されるため、メモリ使用量が一定に保たれる
# バッチサイズを指定することも可能
User.find_each(batch_size: 500) do |user|
puts user.name
end

改善案: 大量データを扱うときはfind_each(バッチ処理)を使い、1,000件ずつ小分けにしてメモリを節約する手法を覚えましょう。

# find_eachの動作
# 1. SELECT * FROM users LIMIT 1000 OFFSET 0
# 2. 1000件を処理
# 3. SELECT * FROM users LIMIT 1000 OFFSET 1000
# 4. 次の1000件を処理
# ... これを繰り返す
# 大量データの更新も安全に
User.find_each do |user|
user.update(last_login_at: Time.current)
end

これにより、データ量が増えてもメモリ使用量が一定に保たれ、サーバーがクラッシュすることを防げます。

「Concern(コンサーン)」は、Railsエンジニアの間でも**「便利だけど、使い方を間違えると地獄への扉が開く」**と言われる、非常に議論の絶えない機能です。

結論から言うと: 「共通ロジックの切り出し」には非常に便利ですが、安易に使うとコードの依存関係がカオスになります。

Reactエンジニアのあなたには、**「Concern = コンポーネントの共通ロジックを抽出した『カスタムフック』」**のようなものだと考えると理解が早いです。ただし、フックと違って「副作用が隠蔽されやすい」のが厄介な点です。

主に以下の2つのケースで使われます。

① 複数のモデルで「全く同じ振る舞い」をさせたい時

例えば、UserモデルとProductモデルの両方に「画像をアップロードしてリサイズする」機能や「論理削除(deleted_atの管理)」機能を持たせたい場合です。

app/models/concerns/discardable.rb
module Discardable
extend ActiveSupport::Concern
included do
scope :kept, -> { where(discarded_at: nil) }
scope :discarded, -> { where.not(discarded_at: nil) }
end
def discard
update(discarded_at: Time.current)
end
end
# app/models/user.rb
class User < ApplicationRecord
include Discardable
end
# app/models/product.rb
class Product < ApplicationRecord
include Discardable
end

② モデルが巨大化(Fat Model)した時の「整理整頓」

1つのモデルファイルが1,000行を超えそうな時、タグ関連のロジックをTaggable、検索関連をSearchableという風に別ファイルに切り出します。

🛠️ Concernの実装例と「まさかり」ポイント

Section titled “🛠️ Concernの実装例と「まさかり」ポイント”
app/models/concerns/common_actions.rb
# ❌ 悪い例:なんでもかんでもConcern
module CommonActions
extend ActiveSupport::Concern
included do
# まさかり:全てのモデルにこのバリデーションが必要か?
validates :name, presence: true
# まさかり:隠れたコールバックはデバッグの敵
before_save :log_save_action
end
def log_save_action
puts "Saving record..."
end
end
app/models/concerns/discardable.rb
# ✅ 良い例:明確な「役割」を切り出す
module Discardable
extend ActiveSupport::Concern
included do
scope :kept, -> { where(discarded_at: nil) }
scope :discarded, -> { where.not(discarded_at: nil) }
end
def discard
update(discarded_at: Time.current)
end
end
# 使用例
class User < ApplicationRecord
include Discardable
end
# User.kept # 削除されていないユーザー
# user.discard # 論理削除

🪓 Concernへの「全力まさかり」

Section titled “🪓 Concernへの「全力まさかり」”
① 「多重継承」のような複雑さを生む
Section titled “① 「多重継承」のような複雑さを生む”

Concernは内部的にinclude(ミックスイン)を使います。複数のConcernを1つのモデルに含めると、**「このメソッドはどのConcernに定義されているんだ?」**と、ファイル間を飛び回る羽目になります。

指摘: Reactでいう「HOC(Higher-Order Components)地獄」に近い状態になります。

# ❌ 問題:複数のConcernが混在
class User < ApplicationRecord
include Discardable
include Searchable
include Taggable
include Loggable
include Cacheable
# 問題:メソッドがどのConcernに定義されているか分からない
# 問題:Concern間の依存関係が複雑になる
end

改善案: Concernは必要最小限に留め、明確な役割を持つものだけを使用しましょう。

② コールバックの隠蔽(最大の罠)
Section titled “② コールバックの隠蔽(最大の罠)”

includedブロックの中にbefore_saveなどのコールバックを書くと、モデルファイルを見ただけでは「なぜデータが書き換わったのか」がわからなくなります。

まさかり: 「Concernにコールバックを書くのは、カレーの中に隠し味でハバネロを入れるようなものだ」(後で誰かが泣く)。

# ❌ 危険:コールバックが隠蔽される
module Searchable
extend ActiveSupport::Concern
included do
before_save :update_search_index # モデルファイルを見ても分からない
end
private
def update_search_index
# 検索インデックスを更新
end
end
# モデルファイル
class Post < ApplicationRecord
include Searchable
# 問題:保存時に何が起きているか分からない
end

改善案: コールバックは極力Concernに書かず、モデルに明示的に書くか、Serviceクラスに移動しましょう。

③ 「継承」ではなく「コンポジション(合成)」を検討せよ
Section titled “③ 「継承」ではなく「コンポジション(合成)」を検討せよ”

Concernでロジックを共有する代わりに、単なるRubyのクラス(Service ObjectやValue Object)を作って、モデルから呼び出す方が、テストもしやすく疎結合になります。

# ❌ Concernを使う場合
module Searchable
extend ActiveSupport::Concern
def search(query)
# 検索ロジック
end
end
class Post < ApplicationRecord
include Searchable
end
# ✅ コンポジションを使う場合(推奨)
class PostSearchService
def self.call(query)
Post.where("title LIKE ?", "%#{query}%")
end
end
class Post < ApplicationRecord
# Concernをincludeしない
end
# 使用例
PostSearchService.call("Rails")

推奨されるアプローチ:

  • Concern: 複数のモデルで全く同じ振る舞いが必要な場合のみ
  • Service Object: ビジネスロジックを処理する場合
  • Value Object: 値の概念を表現する場合

🛠️ database.ymlへの「まさかり」追加

Section titled “🛠️ database.ymlへの「まさかり」追加”

Concernを使って「検索ロジック(Searchable)」などを切り出す際、DBの全文検索機能などを利用することがあります。

重要な理解: Concern内で複雑なSQL(joinseager_load)を多用すると、モデルの動作が重くなります。

database.ymlへの影響: 複雑なクエリが増えると、DBのstatement_timeout(クエリ実行時間の制限)に引っかかるようになります。デフォルトでは無制限のことが多いですが、本番環境では「重すぎる検索クエリがDBを専有して全停止」するのを防ぐため、variables: statement_timeout: 5000(5秒で切断)などの設定をdatabase.ymlに加えるのがプロの防御策です。

config/database.yml
production:
<<: *default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i + 2 %>
timeout: 5000
checkout_timeout: 5
reaping_frequency: 10
# まさかり:重いクエリがDBを専有するのを防ぐ
variables:
statement_timeout: 5000 # 5秒でクエリを切断

🚀 まとめ:Concernを使う時の「自分ルール」

Section titled “🚀 まとめ:Concernを使う時の「自分ルール」”

ReactエンジニアがRailsを書くなら、以下の基準でConcernを使いましょう。

  1. 3箇所以上で同じロジックが出てきたら検討する(2箇所ならコピペの方がマシ)
  2. コールバック(before_save等)は極力書かない
  3. 「何ができるか(Ability)」を名前にする(例:Searchable OK、UserModule NG)
# ✅ 良い命名
module Searchable # 検索できる
module Discardable # 論理削除できる
module Taggable # タグ付けできる
# ❌ 悪い命名
module UserModule # 何ができるか分からない
module CommonActions # 抽象的すぎる

このルールにより、Concernを適切に使い、コードの可読性と保守性を保てます。

RailsのModelは、単にデータベースとやり取りするだけでなく、データの検証、関連性の管理、そして自動実行されるロジックを担う、まさにアプリケーションの「心臓部」です。Active Recordという強力なツールのおかげで、私たちは複雑なデータベース操作から解放され、より本質的なアプリケーションの機能開発に集中できるのです。

ただし、ORMの「魔法」には代償があります。N+1問題、コールバックの副作用、バリデーションとDB制約の二重管理、大量データのメモリ消費など、実務で数千万人規模のデータを扱う際には必ずぶつかる「Active Recordの罠」を理解し、適切に対処することが重要です。

🚀 設計レビューでの「まさかり」文例

Section titled “🚀 設計レビューでの「まさかり」文例”

実務でのコードレビューで使用できる、具体的な指摘文例を以下に示します。

【指摘】N+1問題が発生しています。
【問題】users.map { |u| u.profile } の箇所で、ユーザーの数だけSQLが発行されています。
【影響】ユーザー数が増えた際にレスポンスが極端に遅くなり、DB負荷が激増します。
【推奨】User.includes(:profile).all のようにEager Loadingを使用してください。
【指摘】コールバックに副作用(外部への通知)が含まれています。
【問題】after_create :send_email により、モデルを保存するだけでメールが送信されます。
【影響】テストが複雑になり、バッチ処理で予期せぬメールが大量送信される可能性があります。
【推奨】コールバックは自己完結型の処理(データ整形など)のみに留め、外部への通知はServiceクラスに移動してください。

バリデーションとDB制約の不整合の指摘

Section titled “バリデーションとDB制約の不整合の指摘”
【指摘】ModelのバリデーションとDBレベルの制約が一致していません。
【問題】validates :name, presence: true がありますが、マイグレーションに null: false がありません。
【影響】Railsを通さない操作で不正データが入り込む可能性があります。
【推奨】マイグレーションファイルに null: false を追加し、DBレベルでも制約を設けてください。
【指摘】大量データを一度にメモリに読み込んでいます。
【問題】User.all.each により、全ユーザーデータが一度にメモリに載ります。
【影響】ユーザー数が増えた際にメモリ不足でプロセスがクラッシュする可能性があります。
【推奨】User.find_each を使用し、バッチ処理で小分けに処理してください。

selectメソッドの不適切な使用の指摘

Section titled “selectメソッドの不適切な使用の指摘”
【指摘】User.select(:id, :name) だけでは不十分なケースがあります。
【問題】後続のロジックやシリアライザーで user.email を参照していた場合、select から漏れていると追加の SQL が発行される(N+1)か、エラーになります。
【影響】パフォーマンス改善のつもりが、バグや逆にクエリ増を招きます。
【推奨】シリアライザーが必要とする全属性を把握した上で select を使うか、まずは N+1 解消を優先してください。
【指摘】ループ内での .last.title 取得に includes を使用していますが、効率が悪いです。
【問題】includes(:posts) はそのユーザーの全投稿をメモリに読み込みますが、必要なのは「最新の1件」だけです。
【影響】投稿数が多いユーザーがいる場合、メモリ使用量が不必要に増大します。
【推奨】has_one :latest_post という関連をモデルに定義し、includes(:latest_post) とすることで、読み込むデータ量を最小限に抑えてください。

これらの指摘文例を参考に、コードレビューで適切なフィードバックを行い、堅牢なアプリケーションを構築しましょう。

RailsのModelは、単にデータベースとやり取りするだけでなく、データの検証、関連性の管理、そして自動実行されるロジックを担う、まさにアプリケーションの「心臓部」です。Active Recordという強力なツールのおかげで、私たちは複雑なデータベース操作から解放され、より本質的なアプリケーションの機能開発に集中できるのです。