Asp.Net Core ApiのIncludeの使い方で盛大な勘違いをしていた件

みなさんこんにちは!ひろぽんです!!

今回はAsp.netのModel参照でデータを取得する場合に避けて通れないInclude関数で盛大な勘違いをしていたことについて書いていきたいと思います。

目次

発端はModel参照がNullになっていたこと

💡 EF Core で N+1 問題に詰まる場合は別記事 ASP.NET MVC ORM 3択 (EF6/Dapper/ADO.NET) で性能観点もまとめてます。


namespace IncludeStudy.Models
{
    public class Company
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual ICollection<Employee> Employees { get; set; }

    public Company(int id, string name)
    {
        Id = id;
        Name = name;
    }

    public Company()
    {
        
    }
}

}

こんな感じのモデルがあって、下記のようなControllerがあります。

(Controllerは基本スキャフォールドしてるものなので、抜粋しています。)

        // GET: api/Companies/5
        [HttpGet("{id}/emp")]
        public async Task<ActionResult<ICollection<Employee>>> GetCompanyEmp(int id)
        {
            var company = await _context.Company.FindAsync(id);

        if (company == null)
        {
            return NotFound();
        }

        return company.Employees.ToList();
    }</code></pre></div>

で、このURLをたたいて中身を見ると下記のようになってます。

せっかくModel参照しているのにデータがNullになっています。

なので公式リファレンスを見たところIncludeを使え!と書いていました。

https://docs.microsoft.com/ja-jp/ef/core/querying/related-data/eager

Include関数は戻り値があると思っていた。

でここからが盛大な勘違い。

            ICollection<Employee> emps = _context.Company.Include(com => com.Employees).ToList();

Include関数はLinqみたいにラムダ式を使うので、Linqのように値が返ってくると思っていました。

でもこれだとコンパイルエラーが出ます。

データ型が違うとのこと。

Includeを使っても戻り値は_context.CompanyならCompanyでしかありません。

で、思ったのがIncludeってなんやねんって事。

IncludeはSqlでいうところのJoinをして返してくれる

その後Includeについていろいろ調べていました。

結果わかったことはInclude関数はSqlでいうところのJoinをした結果を全て返してくれるものということが分かりました。

どういうことかというと、下記のプログラムで取得したデータ

var company = await _context.Company.FindAsync(id);

これは、Sqlに直すと下記のようになります。

select * from Company where id = @id

これだとModel参照で取得しようとしているEmployeeのテーブルのデータが取れずにNullになってしまいます。

一方でIncludeを使ってCompanyを取得すると下記のようになります。

var company = await _context.Company.Include(com => com.Employees).FirstOrDefault(com => com.Id == id);

これだとSqlに直すとこんな感じ

select * 
from Company as a
inner join Employees as b
on(a.EmployeeId = b.Id)
where a.Id = @id

こんな感じになるので、あくまでも戻り値はCompanyなのですが、Companyの中にはEmployeeが入っているということになります。

値が入っていますね!

まとめ

今回ThenIncludeの解説を省きましたが、端的に言うとThenIncludeはIncludeで取ったデータのその先のデータを取得できるというものになります。

まあおそらくIncludeについての動きが分かった状態で下記の公式リファレンスを見れば、何をどのようにすれば目的のデータが取れるかはわかると思います!

https://docs.microsoft.com/ja-jp/ef/core/querying/related-data/

今回のソースコードはGithubに上げています!

今回のソースコードは下記リポジトリの中にAspnet=>IncludeDemoという形で置いています!

ご自由にお使いください!

あわせて読みたい
GitHubリポジトリについて 【このブログで紹介しているコードをまとめてアップしています】 下記リポジトリで、このブログで紹介している全てのコードを公開しています。 是非ローカルにクローン...

💡 補足: 業務系の現場でよくハマるパターン

俺もこの Include() 関数、業務でハマってきたところを3つ並べておきます。

① 多段 Include の N+1 問題で本番が遅い

Include().ThenInclude().ThenInclude() を3階層以上重ねると、生成 SQL が巨大化して逆に遅くなる。EF Core 5+ なら AsSplitQuery() を併用して分割クエリ化。N+1 を解決するつもりが Cartesian explosion で死亡するパターン頻出。

② コレクション Include で重複行

One-to-Many で .Include(b => b.Posts) すると親エンティティが Posts 件数分重複する。Distinct() か AsSplitQuery() で対処。気づかず Count() すると2倍3倍の数字が出てパニックになる。

③ Lazy Loading 有効なのに気づかず明示 Include なし

EF6 → EF Core 移行時、Lazy Loading が無効に変わったのに気づかず Include() 書かない → 関連エンティティが null。半日デバッガで追ってから「Include 忘れてただけ」と判明する罠。

❓ よくある質問

Q1. Include() と Select() の使い分けは?

A. 全カラム必要 + ナビゲーション辿りたい → Include / 必要列だけ取って軽量化 → Select で射影。大量データなら Select の方が圧倒的に速い。

Q2. AsNoTracking() と組み合わせるべき?

A. 読み取り専用なら必ず AsNoTracking()。 変更検知トラッキングのオーバヘッドを削減できて、 大量 SELECT で 30-50% 高速化。

Q3. Include() vs 手書き JOIN どっち?

A. シンプル結合は Include / 集計系・複雑条件は手書き JOIN or Raw SQL。 EF Core 7+ で Linq 表現力上がったので、 Raw SQL は本当に必要な時のみ。

Q4. Include() のキャッシュ効果は?

A. ChangeTracker が同一クエリ内の重複エンティティを統合するので、 同じ親が複数の子から参照されても1インスタンス。 これは Include の隠れた利点。

Q5. EF Core から Dapper に切り替えるべきタイミング?

A. EF Core で N+1 / 巨大 JOIN / バッチ更新 に詰まったら検討。 ORM 3択の判断 で具体的な判断軸を書いてます。

📚 関連記事

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次