SqlKata入門|C#で使えるSQLクエリビルダーの使い方【SELECT/JOIN/実行まで】

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 Users

JOINする場合

new Query("Users")
    .Join("Profiles", "Profiles.UserId", "Users.Id")
    .Select(
        "Users.Id",
        "Users.Name",
        "Users.LastName",
        "Profiles.GithubUrl",
        "Profiles.Website",
        "Profiles.Stars"
    )

中括弧を使ってシンプルにかけるようになります

バージョン1.1.2以降では、括弧展開機能を使用して複数の列を同時に選択できるようになりました。

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 ServerMicrosoft.Data.SqlClient
MySQLMySqlConnector(推奨)または MySql.Data
PostgreSQLNpgsql
SQLiteMicrosoft.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 を替えるだけで TOPLIMIT のような方言が切り替わるのが便利なところです。

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(降順)、LimitOffset でページングの基本ができます。

集計・グループ化(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つを大まかに並べると次のようになります。

観点SqlKataDapper 単体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 リポジトリ が参考になります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コスト最小で制作をしたいという意識強め(笑)

コメント

コメントする

目次