ASP.NET MVC 5 で使える ORM 3択 — EF6 / Dapper / ADO.NET の業務SE 視点比較

みなさんこんにちは!ヒロポンです!!

今回は ASP.NET 生存ガイド連載・第4回 の本論記事。WinForms 業務SE が ASP.NET MVC 5 で DB アクセス層 を選ぶ時の話。

「ASP.NET 案件 = EF Core が必須でしょ??」「DbContext ってなんだ??」「Migration とか怖くて触れない」って固まったこと、ないっすか??

結論から言うと、.NET Framework 4.7.2 系の業務系で使える ORM は EF6 / Dapper / ADO.NET の3択で、EF Core は .NET Core 系のため本連載では無視していい。俺は 流通系SIer 時代から EF6 + Code First Migration を本格使用、フリーランス副業でも継続して触ってるので、業務系での EF6 の立ち位置は実体験ベースで書けます。

連載第1回(Form ↔ Razor View)で View、第2回(Controller = Form_Load 拡張版)で Controller、第3回(ルーティング)で URL → Action の流れを押さえたので、今回は 「Action 内部の DB アクセス層」 を扱います。連載通奏低音の「レイヤー分離思考」は、今回で 「ORM レイヤー / DB レイヤー / 接続文字列レイヤー」の3層分離 に再拡張します。

そしてここで先に強く出しておきたい結論があって、ORM 選びは宗教論争じゃない、案件の性質で決まる。ただし 本番接続だけは死守して意識する のが業務SE 鉄則です。俺の実戦経験で最大のハマりがそこに集中してるんですよね。

この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 / ASP.NET MVC 5 環境で、ORM 3択の使い分けEF6 + Code First Migration の最小コード接続文字列を本番/Dev で意識する3つの目視ポイント をコード6本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。

3行で結論:

  • 業務系(.NET Framework 4.7.2)で使える ORM は EF6 / Dapper / ADO.NET の3択(EF Core は別物・本連載では無視)
  • ORM 選びは宗教論争じゃない、案件の性質で決まる(モデル中心 → EF6 / 既存 SQL 活用 → Dapper / 性能チューニング必須 → ADO.NET)
  • 本番接続だけは死守して意識するUpdate-Database の前に 接続先サーバ名を声に出して読む が業務SE 鉄則
目次

俺の体験 — 流通系SIer 時代の EF6 実戦

正直に書いておきます。俺が初めて EF6 を本格使用したのは 流通系SIer 時代 で、Code First Migration を VS パッケージマネージャコンソールの PM> Add-Migration / Update-Database で回す運用フローでした。フリーランス副業でも継続して触ってる。

EF6 の評価は 「普通に DB の変更を自動で行ってくれるから便利」。モデルを変えたら Add-Migration で diff の DDL が生成され、Update-Database で適用される。既存の DataAdapter + DataTable から見ると「ADO.NET から少し背伸び」の階段感で、EF Core ほど別世界じゃない。

ただし業務系で 最大のハマり がここにある:

「今接続しているのが本番か Dev かってのを意識しないと確実に事故る」

DbContext は connection string で接続先を決めて、Update-Databaseその瞬間繋がってる DB に対して DDL を実行する。開発中に 本番接続文字列が紛れ込んだ状態で Update-Database 走らせる本番 DB に予期せぬ DDL 実行 が一発レッドカード事故になる。

