クライアント操作でDBテーブルに列が増える仕組みを採用すると、EF Coreの固定モデルとスキーマがずれて「追加列も含めて取得したいのに取れない」問題に直面します。この記事では、EF Coreの限界とADO.NETでの動的取得例、さらにJSON列/EAVなど設計の落としどころまで整理します。
前提:EF Coreは「固定スキーマを強い型にマップする」ORM
Entity Framework / EF Core は、DBのテーブル(固定スキーマ)をC#のクラス(エンティティ)にマッピングし、LINQで安全にCRUDできるようにするORMです。ここで重要なのは、EF Coreが前提としている世界観が「スキーマは基本的に固定」であり、モデル(エンティティのプロパティ)が契約になっている点です。
そのため、実行中に ALTER TABLE のような操作で列が増減する設計(いわゆる「動的カラム」「自由項目を列追加で表現」)は、EF Coreの得意分野と真逆になりがちです。
「列が増えただけ」ならEF Coreが壊れにくい理由
まず誤解が生まれやすいポイントとして、DB側に列が増えただけで、EF Coreが即座に例外を投げるとは限りません。通常のLINQクエリは、モデルにマップされた列だけをSELECTするためです。
たとえばエンティティに Id と Name しか定義していないなら、EF Coreは概ね次のようなSQLを生成します(概念図)。
SELECT [u].[Id], [u].[Name]
FROM [Users] AS [u]
この場合、DBに CustomField_2026 のような列が増えていても、EF Coreはその列を読みに行かないので、アプリは普通に動き続けます。
それでも「追加列も含めて取得したい」となると詰まる
問題はここからで、要件が「増えた列も含めて取得して返したい」「画面側で列一覧を動的に表示したい」になると、EF Coreの強み(強い型)がそのまま制約になります。
| やりたいこと | EF Coreの現実 | なぜ |
|---|---|---|
| DBに増えた列を、そのままアプリで取得したい | 標準のエンティティでは難しい | プロパティ(型)が存在しない列を受け取る入れ物がない |
| 追加列を「列名→値」の形で返したい | LINQだけでは完結しない | EF Coreの戻り値は基本的に型付きオブジェクト |
| Raw SQLでSELECT *して全部取りたい | 追加列は基本的に無視される | マッピングはモデル定義に従うため、未知の列は参照できない |
「じゃあdynamicで受ければ?」と思いがちですが、EF Coreはクエリ結果を任意の Dictionary や ExpandoObject に直接マップする、という使い方には向きません。可能に見える手段(シャドウプロパティ、キーなしエンティティ、FromSqlなど)も、“事前に定義された列”の範囲を出にくいのが実情です。
結論:動的列を回収するならADO.NET(またはマイクロORM)に寄せる
やりたいことが「モデル変更なしで、増えた列を含めてデータを回収する」なら、現実解は動的取得できる手段を使うことです。代表がADO.NETの DbDataReader や DataTable で、列情報を実行時に見ながら列名→値を組み立てられます。
(補足)DapperのようなマイクロORMでも同じ方向性で、Query<dynamic> や ExpandoObject として受け取れます。ここでは「.NET標準で完結」しやすいADO.NETを中心に説明します。
DbContextを活かしつつDbDataReaderで動的に読む
EF Coreを全面的に捨てる必要はありません。接続・トランザクション管理はDbContextに乗せたまま、読み取りだけをADO.NETに寄せるのが現場では扱いやすいパターンです。
using System.Data;
using System.Data.Common;
public static class DynamicColumnReader
{
// 任意のSQLを実行し、各行を「列名→値」のDictionaryとして返す
public static async Task<List<Dictionary<string, object?>>> QueryAsDictionariesAsync(
DbConnection connection,
string sql,
IEnumerable<DbParameter>? parameters = null,
CancellationToken cancellationToken = default)
{
bool shouldClose = false;
if (connection.State != ConnectionState.Open)
{
await connection.OpenAsync(cancellationToken);
shouldClose = true;
}
try
{
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
if (parameters != null)
{
foreach (var p in parameters)
{
cmd.Parameters.Add(p);
}
}
var rows = new List<Dictionary<string, object?>>();
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++)
{
var name = reader.GetName(i);
row[name] = await reader.IsDBNullAsync(i, cancellationToken) ? null : reader.GetValue(i);
}
rows.Add(row);
}
return rows;
}
finally
{
if (shouldClose)
{
await connection.CloseAsync();
}
}
}
}
ポイントは次の通りです。
- FieldCount / GetName / GetValue で実行時に列を走査できる
- DBに列が増えれば、
SELECT *(または列を明示しない動的SQL)で新列も自動的に取れる - 戻り値はエンティティではなく Dictionary なので、モデル変更なしで拡張できる
DbContextのトランザクションと同じ文脈で読む(整合性が必要な場合)
「EF Coreで更新した直後に、同じトランザクション内で動的列も含めて読みたい」場合は、DbContextが保持している接続・トランザクションを使うのが安全です。以下は考え方の例です(プロバイダーにより細部は調整してください)。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage; // GetDbTransaction()
public async Task<List<Dictionary<string, object?>>> GetWithExtrasAsync(MyDbContext db, int id)
{
// 例:EF Core側で何か更新している前提
// await db.SaveChangesAsync();
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
{
await conn.OpenAsync();
}
await using var cmd = conn.CreateCommand();
// 重要:DbContextがトランザクション中なら同じトランザクションを使う
var current = db.Database.CurrentTransaction;
if (current != null)
{
cmd.Transaction = current.GetDbTransaction();
}
cmd.CommandText = "SELECT * FROM Users WHERE Id = @id";
var p = cmd.CreateParameter();
p.ParameterName = "@id";
p.Value = id;
cmd.Parameters.Add(p);
var rows = new List<Dictionary<string, object?>>();
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < reader.FieldCount; i++)
{
var name = reader.GetName(i);
row[name] = await reader.IsDBNullAsync(i) ? null : reader.GetValue(i);
}
rows.Add(row);
}
return rows;
}
この形にしておくと、「EF Coreで管理する固定列」と「動的列を含む生データ」を同じ整合性で扱えます。
SELECT * を使うときの割り切りポイント
動的列を確実に拾う一番単純な方法は SELECT * です。ただし、運用が長くなるほど“全部取り”の副作用が出やすいので、次の観点で割り切りを決めておくと事故が減ります。
- 列が肥大化する前提ならページング必須:1回で大量行を返さない
- 巨大な列(画像、BLOB、長文)が混じるなら除外:列一覧をメタデータから取得し、除外リストを適用してSELECT句を組み立てる
- 変更に強いクライアント実装:存在しないキーは無視、未知キーは表示、型は柔軟に扱う
「テーブル名や列名が外部入力」の場合は必ずホワイトリストにする
動的列の話題では、しばしば「画面で選んだテーブル/列をそのままSQLに埋め込む」実装になり、SQLインジェクションの温床になります。値パラメータは @p0 のようにパラメータ化できますが、識別子(テーブル名・列名)はパラメータ化できません。
そのため、少なくとも次のいずれかを徹底してください。
- APIで受け取るのは「テーブル名」ではなく「固定の識別子(enum等)」にし、サーバ側で実テーブル名へ変換する
- 列名も同様に「許可リスト(ホワイトリスト)」で検証し、許可されたものだけをSQLに含める
APIの返し方:固定項目+追加項目(辞書)に分けると扱いやすい
自由項目を含む画面は、だいたい「固定列(ID、名前、作成日など)」と「ユーザーが増やした列(自由項目)」が混在します。ここを一つの巨大なDTOにしようとすると破綻しやすいので、返却形式は次のように固定と可変を分離すると運用が安定します。
{
"id": 1001,
"name": "サンプル",
"createdAt": "2026-01-01T09:00:00+09:00",
"extras": {
"CustomField_2026": "A",
"Priority": 3,
"Memo": null
}
}
こうしておけば、バックエンドは固定項目をEF Coreで管理し、可変項目はADO.NETで回収して extras に詰める、という役割分担がしやすくなります。
DataTableとDbDataReaderはどちらを選ぶ?
どちらも「動的列を扱える」手段ですが、得意分野が違います。
| 手段 | 向いているケース | 注意点 |
|---|---|---|
| DbDataReader | 大量データ、低メモリ、ストリーミング処理 | 逐次読み取り。再利用や検索は自前で組み立てが必要 |
| DataTable | 小〜中規模で「表として扱いたい」ケース | メモリに全件展開。大量件数だと重くなりやすい |
「まずは動くものを作りたい」「画面へそのまま渡すテーブル構造が欲しい」という段階ではDataTableが楽です。一方でAPI化・性能最適化まで考えるなら、DbDataReaderで必要な形(Dictionary/DTO)に落とす方が後々ラクになります。
そもそも「列を増やす」設計が辛くなりやすい理由
動的に列を増やす方式は、最初はわかりやすく見えます。しかし運用が進むほど、次のような問題が表面化しがちです。
- 移行管理が難しい:いつ・誰が・どんな列を追加したか追跡しにくい
- 性能影響:ALTER TABLEはロックや再構築が発生するDBもあり、ピーク時は事故になりやすい
- スキーマ肥大:列が増えるほどテーブルが横に広がり、I/Oやインデックス設計が難しくなる
- 監査・権限:列追加を許すと「本来アプリが作るべきでない構造変更」を誰でもできてしまう
「どうしても列追加が必要」な場合でも、少なくともアプリ起動中に勝手に増える前提は避け、マイグレーション管理に寄せた方が安全です。
根本対策:可変データを“列追加以外”で表現する選択肢
ここからは「動的列を取りたい」という症状の根にある、設計の選択肢です。特に「自由項目」「ユーザー定義フィールド」「カスタム属性」の要件では、列追加よりも相性の良い表現があります。
| 方式 | 概要 | 長所 | 短所 | EF Coreとの相性 |
|---|---|---|---|---|
| 列追加(ALTER TABLE) | 自由項目=物理列 | 単純なSQLで読める | 運用・性能・監査が重い/スキーマが肥大化 | 悪い(モデル変更が前提になりやすい) |
| JSON列 | 自由項目を1列のJSONに格納 | スキーマ固定/追加が容易 | 集計や型保証は工夫が必要 | 良い(固定モデル+JSONのハイブリッドが可能) |
| EAV(属性テーブル) | (EntityId, Key, Value)で保持 | 検索・履歴・権限を設計しやすい | クエリが複雑/型が混ざる | 普通(設計次第で扱える) |
| 子テーブル(定義+値) | 項目定義テーブル+値テーブル | UI/権限/バリデーションと相性が良い | 設計コストは上がる | 良い(リレーションで管理しやすい) |
選択肢:JSON列に寄せる(自由項目が“画面表示中心”なら強い)
自由項目の主目的が「画面で表示する」「入力値を保存する」程度で、複雑な集計やJOINが中心ではないなら、JSON列は強力です。テーブルの物理列は固定のまま、自由項目だけを AdditionalData のような列にJSONで格納します。
例えば、DB上は次のようなイメージです。
-- 固定列は通常通り
Id (int), Name (nvarchar), CreatedAt (datetime2), AdditionalData (nvarchar(max) / json / jsonb)
運用で効いてくるのは「検索・ソートをどこまでJSONに求めるか」です。頻出キーだけは生成列(計算列)で取り出してインデックス化し、残りはJSONのまま、というハイブリッドにすると、柔軟性と性能のバランスが取りやすくなります。
EF Core側では、AdditionalData を string / JsonDocument / 独自型として保持し、必要に応じてシリアライズ/デシリアライズします。プロバイダーによってはJSONを構造化してマッピングできる機能もあるため、「自由項目の形がある程度固まってきた」段階で段階的に型を付けていく運用も可能です。
選択肢:EAV(キー・バリュー)に寄せる(検索や履歴が必要なら検討)
自由項目に対して「項目ごとの権限」「型ごとの検証」「変更履歴」「特定キーでの検索」などが求められるなら、EAV(Entity-Attribute-Value)や、項目定義+値テーブルの構成が向きます。
| テーブル | 例 | 役割 |
|---|---|---|
| CustomFieldDefinition | (FieldId, EntityType, Key, DataType, Required, SortOrder…) | 自由項目の定義(項目名、型、必須、表示順など) |
| CustomFieldValue | (EntityId, FieldId, ValueString, ValueNumber, ValueDate…) | 自由項目の値(型別列に分けると検索しやすい) |
列追加より設計は増えますが、「後から運用で破綻しない」ことを優先するなら、最初からこちらに寄せた方が結果的に安く済むケースが多いです。特に複数テナントや監査要件があるシステムでは、列追加よりも権限・履歴・バリデーションの設計がしやすいのが大きな利点です。
選択肢:スキーマ変更はMigration運用に寄せる(列追加が避けられない場合)
どうしても「物理列を追加したい」なら、アプリの外から勝手にALTER TABLEするのではなく、マイグレーション(migration)で管理して、
- いつ
- 誰が
- どの環境に
- どんな変更を
適用したかを追跡できる状態にするのが安全です。EF Coreのマイグレーションに限らず、Flyway/LiquibaseなどのDBマイグレーションツールでも構いません。大事なのは、スキーマを「変更してよいもの」ではなく「管理されるべき契約」に戻すことです。
現場でのおすすめ判断フロー(迷ったときの基準)
- 追加項目が“表示・保存中心”で、複雑な検索が少ない → JSON列が第一候補
- 追加項目で検索・権限・履歴が必要 → 項目定義+値テーブル(EAV寄り)
- 追加項目が本質的に固定(いつか必ず全顧客で使う) → 列追加+マイグレーションで管理
- 今すぐは設計変更できないが、増えた列も返したい → 読み取りだけADO.NETに寄せて凌ぐ
まとめ:EF Coreで「動的に増える列」を自然に扱うのは難しい
EF Coreは、固定スキーマを強い型に落とし込み、保守性と安全性を上げるためのORMです。動的に列が増える設計では、追加列を型付きで扱う“入れ物”がないため、モデル変更なしで完結させるのは現実的ではありません。
短期的には、DbContextの接続(必要ならトランザクションも)を使って DbDataReader で動的に回収し、APIでは「固定+extras(辞書)」の形で返すのが堅実です。中長期的には、JSON列や項目定義テーブルなど、可変データに向いたDB設計へ寄せると、運用コストとトラブルが大きく減ります。

コメント