Skip to content

MS_EntityFrameworkConcerns

nishi_74322014 edited this page Aug 18, 2026 · 1 revision

Entity Framework の懸念

概要

Entity Frameworkを利用する上での懸念をまとめてみた。

補足(本ページの読み方): 本ページは EF6 時代の評価であり、
ADO.NET vs ORM (Entity Framework, Dapper)」の
ORM 側に対する批判をより詳しく展開したもの。
指摘の多くは現在も成立する ORM 一般の論点だが、
EF Coreで改善された点もあるため、
節ごとに「現在どうか」を補足する。

外部スキーマ云々

JOINのサポート

JOINの使用

JOINを使用しない内部結合

集計処理のサポート

補足(節の意図): 「集計処理のサポート」は見出しのみで本文が無い。
文脈から、GROUP BY を伴う集計を LINQ で表現しきれるかという
懸念を示していると読める。

EF Core では、GroupBy の翻訳が長く制約されており、
特に EF Core 3.0 では翻訳できない GroupBy
実行時例外になるケースが多かった
(それ以前は暗黙にクライアント側で評価されていた)。
EF Core 7 以降で翻訳可能な範囲がかなり広がっているが、
複雑な集計は生 SQL やビューに逃がすのが現在も無難である。

性能云々

全体的に、

  • context へのアクセス
  • 呼び出す LINQ メソッド

から

  • どういう SQL が実行されるのか?
  • 内部動作はどのようになっているのか?

などがブラックボックス化されているため、
Entity Framework 自体の仕様に精通していないと、

ハイパフォーマンスな実装をすることができない。

という問題がある。

補足(ブラックボックス性は「見れば」緩和できる): この指摘の本質は
「見えない」ことなので、ログを出せば大幅に改善する

ただし、「書いた LINQ から発行される SQL を予測できる必要がある」という
本質的な難しさは残る。この点は現在も ORM 全般の弱点である。

検索SQLが検索キーや射影の列名を指定しない

foreach でデータアクセスした場合など、
検索 SQL が検索キーや射影の列名を指定しないので性能的に問題になる。

補足: context.YYYYYs を直接 foreach すると
SELECT(全列・全行)が発行される。
絞り込みと射影を LINQ 側で書けば WHERE / 列指定に翻訳される。

// 全列・全行
foreach (var y in context.YYYYYs) { }

// WHERE と列指定に翻訳される
var rows = await context.YYYYYs
    .Where(y => y.DeptId == deptId)
    .Select(y => new { y.Id, y.Name })
    .ToListAsync();

つまり「EF が列名を指定しない」のではなく、
指定するように書いていないというのが正確なところである。

Iterator的にDBアクセスしてしまう。

以下の様な手段により、処理対象の Entity をメモリに起こしてしまう。

ToArray()メソッド

この場合、ToArray() メソッドを使って、はじめに結果を確定させる。

var YYYYYs = context.YYYYYs.ToArray();

Load()メソッド

この場合、Load() メソッドを使って、DbContext 内にデータをキャッシュさせる。

context.YYYYYs.Load();

この後、context.YYYYYs にアクセスしても SQL は実行されない。

大量データ処理

ライブラリがいくつかある。(非 MS 製、NuGet からインストール可能)

補足(最新化:一括更新は標準機能になった): この節の懸念は、
EF Core 7 で ExecuteUpdate / ExecuteDelete が追加されたことで解消した。
サードパーティ ライブラリを入れなくても、SQL 1 発で一括処理できる。

await context.YYYYYs
    .Where(y => y.CreatedAt < threshold)
    .ExecuteDeleteAsync();

なお、これらは変更追跡を経由しないため
SaveChanges は不要であり、逆にメモリ上のエンティティとは
同期しない点に注意(Entity Framework参照)。
大量の挿入は現在も SqlBulkCopy が最速である
SQL Server 大量データ処理時の性能問題)。

LINQ to Object vs LINQ to Entities

処理される場所が異なる