連載通奏低音の レイヤー分離思考(child #1〜#3 と継続)が、今回では ORM レイヤー / DB レイヤー / 接続文字列レイヤー の3層分離に進化する。接続文字列レイヤーは ORM コードと別の存在として常に意識する のが今回の最大のメッセージっす。

対応マップ — ORM 3択の比較

業務系 .NET Framework 4.7.2 で使える ORM の比較を5観点でまとめると、こんな感じ:

観点 ADO.NET Dapper EF6
学習コスト 低(DataAdapter 経験者は即) 中(薄い ORM) 高(DbContext / Migration)
SQL 可視性 完全(生 SQL) 高(生 SQL + マッピング) 低(LINQ → SQL 自動生成)
Migration 自動化 手動 手動 自動(Add-Migration / Update-Database)
チーム適合 WinForms 経験者向き 中間解として モデル中心の開発向き
俺の選択実績 業務系で多数経験 軽量で扱いやすい 流通系SIer時代に本格使用 / フリーランス副業で継続

判断軸:

  • 巨大既存テーブル + 性能チューニング必須 + DataAdapter 経験者チーム → ADO.NET
  • 既存 SQL を活かしつつマッピングだけ楽にしたい → Dapper
  • 新規モデル中心の開発 + Migration を回したい → EF6

ここから順に、コード対比で見ていきます。

定石1: ADO.NET をそのまま使う最小コード

業務系で 既存資産との地続き を選ぶなら、ADO.NET が最もコストが低い。連載第3回までで紹介した SqlConnection + SqlCommand + SqlDataReader の構成がそのまま使える:

// ✅ 定石1: ADO.NET 最小コード(Controller の Action 内で使う)
public class CustomerController : Controller
{
    public ActionResult Index()
    {
        var customers = new List<CustomerVm>();

        using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["MyDb"].ConnectionString))
        using (var cmd = new SqlCommand("SELECT id, name FROM customers WHERE status = @s", conn))
        {
            cmd.Parameters.AddWithValue("@s", "active");
            conn.Open();

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    customers.Add(new CustomerVm
                    {
                        Id = reader.GetInt32(0),
                        Name = reader.GetString(1),
                    });
                }
            }
        }
        return View(customers);
    }
}

ポイント:

  1. 既存 WinForms 知識がそのまま使えるDataAdapter 経験者なら半日で書ける)
  2. 生 SQL が完全に見える(パフォーマンスチューニングがしやすい)
  3. Migration は手動(DDL の管理は SQL Script で自前運用)

業務系の判断軸: ん?じゃあ ADO.NET だけで全部書けばよくない??って思うかもだけど、既存業務系を ASP.NET に移植する場合は ADO.NET 寄せが最も学習コストが低くて、新規モデル中心開発なら EF6 に進む方が後段が楽になります。

定石2: Dapper を導入する最小コード

Dapper薄い ORM で、ADO.NET の SqlCommand.ExecuteReader の上に マッピングだけ を被せた軽量ライブラリ:

PM> Install-Package Dapper
// ✅ 定石2: Dapper 最小コード(SQL を書きつつマッピングは自動)
using Dapper;

public class CustomerController : Controller
{
    public ActionResult Index()
    {
        using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["MyDb"].ConnectionString))
        {
            // ↓ SQL を渡すだけで CustomerVm にマッピングしてくれる
            var customers = conn.Query<CustomerVm>(
                "SELECT id AS Id, name AS Name FROM customers WHERE status = @s",
                new { s = "active" }
            ).ToList();

            return View(customers);
        }
    }
}

ポイント:

  1. 生 SQL は維持 — 性能チューニング / 複雑な JOIN は SQL で書ける
  2. マッピングだけ自動reader.GetInt32(0) の手書きから解放
  3. Install-Package Dapper の1行で導入(依存が軽い)
  4. 学習コスト低(30分で書けるようになる)

業務系の判断軸: 既存 SQL を活かしつつコード量を減らしたい時 に Dapper が刺さる。EF6 ほどの抽象化は要らない、でも生 ADO.NET の手書きマッピングは面倒、という中間解として優秀です。

定石3: EF6 + Code First Migration の最小コード

ここからが今回の 。俺の実体験ベースで書きます。

PM> Install-Package EntityFramework
// ✅ 定石3-a: DbContext の定義
using System.Data.Entity;

public class MyDbContext : DbContext
{
    public MyDbContext() : base("name=MyDb") { }   // Web.config の MyDb 接続文字列を参照

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Status { get; set; }
    public virtual ICollection<Order> Orders { get; set; }   // Lazy Loading
}

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public decimal Amount { get; set; }
    public DateTime CreatedAt { get; set; }
    public virtual Customer Customer { get; set; }
}

