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コードと別の存在として常に意識するのが今回の最大のメッセージっす。

ASP.NET MVC の ORM 3択 判定フロー (EF6 / Dapper / ADO.NET)
EF6 / Dapper / ADO.NET の観点別比較マトリクス (○×△◎)

対応マップ— 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仲間いたら、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

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

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

コメント

コメントする

CAPTCHA


目次