となる。

  • 基本的には、LINQ to Objectが高速だが、

  • ストレージが必要な大量データ処理になってくると、
    LINQ to Entitiesの方が高速になる可能性がある。

補足(比較の前提): 「LINQ to Object が高速」というのは
既にメモリ上にあるデータに対する演算の話である。
DB 上のデータを扱う場合は、

  • LINQ to Entities: DB 側で絞り込み、必要な行・列だけ転送
  • LINQ to Objects: 全件転送してからメモリ上で絞り込み

となるため、通信量と DB 側のインデックス活用の差で
ほぼ常に LINQ to Entities が有利である
SQL Server のインデックス)。

処理結果が異なる

処理される場所だけでなく、処理結果が異なることがある。

  • 以下は、DB 側で Name でソート(OrderBy)される。
context.YYYYYs.OrderBy(x => x.Name).ToArray()
  • 以下は DB からロードされた後、メモリ上で Name でソート(OrderBy)される。
context.YYYYYs.ToArray().OrderBy(x => x.Name)

DBMS の照合順序などの関係により、
処理される場所によって、ソート(OrderBy)順が異なるためである。

移行メモ(誤字): 元ページの OrberByOrderBy の誤記。
本文中の記述も併せて修正した。

補足(この指摘は重要): これは性能ではなく正しさの問題であり、
現在も完全に成立する。

  • DB 側: 照合順序(例 Japanese_XJIS_100_CI_AS)に従う
    → 大文字小文字を区別しない、全角半角を区別しないなど
  • メモリ側: .NET の StringComparer(既定はカルチャ依存)に従う

「ページングは DB 側、表示順の微調整はメモリ側」のように
ソートの場所が混在すると順序が破綻する
ページング(Skip/Take)を伴う場合は特に致命的で、
同じ行が 2 ページに出たり欠けたりする。

トラッキングしない

AsNoTracking() メソッドで Collection を取得すると、
Entity Statesのトラッキングをしなくなる。

var YYYYYs = context.YYYYYs.AsNoTracking().ToArray();

これにより、使用するメモリ量を削減できる。

補足: メモリだけでなく、変更追跡のためのスナップショット作成が
省かれるため CPU も削減でき、参照専用のクエリでは体感差が出る。
ただし、AsNoTracking() で取得したエンティティを Update() すると
全列が更新対象になる点に注意
Entity Framework参照)。
EF Core では QueryTrackingBehavior.NoTracking で既定値を変更することもできる。

コードファースト云々

  • クラス名(+s) がテーブル名にマップされる。

  • プロパティ名がカラム名にマップされる。

  • 主キーは id or クラス名 + id がデフォルト。

  • 参考

マイグレーション

コードファーストのデータモデル クラスの変更をもとに、
既存のデータを残したままデータベースのテーブルを変更する機能。

規約

コードファーストでは規約に基づいて Entity と Database を紐付ける。

属性

属性による規約。

項番 属性名 説明
1 Table Entity と Table 間のマッピング
2 Column Entity の Property と Table の Column 間のマッピング
3 ComplexType Entity の複数の Property(Properties) と Table 間のマッピング
ComplexType の複数の Property(Properties) は、別の Table に切り出される。
4 Key Entity の Property を対応する Table の PrimaryKey に設定する。
5 Required Entity の Property に対応する Table の Column を NOT NULL に設定する。
MVC の ModelMetadata として検証処理に使用される。
6 MaxLength Entity の Property に対応する Table の Column のデータ長を設定する。
MVC の ModelMetadata として検証処理に使用される。
7 Index Entity の Property に対応する Table の Column を使用した非クラスタ化・インデックスを作成する。
8 NoMapped Entity の Property を Database にマップしない。

その他、DbContext を継承した Context のコンストラクタで接続文字列を決定できる。

移行メモ(正誤): 項番 8 の属性名は NoMapped ではなく NotMapped が正しい。
また、EF Core では [Index] 属性は
一時期廃止されたのち EF Core 5 で復活しており
[Index(nameof(Prop))] としてクラスに付与)、
EF6 の書き方(プロパティに付与)とは異なる。
ComplexType は EF Core では長く未サポートで、
EF Core 8 で複合型(Complex Types)として再導入された。