Migration のフロー(VS パッケージマネージャコンソール):

# 初回 Migration を作成
PM> Add-Migration Initial

# 生成された Migration クラスを確認(Migrations/202605120500_Initial.cs)
# DDL(CREATE TABLE 等)が C# コードとして生成される

# DB に適用(接続文字列が指す DB に対して DDL 実行)
PM> Update-Database

# モデル変更後は再度 Add-Migration
PM> Add-Migration AddCustomerEmail
PM> Update-Database

Controller での使い方:

// ✅ 定石3-b: EF6 で Controller から DB アクセス
public class CustomerController : Controller
{
    private MyDbContext _db = new MyDbContext();

    public ActionResult Index()
    {
        var customers = _db.Customers
            .Where(c => c.Status == "active")
            .Include(c => c.Orders)   // ← N+1 回避のため Eager Loading
            .Select(c => new CustomerVm { Id = c.Id, Name = c.Name })
            .ToList();
        return View(customers);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing) _db.Dispose();
        base.Dispose(disposing);
    }
}

ポイント:

  1. Install-Package EntityFramework の1行で導入
  2. DbContext 定義 → Add-MigrationUpdate-Database の3ステップ
  3. LINQ で SQL を書く(コードビハインドの SQL が消える)
  4. .Include() で関連エンティティを Eager Loading(N+1 回避)
  5. DbContext のライフサイクルは Controller スコープ(リクエスト終了で Dispose)

普通に DB の変更を自動で行ってくれるから便利」というのが俺の正直な評価。モデルを変更したら Add-Migration MigrationName で diff の DDL が生成され、Update-Database で適用される。手で DDL を書く手間がいい感じに減るんですよね。

定石4: 接続文字列を本番/Dev で切り替える Web.config Transform

EF6 + Code First Migration の 最大のハマり に直結する話。Web.config Transform を使って 構成ごとに接続文字列を切り替える のが業務系の鉄則:

<!-- ✅ 定石4-a: Web.config(開発時のデフォルト) -->
<connectionStrings>
  <add name="MyDb"
       connectionString="Server=localhost;Database=MyApp_Dev;Integrated Security=true"
       providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- ✅ 定石4-b: Web.Release.config(Release 時に変換される) -->
<connectionStrings>
  <add name="MyDb"
       connectionString="Server=prod-sql.example.com;Database=MyApp_Prod;User Id=app_user;Password=***"
       xdt:Transform="SetAttributes" xdt:Locator="Match(name)" />
</connectionStrings>

ポイント:

  1. Web.config は開発用デフォルト(localhost / Dev DB)
  2. Web.Release.config は Release ビルド時に変換(本番接続文字列に置き換わる)
  3. VS の構成設定(Debug/Release)と連動
  4. xdt:Transform="SetAttributes" で接続文字列を書き換える

これだけだと「VS の構成」と「接続文字列」が見えにくいので、業務系チームでは更に運用ルールを足します(次のセクション)。

本番接続事故を防ぐ運用ルール — 「接続先を声に出して読む」3つの目視ポイント

ここが今回の 最重要セクション。俺の言葉そのまま再掲:

「今接続しているのが本番か Dev かってのを意識しないと確実に事故る」

Update-Database を打つ前に、接続先サーバ名を声に出して読む ことを業務系チーム規約に入れるのが事故予防の最大の対策。声に出して読む3つの目視ポイント:

1. VS の構成設定 + 起動プロジェクト

  • VS 上部のドロップダウンが Debug / Release のどちらか
  • 起動プロジェクトがどのプロジェクトに設定されているか
  • Release で Update-Database を打つと本番接続文字列が読まれるリスク

2. Web.config / app.configconnectionString が指す DB サーバ名

  • PM コンソールが読む config ファイルは 「起動プロジェクトの app.config / Web.config」
  • そのファイルの <connectionStrings> セクションで Server= の値を声に出して読む
  • Server=localhost なら Dev、Server=prod-sql.example.com なら本番

3. Update-Database -Verbose の表示

