目次
概要
Entity Framework Core (EF Core) を使用しつつ、パフォーマンス調整や複雑なクエリ記述のために、LINQではなく直接SQL文を実行する方法です。 データの更新系には ExecuteSqlAsync、取得系には FromSql を使用し、C#の文字列補間($)を使うことでSQLインジェクション対策も自動的に行います。
仕様(入出力)
- 入力: ユーザーID、メッセージ本文、日時。
- 出力: SQLの
INSERT文でデータを登録し、SELECT文で検索した結果をコンソールに表示。 - 前提: .NET 6.0以上。Raw SQLの実行にはリレーショナルデータベースが必要です(本コードでは軽量なSQLiteを使用)。
基本の使い方
EF Core 7.0以降では、ExecuteSqlAsync と FromSql が推奨されます。文字列補間 $ を使うことでパラメータ化されます。
int userId = 1;
string name = "gushwell";
// 更新系(INSERT, UPDATE, DELETE)
// {0}, {1} のようなパラメータとして処理されるため安全
await context.Database.ExecuteSqlAsync($"UPDATE Users SET Name = {name} WHERE Id = {userId}");
// 取得系(SELECT)
var users = await context.Users
.FromSql($"SELECT * FROM Users WHERE Id = {userId}")
.ToListAsync();
コード全文
このコードは SQLite を使用して動作します。 NuGetパッケージ Microsoft.EntityFrameworkCore.Sqlite が必要です。
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
public class Program
{
public static async Task Main()
{
// SQLiteデータベースを使用するためのオプション設定
var options = new DbContextOptionsBuilder<BlogDbContext>()
.UseSqlite("Data Source=blog.db")
.Options;
// --- 1. データベースの初期化 ---
using (var context = new BlogDbContext(options))
{
// データベースファイルが存在しなければ作成(テーブル作成)
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
}
// --- 2. 生のSQLを使った操作 ---
using (var context = new BlogDbContext(options))
{
var worker = new SqlWorker(context);
await worker.RunAsync();
}
}
}
// 業務ロジッククラス
public class SqlWorker
{
private readonly BlogDbContext _context;
public SqlWorker(BlogDbContext context)
{
_context = context;
}
public async Task RunAsync()
{
var message = "明日の勉強会参加させていただきます。";
var sentTime = new DateTime(2025, 11, 22, 12, 5, 7); // DateTime型を使用
var userId = 101;
Console.WriteLine("--- 1. INSERT (ExecuteSqlAsync) ---");
// ExecuteSqlAsync を使用して INSERT 文を実行
// 文字列補間 ($"...") を使うことで、変数は自動的にSQLパラメータ(@p0, @p1...)に変換され安全です
int affectedRows = await _context.Database.ExecuteSqlAsync($@"
INSERT INTO Posts (UserId, Message, SentTime)
VALUES ({userId}, {message}, {sentTime})
");
Console.WriteLine($"{affectedRows} 件のデータを挿入しました。");
Console.WriteLine("--- 2. SELECT (FromSql) ---");
// FromSql を使用して SELECT 文を実行
// エンティティとして取得されるため、ChangeTracker(変更追跡)も有効です
var posts = await _context.Posts
.FromSql($"SELECT * FROM Posts WHERE UserId = {userId}")
.ToListAsync();
foreach (var post in posts)
{
Console.WriteLine($"[ID:{post.PostId}] User:{post.UserId} / Time:{post.SentTime}");
Console.WriteLine($" Message: {post.Message}");
}
}
}
// エンティティ定義
public class Post
{
[Key]
public int PostId { get; set; }
public int UserId { get; set; }
public string Message { get; set; } = string.Empty;
public DateTime SentTime { get; set; }
}
// DbContext定義
public class BlogDbContext : DbContext
{
public BlogDbContext(DbContextOptions<BlogDbContext> options)
: base(options) { }
public DbSet<Post> Posts => Set<Post>();
}
カスタムポイント
- ストアドプロシージャの実行:
FromSql($"EXEC GetPosts {userId}")のように記述することで、ストアドプロシージャの結果をエンティティにマッピングできます。 - スカラー値の取得: テーブル全体ではなく「件数」や「IDリスト」だけを取得したい場合は、
context.Database.SqlQuery<int>($"SELECT Count(*) ...")(EF Core 8以降) が利用できます。 - 非追跡クエリ: データ表示のみで更新予定がない場合は、
.FromSql(...).AsNoTracking()を組み合わせることでパフォーマンスが向上します。
注意点
- SQLインジェクション:
$(FormattableString) を使用していれば安全ですが、文字列結合("SELECT * FROM Users WHERE Name = '" + name + "'")をしてFromSqlRawに渡すのは絶対禁止です。必ずパラメータ化クエリ(補間文字列)を使用してください。 - カラム名の不一致:
SELECT *を使用する場合、データベースのカラム名とエンティティのプロパティ名が一致している必要があります。不一致がある場合はSELECT user_id AS UserId ...のようにエイリアスを使うか、エンティティ設定でマッピングを行ってください。 - InMemoryプロバイダの制限:
Microsoft.EntityFrameworkCore.InMemoryは生のSQL実行をサポートしていません。テスト時も SQLite (In-Memory mode) などのリレーショナルDBプロバイダを使用する必要があります。
応用
プリミティブ型(スカラー)のリスト取得 (EF Core 8.0+)
エンティティ全体ではなく、特定のカラムだけを抽出する場合です。
// IDのリストだけを取得したい場合
List<int> ids = await context.Database
.SqlQuery<int>($"SELECT PostId FROM Posts WHERE UserId = {userId}")
.ToListAsync();
まとめ
- 更新系には
ExecuteSqlAsync、取得系にはFromSqlを使用します。 - 文字列補間
$を使用することで、簡潔かつ安全(SQLインジェクション対策済み)に値を渡せます。 - LINQで表現しきれないCTE(共通テーブル式)やWindow関数を使用する場合に有効です。