Fluent API

Fluent API による規約。

補足(属性と Fluent API の使い分け): 両方使える場合、
Fluent API が優先される
属性はエンティティ クラスに永続化の知識を持ち込むため、
ドメイン モデルを純粋に保ちたい場合は
IEntityTypeConfiguration<T> に分離するのが定石である。

ナビゲーション・プロパティ

エンティティ間の関連(アソシエーション)を表す。

  • x 対一は、オブジェクト参照を使用。
  • x 対多は、ICollection<T> を使用。

補足(遅延読み込みの既定が違う): EF6 は
ナビゲーション プロパティを virtual にすると遅延読み込みが既定で有効だったが、
EF Core では既定で無効である
Microsoft.EntityFrameworkCore.Proxies を導入して
UseLazyLoadingProxies() を呼ぶと有効化できる)。

遅延読み込みは N+1 問題の主要因であるため、
EF Core の既定(明示的な Include)のほうが安全側に倒れている。

批判云々

  • 性能と柔軟性が重視されるエンタープライズの領域では、使い難いとの批判も多い模様。

  • ツール等のスタンドアロンのプログラム、情報系システムなどの EUC による
    RAD 開発などにはマッチする可能性もある。

  • また、ADO.NET は枯れた技術であるのに対し、Entity Framework は進化の余地がある。

信任投票

補足(信任投票の顛末): 2008 年の "Vote of No Confidence" は、
EF v1 が「永続化非依存(Persistence Ignorance)を欠く」
「モデル駆動が過剰」といった点を批判したもの。
その後、EF 4.1 の Code First と POCO サポートによって
批判の中心的な論点はほぼ解消された。
歴史的経緯として読むべき節である。

ORMの問題

気付き

Entity Framework をキャンセルしたASP.NET Identity
UserStoreを実装して「ORM の問題だな。」と思った点は、

プログラムの

  • インターフェイスとなっている親子階層のあるオブジェクト・モデルと、
  • ストレージとの同期処理(セーブ・ロード)のタイミングなどが、

実装者にとって「難しい。」ってトコロではないかと思う。

背景

  • 難しさの背景は、

    • 自分でUserStoreを実装していてもこの同期は難しい。

      • 同期処理の仕様を決める必要がある。
      • 同期処理の仕様を思い出し、
        処理全体との繋がりを理解する必要がある。
    • ORM プロダクトでブラックボックス化した場合、

      • 同期処理の仕様を決める必要は無いが、
      • 同期処理の仕様を調べたり、デバッガでリバースして
        Entity Framework の調査)、
        処理全体との繋がりを理解する必要がある。

    等と言った点だと思う。

    • UserStoreは、Memory モードと DBMS モードを処理するので、
      Memory モードの実装の後、DBMS モードを実装しようとしたタイミングで
      この難しさに気付く。

    • オブジェクトを操作したタイミングで直ちに同期される仕様にすれば、
      問題はなくなるが、実際は、性能云々に書いたように、
      性能を考慮して適切なタイミングに同期する必要がある。

補足(この難しさの正体): ここで述べられているのは、
オブジェクト グラフの永続化タイミングという、
ORM の本質的な難所である。
EF の SaveChanges() や JPA の flush が
「いつ、どこまでを書き込むのか」を理解しにくいのと同根で、
Unit of Work パターンが抱える固有の複雑さと言える。

実務での緩和策は、

  • 集約(Aggregate)の境界を小さく保つ
    親子階層を深くしない
  • DbContext の寿命を短くする
    ASP.NET Core の Scoped(リクエスト単位)が既定なのはこのため
  • 読み取りと書き込みでモデルを分ける(CQRS 的な割り切り)

あたりになる。

エンプラ領域でのミスマッチ

基幹系システムでは以下の理由でミスマッチと判断されることが多い。

インピーダンス・ミスマッチ