PM> Update-Database -Verbose
# 表示例:
# Target database: 'MyApp_Dev' (DataSource: 'localhost', ...)

Target database の行を声に出して読むlocalhost なら Dev、本番サーバ名が出たら 即 Ctrl+C で停止して環境を見直す。

業務系チームの運用ルール

俺の流通系SIer 時代に運用していたルール:

  1. 本番 DB への自動 Update-Database は走らせない(手動 SQL Script Migration 経由)
  2. Update-Database -Script で生成 SQL を確認してから本番デプロイ
  3. Dev 環境の Update-Database も、上記3つの目視ポイントを声に出して読んでから実行

これでもう一段の安全策として:

# ✅ 定石4-c: 本番には Script で安全に Migration を適用
# 1. ローカルで Script を生成
PM> Update-Database -Script -SourceMigration:InitialDatabase -TargetMigration:AddCustomerEmail

# 2. 生成された .sql ファイルをレビュー
# 3. DBA / リードエンジニアが本番に手動適用
# 4. 自動 Update-Database は本番では走らせない

このフローを業務系チームで揃えると、「本番 DB に予期せぬ DDL 実行」事故は構造的に起きなくなる のがいい感じに効きます。

ハマりポイント — 俺の実体験ベース

1. Update-Database が本番 DB に当たりかけて血の気が引いた朝(半日デバッガで追ってハマった)

Web.Release.config で本番接続文字列に置き換わる設定で、VS の構成を Release のまま Update-Database を打ちかけた事件。朝、PM コンソールで打つ直前に「待てよ、Release が読まれるんじゃないか??」と気付いて手が止まった。Verbose で確認したら本番接続文字列が読まれる挙動で、半日デバッガで追ってハマった。これが 3つの目視ポイントを声に出して読む ルールの起点。

2. N+1 問題で1リクエスト100SQL(30分溶かした)

EF6 で Customer → Orders を Lazy Loading でループ参照、100件で101本の SQL 事件。30分溶かした末に .Include(c => c.Orders) で Eager Loading に切り替えて1本に。ループ前に Include を業務系チーム規約化した。

3. DbContext を static で持ち回して変更追跡が混ざった(数日プロファイラで追った)

DbContext をシングルトンで持ち回した結果、ユーザーA の SaveChanges でユーザーB の編集中エンティティが意図せず保存される事故。数日プロファイラで追った末に、Controller スコープ(リクエスト単位)で Dispose する形に修正。DbContext は短命に保つ を業務系チーム規約に揃えた。

俺の現場メモ — 業務系チームでの ORM 規約

流通系SIer時代に過去コードを grep -rnE "SqlConnection|Dapper|DbContext" . で90箇所近くひっかけたら、ADO.NET と EF6 が混在・接続文字列の管理がバラバラ・本番への自動 Update-Database が走るリスクあり、で全部入りだった。後輩と一緒に 3行ルール にまとめた:

  1. 新規モデル中心開発は EF6 寄せ、既存資産活用は ADO.NET / Dapper(混在は避ける)
  2. 接続文字列は Web.config Transform で構成ごと管理(直書きハードコード禁止)
  3. Update-Database 前に3つの目視ポイントを声に出して読む(VS 構成 / Web.config Server / Verbose 出力)

このルール化で、ORM 周りの本番接続事故・N+1・DbContext ライフサイクル事故が消えた。書き方を1パターンに揃えるだけで保守工数と事故率が両方下がるので、業務系チームにはおすすめのルールっす。

まとめ

状況 推奨 ORM
既存業務系を移植・DataAdapter 経験者チーム ADO.NET
既存 SQL を活かしつつマッピング楽にしたい Dapper
新規モデル中心開発・Migration を回したい EF6
巨大既存テーブル + 性能チューニング必須 ADO.NET
接続文字列管理 Web.config Transform で構成ごと
Update-Database 3つの目視ポイントを声に出して読む
本番 DB への DDL 適用 手動 SQL Script Migration 経由
DbContext ライフサイクル Controller スコープ(リクエスト単位)
N+1 回避 ループ前に .Include() で Eager Loading

