Stories

Detail Return Return

C#.NET FluentSqlKata 全面解析:基於鏈式語法的動態 SQL 構建 - Stories Detail

簡介

在使用 SqlKata 構建 SQL 時,雖然其鏈式 API 強大靈活,但仍需通過字符串或匿名字段進行表與列的映射,缺乏對實體類型和字段的靜態檢查。FluentSqlKata 基於 SqlKata,提供了一套基於表達式的強類型查詢構建能力,能夠:

  • 通過 Lambda 表達式指定實體與列,更安全、可重構
  • 保留 SqlKata 的所有特性與多數據庫編譯器支持
  • 在運行時動態構造 ORDER BY 時,支持別名排序等高級功能

支持環境與安裝

  • 目標框架:.NET Standard 2.0,兼容 .NET Framework 4.6.1+.NET Core 2.0+.NET 5/6/7/8/9/10
  • 安裝命令:
dotnet add package FluentSqlKata --version 1.1.7

核心功能

強類型查詢

使用 FluentQuery.Query() 開始,From(() => alias) 指定實體別名,後續所有列、條件、排序均通過表達式指定:

var query = FluentQuery.Query()
    .From(() => myCust)
    .Select(() => result.CustomerId, () => myCust.Id)
    .Where(q => q.Where(() => myCust.Name, "John"))
    .OrderByColumn(() => myCust.Name);

動態別名排序

對於 SelectRaw 或計算列,支持 OrderByAlias(() => aliasProp),自動以別名生成 ORDER BY

.SelectRaw(() => model.Name, "ISNULL({0}, 'Unknown')", () => myCust.Name)
.OrderByAlias(() => model.Name);

聯表與聚合

支持多種 Join 重載,包括表達式構造的 ON 條件;GroupBySelectCount 等聚合 APISqlKata 保持一致:

.Join(() => myCont, () => myCont.CustomerId, () => myCust.Id)
.SelectCount(() => result.Count, () => myCont.Id)
.GroupBy(() => myCust.Name);

API 詳解

方法 説明
FluentQuery.Query() 創建一個新的強類型查詢構建器
.From(() => alias) 指定根表實體及其別名
.Select(exprAlias, exprColumn) 添加列映射,並指定結果字段
.Where(Func<Query, Query>) 通過內部 SqlKata API 構造過濾條件
.Join(...) 多種重載,可按表達式或列映射方式構造聯表
.SelectRaw(alias, fmt, expr…) 原生 SQL 片段映射,同時支持別名排序
.SelectCount(alias, expr) 生成 COUNT(...) AS alias 聚合列
.GroupBy(expr…) 指定分組字段
.OrderByColumn(expr) 按指定列排序
.OrderByAlias(expr) 按先前定義的列別名排序
.Compile(compiler) 使用指定編譯器生成最終 SQL 與參數

用法示例

public class Customer
{
    public string Id { get; set; }
    public string Name { get; set; }
    public DateTime LastUpdated { get; set; }
}

基本查詢

使用 FluentQuery 構建類型安全的 SELECT 查詢:

using FluentSqlKata;
using SqlKata;
using SqlKata.Execution;
using System.Data.SqlClient;

public async Task Main()
{
    using var connection = new SqlConnection("Server=localhost;Database=testdb;Trusted_Connection=True;");
    var compiler = new SqlServerCompiler();
    var db = new QueryFactory(connection, compiler);

    Customer myCust = null;
    (string CustomerId, string CustomerName) result = default;

    var query = FluentQuery.Query()
        .From(() => myCust)
        .Select(() => result.CustomerId, () => myCust.Id)
        .Select(() => result.CustomerName, () => myCust.Name);

    var customers = await db.FromQuery(query).GetAsync<(string, string)>();

    foreach (var customer in customers)
    {
        Console.WriteLine($"ID: {customer.Item1}, Name: {customer.Item2}");
    }
}

條件查詢

使用類型安全的 Where 方法:

Customer myCust = null;
(string CustomerId, string CustomerName) result = default;

var query = FluentQuery.Query()
    .From(() => myCust)
    .Select(() => result.CustomerId, () => myCust.Id)
    .Select(() => result.CustomerName, () => myCust.Name)
    .Where(q => q.Where(() => myCust.Name, "John")
                .OrWhereContains(() => myCust.Name, "oh"));

var query_str = new SqlServerCompiler().Compile(query).ToString();
Console.WriteLine(query_str);

連接(JOIN)

類型安全的 JOIN 查詢:

Contact myCont = null;
Customer myCust = null;
(string FirstName, string LastName, string CustomerId, string CustomerName) result = default;

var query = FluentQuery.Query()
    .From(() => myCont)
    .Join(() => myCust, () => myCust.Id, () => myCont.CustomerId)
    .Select(() => result.FirstName, () => myCont.FirstName)
    .Select(() => result.LastName, () => myCont.LastName)
    .Select(() => result.CustomerId, () => myCont.CustomerId)
    .Select(() => result.CustomerName, () => myCust.Name);

var query_str = new SqlServerCompiler().Compile(query).ToString();
Console.WriteLine(query_str);

優缺點

優點

  • 類型安全:通過表達式引用表和列,編譯器捕獲拼寫錯誤。
  • 靈活性:繼承 SqlKata 的複雜查詢支持(子查詢、JOINCTE)。
  • 高性能:結合 Dapper,性能接近原生 ADO.NET
  • 跨數據庫支持:通過 SqlKata 的編譯器適配多種數據庫。

缺點

  • 學習曲線:表達式語法(如 () => myCust.Name)比 SqlKata 的字符串語法複雜。
  • 部分功能依賴字符串:插入、更新和刪除仍使用 SqlKata 的字符串-based API
  • 文檔有限:FluentSqlKata 的文檔較少,需參考 SqlKata 文檔和 GitHub 示例。
  • 依賴 SqlKata:增加了依賴項,需熟悉 SqlKata 的核心概念。

示例項目

using Microsoft.AspNetCore.Mvc;
using FluentSqlKata;
using SqlKata;
using SqlKata.Execution;
using System.Threading.Tasks;

public class Customer
{
    public string Id { get; set; }
    public string Name { get; set; }
    public DateTime LastUpdated { get; set; }
}

[ApiController]
[Route("api/customers")]
public class CustomersController : ControllerBase
{
    private readonly QueryFactory _db;

    public CustomersController(QueryFactory db)
    {
        _db = db;
    }

    [HttpGet]
    public async Task<IActionResult> Search([FromQuery] string searchText)
    {
        Customer myCust = null;
        (string CustomerId, string CustomerName) result = default;

        var query = FluentQuery.Query()
            .From(() => myCust)
            .Select(() => result.CustomerId, () => myCust.Id)
            .Select(() => result.CustomerName, () => myCust.Name);

        if (!string.IsNullOrEmpty(searchText))
        {
            query.Where(q => q.WhereContains(() => myCust.Name, searchText));
        }

        var customers = await _db.FromQuery(query).GetAsync<(string, string)>();
        return Ok(customers);
    }
}

啓動配置:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddTransient<QueryFactory>(sp =>
        {
            var connection = new SqlConnection("Server=localhost;Database=testdb;Trusted_Connection=True;");
            var compiler = new SqlServerCompiler();
            return new QueryFactory(connection, compiler);
        });
    }
}

Add a new Comments

Some HTML is okay.