あかね
【Rails】includesはRelationを返しているだけ/AIの嘘と誤解に気づいた話
2026年08月05日
見出しはありません
要約を生成中...
「とりあえずネストしたアソシエーションを巨大includesしてメモリ上で処理する」という秘伝のタレコードのリファクタがスタートしました。
includes が「即時にクエリを発行してメモリに乗せるトリガー」だと思い込んでいたんですが、実はincludesの挙動ではなく .find_by などの評価メソッドが引き金だったことに気づきました。
今日はそんなどハマりエピソードの振り返りです。
includesしてN+1を防ぐ仕組みは、必要な関連データを最初にまとめて取得して、メモリ上で紐付けるから ってclaudeが言ってて、「確かに事前にメモリに乗せればN+1も発生しないメモリの操作になるし、クエリ発行回数も少なくなるな。よしよし」って理解してたんです。
今までその方針でやっててPRもapprpveもされて。確かに言ってることもおかしくないしって感じで。
そしたらなんか「コードが読みにくい、何してんのこのサービスクラス。なんでこんなに読みにくの??」ってめちゃレビューで聞かれるんですよ。
私もわかんねぇんだけど、なんかHash多くね?何が入ってるかわかんないし何が取れるかわかりにくいんだけど!だからRails嫌いなんだよ!!ってなってましたね。
中間変換処理みたいな。これなんでいるんやろ?って見ていくけど確かにいるのか、、ってなることがすごく多くて🥹
原因は全部メモリ操作だったからRailsなのにRubyやってる感じになってたわけですよ。
それに気づいて、「よし、リファクタしよう!!」ってなって今せっせとAR使う方向に修正してます。
脱includesプロジェクト!!!
タスクの真の狙いは、最初から全部メモリに乗せるのをやめ、ActiveRecordを正しく使って、本当に必要なタイミングで最適なクエリを発行させるということです。
遅延処理のフル活用です。
ギリギリまでクエリの組み立てを維持し続けるゲームです。
ほんとにインスタンスが必要なのはどこ??の見極めゲームです。
やっぱ公式が一番だよね!AIは平気で嘘つくしな!!と思って、RailsのARのページを見てたら、、
その中にあったんです、
includesが、、、
なんでだ、、お前のせいでメモリに乗ってたんではないのか、、?
なんでARのクエリビルダーの一覧にお前がいるんだよ、、
おかしいじゃねぇか、、、話が違うんだけど、、
includesは単なる ActiveRecord::Relation(クエリビルダー)を返しているだけでした。
公式のサンプルに
User.includes ( : address , : friends ) .to_a こうあってもう察しました。 .to_a あるやんけ。
書かんやったらRelationてことやん。
前提が崩れて私的には大どんでんがえしでした。
今の何が問題やったん?と思って問題のコード見にいくと
Hoge.includes(:hage, :hhaage).find_by(id: haaage)うわ!find_byしてるやん!メモリに乗せてるのincludesやなくてfind_byやん!!!!
そしてincludesしてるモデル実際は10個以上あったりしてて、一気に載せてるから各所で目的のデータにアクセスしにくく読みにくいにもつながってました。
メモリ使用率も跳ね上がるような実装です。怖い、、
頭がプチパニックになった私は、Slackに思考をぶちまけました。
そこへボスが乗っかってくれ、泥臭い検証が始まりました。
私:「includes ってメモリに乗るとずっと思ってたんですけど、Relation返すんですね…😭
早期にメモリに乗せる処理をしてたのが問題で、まとめてネストで発火させてたのがダメだったんだ…ずっと勘違いしてました😭」
ボス:「むむなるほど、、、!!!自分の理解があやしいところでした💦 共有あざます!!!」
私:「公式に User.includes(:posts).to_a ってあったので、.to_a で初めて実行されるってことですよね…!」
ボス:「そうなりますね、、includes がN+1を防ぐ仕組みは、必要な関連データを最初にまとめて取得して、メモリ上で紐付けるから、、」
ここから議論は「Relationを返すのか、それともメモリ上のオブジェクトを返しているのか」という深みへハマっていきます。。。
ボス:「Relationじゃなくて、CollectionProxy オブジェクトを返していそうですよ🤔」
私:「CollectionProxyって何なんだ…😭 クエリ組み立ててるだけだと思ったのに!」
私(ドキュメントを調べ直して):「ActiveRecord::Associations::CollectionProxy は ActiveRecord::Relation のサブクラスでした!!じゃあRelationやないですか。これだからRailsは、、」
ボス:「🤝 Rails」
公式見ても私調べでもRelationになるのにCollectionProxyって新キャラ出てくるし、もうこれは実際にコンソールで型を確認してみることにしました。
① モデルクラスから直接 includes を呼んだ場合
[1] pry(main)> Article.all.includes(:comments).class
=> Article::ActiveRecord_Relationほらほら!ActiveRecord_Relationやん!!!
思った通りの挙動!!
② インスタンスのアソシエーション経由でアクセスした場合
[2] pry(main)> article = Article.first
[3] pry(main)> article.comments.class
=> ArticleComment::ActiveRecord_Associations_CollectionProxyモデル直(Article.includes(...))だと ActiveRecord::Relation が返り、インスタンスのアソシエーション経由(article.comments)だとCollectionProxyが返ることが判明!!!
includeは関係なかった!
has_manyが生やしてるインスタンスメソッドにアクセスすると返ってくるのがCollectionProxyでした!!
ここからはincludes関係ないんですが、さらに議論を進める中で、もう一つの大きな開眼がありました。
ボス:「あーちなみに、has_many :hoge ってDBのカラムとは全く無関係ですよ👍 モデルにメソッドが生えるってことです!
has_many :sorted_model_b, -> {
ModelB
.where(:hogehoge)
.order(id: :desc)
}こんな風に書けるよ!」
私:「こう見ると、ただのメソッドの定義じゃないですか😭つまり、
has_many :sorted_model_b, -> {
ModelB
.where(:hogehoge)
.order(id: :desc)
}これ書いてるモデルHageだとしたら
Hage.first.sorted_model_bってするとOrderProduct::ActiveRecord_Associations_CollectionProxyが返ってくるってことですね!!!」
ボス:「SOUDAYO」
has_many :recent_comments は、実質的にこういうメソッドを生やしているのと同じ!
class Article < ApplicationRecord
has_many :recent_comments, -> { order(created_at: :desc) }
def recent_comments
Comment.where(article_id: self.id).order(created_at: :desc)
end
endhas_many という記述を1対多の表現としてぼんやり捉えていましたが、「単にモデルクラスにインスタンスメソッドを生やして、呼び出されたらRelation/CollectionProxyを返しているだけ」 という構造がクリアに理解できました。
メタプログラミング
includes 単体はクエリを評価せず、ActiveRecord::Relation を構築しているだけでした。
実際にクエリが飛ぶのは .to_a, .each, .find_by などの評価トリガーメソッドが呼ばれた瞬間です。
遅延処理については理解してたけどincludesも仲間だったとは。公式見なくなるとこういう誤解が発生しますね。反省。
has_many はDBカラムではなく、モデルに便利メソッドを自動定義する宣言に過ぎない。
「includes撲滅」という一見怪しいリファクタリングタスクから始まりましたが、裏側をコンソールで叩いて解剖したことで、アソシエーションの仕組みまで解像度が格段に上がりました。
フレームワークの魔法に頼って雰囲気でコードを書くのをやめ、「どこでクエリが発行され、どこでメモリに乗るのか」 を意識することの大切さを痛感した出来事でした。
どこでクエリ走るかって理解しないとパフォチューも無理なので頑張るぞ!!
リファクタは順調に進んでますが、「後の処理でインスタンスがいるからここはRelationじゃダメだよ!」みたいにclaude言ってくるんです。
でも実際は、後の処理もまだRelationでいけるから後の処理を変える必要があるみたいなことも多くて、AI嘘つきすぎてやっぱこっちの理解大事すぎるって思いました。
前提がそもそも間違ってる!みたいな。
とにかく頑張るぞ!!!
AIにトレーニングしてもらってて、クイズ出して!って感じで。ちょっと紹介です。

→ 2回!!!group = OrderShippingGroup.firstとp filtered.to_a

→ 1はメモリのカラムにアクセスしてるだけ。2も同じ。3は別クエリっぽいから3だけと見た

→ Aだね、1ならDだけど
みたいなことやってます。
この3問はスラッと全問正解できました!嬉しい!
復習に使うの楽しいですよ〜おすすめです!!
要約
コメント
まだコメントはありません。