業務系の ORM 選びは、「案件の性質で決まる」「3層分離思考」「接続先を声に出して読む」 の3点で9割困らなくなります。ORM レイヤー / DB レイヤー / 接続文字列レイヤー の3層を分けて考えれば、ORM 選びの宗教論争に巻き込まれず、本番接続事故も予防できる。次回(連載第5回)は「DI は業務系で必要か — 入れない派の論点」を扱うので、DbContext のライフサイクル管理を DI コンテナで扱う話まで踏み込みます。

よくある質問

Q1. .NET Framework 4.7.2 で EF Core は使えますか?

A. 実用的には使えないと思っておくのが安全です。EF Core は .NET Core / .NET 5+ が前提で、.NET Framework 4.7.2 系では NuGet で参照できても性能・安定性のテストが薄く、業務系本番では EF6 一択 が現実解です。Microsoft の公式ガイドも .NET Framework 系は EF6 を継続推奨しています。本連載で EF と呼ぶ時は EF6 を指します。

Q2. EF6 と Dapper と ADO.NET、新規プロジェクトならどれを選ぶ?

A. 案件の性質で決まるので宗教論争じゃないです。判断軸は(1)モデル中心の新規開発で Migration を回したい → EF6、(2)既存 SQL を活かしつつマッピングだけ楽にしたい → Dapper、(3)巨大既存テーブル + 性能チューニング必須 + チームが DataAdapter 経験者だけ → ADO.NET、の3つ。著者は流通系SIer 時代から EF6 で Code First Migration を本格使用しているので、新規なら EF6 寄せが現実的です。

Q3. EF6 の Code First Migration で「接続先が本番か Dev か」を意識するって、具体的にどう確認する?

A. 3つの目視ポイントで確認します。(1)VS の構成設定(Debug/Release)と起動プロジェクトの確認、(2)Web.config / app.config の connectionString が指している DB サーバ名、(3)PM コンソールで Update-Database -Verbose を打って表示される接続先サーバ名。Update-Database を打つ前にこの3点を声に出して読む という運用ルールが、業務系で本番事故を防ぐ最大の対策です。本番への自動 Update-Database は走らせず、手動 SQL Script Migration(Update-Database -Script)で生成 SQL を確認してからデプロイする運用が業務SE 鉄則です。

Q4. EF6 の N+1 問題ってどう対処する?

A. .Include() で関連エンティティを Eager Loading するか、AsNoTracking() + Select で射影の2択です。デフォルトの Lazy Loading だと、ループの中で関連プロパティを参照するたびに SQL が発行されて、100件ループで101本の SQL が走る事故が起きる。EF6 は db.Customers.Include(c => c.Orders).ToList() で関連も一発取得できるので、ループ前に Include しておくのが業務系の鉄則。詳しいパフォーマンスチューニングは別記事に書きますが、まず Include を覚えるだけで N+1 の9割は防げます。

Q5. DbContext のライフサイクルは?

A. ASP.NET MVC 5 の業務系では リクエスト単位(リクエストごとに new、リクエスト終了で Dispose)が原則です。Controller のコンストラクタで new し、Dispose は MVC が自動で行うか、Controller の Dispose をオーバーライドして明示的に呼ぶ形。シングルトンや static で持ち回すと、複数リクエストの変更追跡が混ざって事故るので避けるのが業務系の判断軸。詳しい DI コンテナでのライフサイクル管理は連載第5回(DI 編)で深掘りします。

ここまでで ORM 3択の使い分け・EF6 + Code First Migration の最小コード・接続先を意識する3つの目視ポイントは押さえた。次回は DI の話を扱うので、DbContext のライフサイクル管理がもう一段見えてきます。WinForms / ASP.NET の隣接トピックも貼っておきます。

関連記事

ASP.NET 生存ガイド・連載目次

今回は WinForms 業務SE のための ASP.NET 生存ガイド 全10回の 第4回 です。

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!

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

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

コメント

コメントする

CAPTCHA


目次