C#でデータベースを扱うとき、「生のSQLは書きたくないけど、Entity Framework ほど重装備にはしたくない」と感じることはないでしょうか。そんなときにちょうどいいのが、SQLをC#のメソッドチェーンで組み立てられるクエリビルダー SqlKata です。
この記事では、SqlKata のインストールから、SELECT・WHERE・JOIN・INSERT / UPDATE / DELETE、Dapper と組み合わせた実行、データベースごとの方言の切り替えまで、実際に動くコードで解説します。
- SqlKata とは何か(ORM との違い・対応DB)
- NuGet でのインストールと最小構成
- SELECT / WHERE / JOIN / 集計 / ページネーション の書き方
- INSERT / UPDATE / DELETE と、値の自動パラメータ化(SQLインジェクション対策)
- Dapper・EF Core との使い分けと、ハマりどころ
SqlKata
SQLを書いてやってもいいのですが、SQLインジェクションへの対応など、やらないといけないこともあるし、DBが変わると微妙に方言のようにSQLも変わります。
よりプログラムのようにSQLを書きたいし、インピーダンスミスマッチを防ぎたいということで「SqlKata」が使いやすくていいかなと思います。
対応するデータベース
SqlServer、MySql、PostgreSql、Oracle、SQLite、Firebird をサポートしています。Dapperでつないで、SqlKataで書くことも可能です。
SELECT文
new Query("Users").Select("Id", "UserName", "Address", "tel as Telephone");これをSQLにすると
select Id, UserName, Address, Telephone From Usersサブクエリの場合
var countQuery = new Query("Comments").WhereColumns("Comments.UserId ", "=", "Users.Id").AsCount();
var query = new Query("Users").Select("Id").Select(countQuery, "CommentsCount");SELECT Id, (SELECT COUNT(*) AS count FROM Comments WHERE Comments.UserId = Users.Id) AS [CommentsCount] FROM UsersJOINする場合
new Query("Users")
.Join("Profiles", "Profiles.UserId", "Users.Id")
.Select(
"Users.Id",
"Users.Name",
"Users.LastName",
"Profiles.GithubUrl",
"Profiles.Website",
"Profiles.Stars"
)中括弧を使ってシンプルにかけるようになります
new Query("Users")
.Join("Profiles", "Profiles.UserId", "Users.Id")
.Select(
"Users.{Id, Name, LastName}",
"Profiles.{GithubUrl, Website, Stars}"SELECT
Users.Id,
Users.Name,
Users.LastName,
Profiles.GithubUrl,
Profiles.Website,
Profiles.Stars
FROM
Users
INNER JOIN Profiles ON Profiles.UserId = Users.Id条件文 WHERE
new Query("Users").Where("Id", 10);
new Query("Users").Where("Id", "=", 10);
new Query("Users").WhereFalse("IsDelete").Where("Id", ">", 10);SELECT * FROM Users WHERE IsDelete = 0 AND Id> 10複数の条件の場合
var query = new Query("Users").Where(new {
Birthday = 10,
Age = 2,
IsDelete = false
});SELECT * FROM Users WHERE Birthday=10 AND Age=2 AND IsDelete=Trueインストール(NuGet)
SqlKata のパッケージは、用途で2つに分かれています。SQL文字列を組み立てるだけなら SqlKata だけ、実際にデータベースへ問い合わせて結果を受け取るなら SqlKata.Execution(内部で Dapper を使います)も入れます。
dotnet add package SqlKata
dotnet add package SqlKata.Execution加えて、接続先DBの ADO.NET ドライバを別途入れる必要があります(SqlKata には含まれません)。
| データベース | 追加するドライバ(NuGet) |
|---|---|
| SQL Server | Microsoft.Data.SqlClient |
| MySQL | MySqlConnector(推奨)または MySql.Data |
| PostgreSQL | Npgsql |
| SQLite | Microsoft.Data.Sqlite |
接続せずにSQL文字列だけ作る(Compiler)
データベースに実行せず、生成されるSQLとパラメータだけ見たいときは、Query を組み立てて Compiler の Compile() に渡します。動的SQLのテストや、SQLだけ使いたい場面で便利です。
using SqlKata;
using SqlKata.Compilers;
var compiler = new SqlServerCompiler();
var query = new Query("Users")
.Where("IsActive", true)
.OrderBy("Name");
SqlResult result = compiler.Compile(query);
Console.WriteLine(result.Sql);
// SELECT * FROM [Users] WHERE [IsActive] = cast(1 as bit) ORDER BY [Name]
// 値は自動でパラメータ化される
foreach (var b in result.NamedBindings)
Console.WriteLine(b);Compiler は接続先DBに合わせて差し替えます。SqlServerCompiler / MySqlCompiler / PostgresCompiler(※PostgreSql ではなく Postgres)/ SqliteCompiler / OracleCompiler / FirebirdCompiler が用意されています。組み立てのコードは共通のまま、Compiler を替えるだけで TOP と LIMIT のような方言が切り替わるのが便利なところです。
QueryFactory + Dapper で実行する
実際にDBへ投げるときは、接続と Compiler を渡して QueryFactory を作ります。取得結果は Dapper がマッピングしてくれます。
using Microsoft.Data.SqlClient;
using SqlKata.Compilers;
using SqlKata.Execution;
using var connection = new SqlConnection(connectionString);
using var db = new QueryFactory(connection, new SqlServerCompiler());
// 全件を dynamic で取得
var users = db.Query("Users").Where("IsActive", true).Get();
// DTO にマッピングして取得
var typed = db.Query("Users").Get<UserDto>();
// 1件だけ
var user = db.Query("Users").Where("Id", 1).First<UserDto>();Get() は複数行、First() は先頭1行。Get<T>() のように型を渡すと、その型に流し込んで返してくれます(マッピングは Dapper 任せ)。
JOIN
var posts = db.Query("Posts")
.Join("Authors", "Authors.Id", "Posts.AuthorId")
.Select("Posts.Title", "Authors.Name as AuthorName")
.Get();LeftJoin / RightJoin / CrossJoin も同じ書き方です。サブクエリを結合するときは .As("エイリアス") を必ず付けます(付け忘れると意図しないSQLになります)。
並び替え・件数の制限(OrderBy / Limit / Offset)
var latest = db.Query("Posts")
.OrderByDesc("CreatedAt")
.Limit(10)
.Offset(20)
.Get();OrderBy(昇順)/ OrderByDesc(降順)、Limit と Offset でページングの基本ができます。
集計・グループ化(GroupBy / Having)
var counts = db.Query("Comments")
.Select("PostId")
.SelectRaw("count(1) as Count")
.GroupBy("PostId")
.Having("Count", ">", 5)
.Get();ページネーション(Paginate)
Laravel 由来の Paginate があり、総件数や総ページ数もまとめて返ります。
// page: 1始まりのページ番号, perPage: 1ページの件数
var result = db.Query("Posts")
.OrderByDesc("CreatedAt")
.Paginate<PostDto>(page: 1, perPage: 20);
foreach (var post in result.List) // IEnumerable<T>
Console.WriteLine(post.Title);
Console.WriteLine(result.Count); // 全件数
Console.WriteLine(result.TotalPages); // 総ページ数
var next = result.Next(); // 次ページを取得INSERT / UPDATE / DELETE
// INSERT(1行)+ 自動採番IDを取得
int newId = db.Query("Books").InsertGetId<int>(new
{
Title = "Toyota Kata",
Author = "Mike Rother",
CreatedAt = DateTime.UtcNow
});
// INSERT(複数行)
var cols = new[] { "Name", "Price" };
var data = new[]
{
new object[] { "A", 1000 },
new object[] { "B", 2000 },
};
int inserted = db.Query("Products").Insert(cols, data);
// UPDATE(影響行数を返す)
int updated = db.Query("Posts").Where("Id", 1).Update(new { AuthorId = 10 });
// DELETE(影響行数を返す)
int deleted = db.Query("Posts").Where("Id", 1).Delete();注意点が2つ。Where を書かない UPDATE / DELETE は全件に効きます(SqlKata は警告してくれません)。また INSERT / UPDATE / DELETE では Join・OrderBy・GroupBy・Limit などは無視されます。付けても効かないので、効かない理由をここで知っておくとハマりません。
値は自動でパラメータ化される(SQLインジェクション対策)
Where などに渡した値は、SQL文字列に直接埋め込まれず、自動でバインドパラメータになります。ふつうに使っていれば SQLインジェクション対策が最初から効く、というのが SqlKata の安心なところです。
new Query("Posts").Where("Title", userInput);
// SELECT * FROM [Posts] WHERE [Title] = @p0 (userInput は値としてバインドされる)生SQL(Raw)を混ぜるときの注意
メソッドで表現しづらい部分は WhereRaw / SelectRaw などで生SQLを差し込めます。ただし値を入れるときは文字列連結ではなく、? プレースホルダを使ってください。連結すると、その部分だけインジェクション対策の穴になります。
// OK:? がパラメータとしてバインドされる
new Query("Posts").WhereRaw("lower(Title) = ?", "sql");
// NG:値を直接埋め込むとインジェクションのリスク
// new Query("Posts").WhereRaw($"lower(Title) = '{userInput}'");トランザクション
複数の更新をまとめて成功/失敗させたいときですが、SqlKata には専用の Transaction メソッドはありません。各実行メソッドが任意の IDbTransaction を受け取れるので、ADO.NET の作法でトランザクションを作り、各呼び出しに渡します。
using var connection = new SqlConnection(connectionString);
connection.Open();
using var db = new QueryFactory(connection, new SqlServerCompiler());
using var tx = connection.BeginTransaction();
try
{
db.Query("Accounts").Where("Id", 1).Update(new { Balance = 900 }, tx);
db.Query("Accounts").Where("Id", 2).Update(new { Balance = 1100 }, tx);
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}Dapper・EF Core との使い分け
SqlKata はあくまで「SQLを組み立てるビルダー」で、実行は Dapper に委ねています。3つを大まかに並べると次のようになります。
| 観点 | SqlKata | Dapper 単体 | EF Core |
|---|---|---|---|
| 立ち位置 | クエリビルダー | マイクロORM(生SQL) | フルスタックORM |
| 動的にWHERE/JOINを足す | 得意 | 文字列連結で複雑化しがち | 可能だがやや煩雑 |
| SQLへの近さ | 近い | そのもの | 遠い(生成SQLは隠れる) |
| 型安全(列名タイポ) | 弱い(実行時に判明) | 弱い | 強い |
| マイグレーション・変更追跡 | なし | なし | あり |
| パフォーマンス | 軽い(Dapper相当) | 最速級 | 相対的に重い |
ざっくり言うと、検索条件が可変で WHERE や JOIN を動的に組み立てたい管理画面・検索APIには SqlKata が強いです。クエリが固定でシンプルなら Dapper 単体、モデル中心でマイグレーションや変更追跡が欲しいなら EF Core、という住み分けになります。列名を文字列で書くのでコンパイル時の型安全性は EF Core に劣る点は正直なデメリットです。
よくあるハマりどころ
- Compiler の選択ミス:接続先DBと Compiler が食い違うと、TOP と LIMIT の違いなどで構文エラーになる
- SqlKata.Execution の入れ忘れ:これが無いと Get() / Insert() などの実行メソッドが使えない
- INSERT/UPDATE/DELETE で Join・OrderBy 等が無視される:付けても効かない
- Raw への値の直書き:? プレースホルダを使わないとインジェクションの穴になる
- サブクエリ JOIN のエイリアス忘れ:.As() を付け忘れると意図しないSQLになる
- トランザクション引数の渡し忘れ:各メソッドに tx を渡し忘れるとトランザクション外で実行される
よくある質問
SqlKata は ORM ですか?
いいえ。ORM ではなく「SQLクエリビルダー」です。モデルとテーブルのマッピングやマイグレーション、変更追跡といった EF Core 的な機能は持たず、SQL文字列とパラメータを組み立てることに特化しています。
Dapper は必要ですか?
SQL文字列を作るだけなら SqlKata だけでOKです。実際にDBへ実行して結果を受け取るときは SqlKata.Execution を入れます。これが内部で Dapper を使うため、実行するなら Dapper が関わってきます。
SQLインジェクションは大丈夫ですか?
Where などに渡した値は自動でパラメータ化されるので、通常の使い方なら対策が効いています。例外は Raw 系メソッドで、ここだけは自分で ? プレースホルダを使う必要があります。
まとめ
SqlKata は、C# の中でSQLをメソッドチェーンとして安全に組み立てられるクエリビルダーです。値の自動パラメータ化でインジェクションに強く、Compiler を替えるだけで複数のDB方言に対応でき、動的にクエリを組み立てたい場面でとても書きやすくなります。生SQLの手書きと EF Core の中間がほしいときの、有力な選択肢です。
より詳しい仕様は 公式ドキュメント と GitHub リポジトリ が参考になります。

コメント