概念モデルに対してプログラミングを採用している。

  • 外部スキーマを使用できないため、論理データ独立性が無い。
  • 概念スキーマのデータ構造を使用してプログラミングを行う必要がある。

スキーマ構造の変更

  • DBMS のスキーマからモデル(EDM)を生成する方法を採用している場合、
    スキーマの構造が変わった場合、モデル(EDM)の作り直しが発生する。

  • 生成されたモデル(EDM)をカスタマイズしていた場合、
    作り直しにより、カスタマイズが破棄されてしまう可能性がある。

    • カスタマイズがなければ、概念スキーマ構造の変更を
      モデル(EDM)に同期し、変更に迅速に対応できるとも言える。
    • しかし、論理データ独立性が無いため、プログラムの変更は必要になる。
  • RAD 開発ツールに特有の、保守性の悪さがある。

補足(EDMX が無くなったことによる変化): EF Core には EDMX が無く、
リバース エンジニアリングは dotnet ef dbcontext scaffold による
C# コード生成である。
生成先を partial class として扱い、カスタマイズを別ファイルに置けば、
再 scaffold でカスタマイズが失われる問題は緩和できる。
ただし「論理データ独立性が無い」という指摘そのものは、
エンティティ=テーブルという対応を採る限り現在も成立する。

対処としては、

  • DB 側にビューを作り、それをエンティティにマップする
    (EF Core の ToView()。外部スキーマに相当する層を DB 側で持つ)
  • 読み取り専用の DTO へ直接射影するSelect で必要な形に整形)

といった形で、外部スキーマ的な層を別途設けることになる。

内部実装とその動作がブラックボックス

SQL が、LINQ to Entity のエンジンに生成される形であり、
また、内部実装とその動作がブラックボックスになっているため(※1)
それらが明確にならないと設計、チューニング、問題分析などが困難。

従って(特に日本の)エンタープライズ・アプリケーションでは敬遠されている。
これは、Java の Java8 で JPA で Hibernate で Jinq が敬遠されるのと ≒。

最新の動向

エンプラ界隈でのEntity Framework離れ進んでますね。

https://www.osscons.jp/jowlrb9pr-537/

NoSQL用のEntity Frameworkが流行っていない。

  • Entity Framework、RDB より NoSQL のほうがマッチしそう。
  • しかし、長々、NoSQL 用の Entity Framework プロバイダがリリースされなかった。
  • 2018 年に EF コアが SQL データベースと NoSQL データベースを統一したらしい。
  • ...しかし、まったく流行っていない感。

補足(最新化:Cosmos DB プロバイダ): ここで言及されているのは
EF Core の Azure Cosmos DB プロバイダ(EF Core 3.0 で正式提供)である。
現在も提供・改善が続いているが、

  • リレーショナル固有の機能(結合、マイグレーション等)は使えない
  • Cosmos DB 側の API(パーティション キー、RU 消費)を意識する必要があり、
    結局「抽象化できていない」

ため、NoSQL では各サービスの SDK を直接使うのが主流のままである。
「まったく流行っていない感」という観察は、現在も概ね妥当と言える。

補足(現在の総括): 本ページの懸念のうち、

懸念 現在
一括更新ができない 解消ExecuteUpdate / ExecuteDelete
遅延読み込みによる N+1 緩和(EF Core は既定で無効)
EDMX の作り直し 解消(EDMX 自体が無い)
信任投票の論点(POCO / 永続化非依存) 解消(Code First)
発行 SQL が見えない 緩和(ログ・クエリ ストア)
論理データ独立性が無い 残る(ビューや DTO で別途対処)
複雑な集計・動的クエリの表現力 残る(生 SQL との併用が前提)

結論として、現在は排他ではなく併用が定石である。
大半の CRUD は EF Core、性能が要る箇所と複雑な参照系は
Dapperか生の ADO.NET、という構成が広く採られている
ADO.NET vs ORM (Entity Framework, Dapper))。


Tags: 移行, .NET開発, データアクセス, ADO.NET, Entity Framework, 性能

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally