Skip to content

贡献者指南

欢迎来到 SmartSql 贡献者指南。本文档将带你从零上下文成为一个高效的贡献者。SmartSql 是一个受 MyBatis 启发的 .NET ORM 库,使用 XML 来管理 SQL 语句。如果你有 .NET 经验但从未接触过 SmartSql,本指南就是为你准备的。


第一部分:.NET ORM 基础

什么是 ORM?

对象关系映射器(ORM)弥合了 C# 中面向对象代码与数据库中关系数据之间的差距。你不再需要编写内联的原始 SQL 字符串,而是声明映射关系,让 ORM 处理参数绑定、结果反序列化和连接管理。

.NET ORM 生态

.NET 生态中最常见的三个选择:

特性Entity Framework CoreDapperSmartSql
SQL 管理LINQ / Fluent API内联字符串XML 文件
映射策略Code-first 或 DB-first手动XML 声明式
变更跟踪可选(PropertyChangedTrack
迁移支持内置
中间件管道
缓存二级缓存提供程序内置 LRU/FIFO/Redis
读写分离通过库手动内置加权路由
动态仓库是(IL emit 代理)
批量插入通过扩展通过 SqlBulkCopy内置按提供程序
学习曲线中高
性能不一非常快快(最小开销)

为什么使用 MyBatis 风格的 XML?

SmartSql 的核心设计选择是 MyBatis 风格的 XML SQL 管理。SmartSql 不是从 LINQ 表达式生成 SQL(EF Core)或将 SQL 作为字符串字面量嵌入(Dapper),而是将 SQL 外部化到 XML 文件中。这有几个实际好处:

  • SQL 可见性:每个查询都在 XML 文件中,便于在不重新编译的情况下审查、优化和调整 SQL。
  • 动态 SQL:XML 标签(<Where><IsNotEmpty><Switch><For><Include>)让你无需在 C# 代码中进行字符串拼接即可构建条件查询。
  • 对 DBA 友好:数据库管理员可以在不浏览 C# 源代码的情况下阅读和修改 SQL。
  • 关注点分离:应用程序逻辑和 SQL 在不同文件中,遵循分离原则。

MyBatis 对照

如果你在 Java 中使用过 MyBatis 或 MyBatis-Plus,你会认出很多概念:

MyBatis 概念SmartSql 对应
mybatis-config.xmlSmartSqlMapConfig.xml
Mapper XML(<mapper>SmartSqlMap XML(<SmartSqlMap>
<select><insert>带有 StatementType<Statement>
<if><where><choose><IsNotEmpty><Where><Switch>
#{param}@Param
ResultMapResultMap
TypeHandlerTypeHandler

第二部分:SmartSql 架构

高层架构

mermaid
graph TB
    subgraph Application["Application Layer"]
        style Application fill:#161b22,stroke:#30363d,color:#e6edf3
        Code["Application Code"]
        DyRepo["Dynamic Repository<br>(DyRepository)"]
    end

    subgraph SmartSqlCore["SmartSql Core"]
        style SmartSqlCore fill:#161b22,stroke:#30363d,color:#e6edf3
        ISqlMapper["ISqlMapper / SqlMapper"]
        Builder["SmartSqlBuilder"]
        Config["SmartSqlConfig"]
        Pipeline["Middleware Pipeline"]
    end

    subgraph PipelineStages["Pipeline Stages"]
        style PipelineStages fill:#161b22,stroke:#30363d,color:#e6edf3
        Init["InitializerMiddleware<br>Order: 0"]
        Prep["PrepareStatementMiddleware<br>Order: 100"]
        Cache["CachingMiddleware<br>Order: 200"]
        Tx["TransactionMiddleware<br>Order: 300"]
        DS["DataSourceFilterMiddleware<br>Order: 400"]
        Cmd["CommandExecuterMiddleware<br>Order: 500"]
        Result["ResultHandlerMiddleware<br>Order: 600"]
    end

    subgraph Extensions["Extension Layer"]
        style Extensions fill:#161b22,stroke:#30363d,color:#e6edf3
        DI["DIExtension"]
        AOP["AOP Transactions"]
        Redis["Cache.Redis"]
        Bulk["Bulk Insert"]
        InvokeSync["InvokeSync (Kafka/RabbitMQ)"]
    end

    subgraph DataSources["Data Sources"]
        style DataSources fill:#161b22,stroke:#30363d,color:#e6edf3
        WriteDB["Write DB"]
        ReadDB1["Read DB 1<br>Weight: 100"]
        ReadDB2["Read DB 2<br>Weight: 100"]
    end

    Code --> ISqlMapper
    DyRepo --> ISqlMapper
    ISqlMapper --> Pipeline
    Builder --> Config
    Config --> Pipeline

    Pipeline --> Init
    Init --> Prep
    Prep --> Cache
    Cache --> Tx
    Tx --> DS
    DS --> Cmd
    Cmd --> Result

    DS --> WriteDB
    DS --> ReadDB1
    DS --> ReadDB2

    style ISqlMapper fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Builder fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Config fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Pipeline fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Init fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Prep fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Cache fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Tx fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DS fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Cmd fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Result fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Code fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DyRepo fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style WriteDB fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ReadDB1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ReadDB2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style DI fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style AOP fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Redis fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Bulk fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style InvokeSync fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

中间件管道

SmartSql 通过链表中间件管道处理每个 SQL 操作。每个中间件实现 IMiddleware,并有一个 Order 属性来决定其在链中的位置。

mermaid
sequenceDiagram
autonumber
    participant App as Application
    participant SM as SqlMapper
    participant Init as Initializer<br>Order:0
    participant Prep as PrepareStatement<br>Order:100
    participant Cache as Caching<br>Order:200
    participant Tx as Transaction<br>Order:300
    participant DS as DataSourceFilter<br>Order:400
    participant Cmd as CommandExecuter<br>Order:500
    participant RH as ResultHandler<br>Order:600
    participant DB as Database

    App->>SM: Query(request)
    SM->>SM: Open session if needed
    SM->>Init: Invoke
    Init->>Init: Resolve Statement, DataSourceChoice, Cache, ResultMap
    Init->>Prep: InvokeNext
    Prep->>Prep: Build SQL from XML tags, create DbParameters
    Prep->>Cache: InvokeNext
    Cache->>Cache: Check cache hit
    alt Cache Hit
        Cache-->>App: Return cached result
    else Cache Miss or Write
        Cache->>Tx: InvokeNext
        Tx->>Tx: Wrap in transaction if needed
        Tx->>DS: InvokeNext
        DS->>DS: Elect read or write datasource
        DS->>Cmd: InvokeNext
        Cmd->>DB: Execute SQL
        DB-->>Cmd: IDataReader
        Cmd->>RH: InvokeNext
        RH->>RH: Deserialize result
        RH->>Cache: (back in CachingMiddleware) Store in cache
        RH-->>App: Return result
    end

中间件顺序参考

顺序中间件职责
0InitializerMiddleware从配置解析 Statement,设置 DataSourceChoiceCacheResultMapAutoConverter
100PrepareStatementMiddleware从 XML 标签构建最终 SQL,使用类型处理器创建 DbParameter 对象
200CachingMiddleware读取时检查缓存,执行后存储结果(禁用缓存时跳过)
300TransactionMiddlewareRequest.Transaction 被设置时将执行包装在事务中
400DataSourceFilterMiddleware使用加权选择选择写或读数据源
500CommandExecuterMiddleware对数据库执行 DbCommand
600ResultHandlerMiddleware通过反序列化器链将 IDataReader 反序列化为类型化结果

每个中间件可以通过不调用 InvokeNext 来短路管道,这正是 CachingMiddleware 处理缓存命中时的方式。

配置系统

mermaid
graph LR
    subgraph ConfigSources["Configuration Sources"]
        style ConfigSources fill:#161b22,stroke:#30363d,color:#e6edf3
        XML["SmartSqlMapConfig.xml"]
        Native["NativeConfigBuilder"]
        Options["Options Pattern<br>(appsettings.json)"]
    end

    subgraph ConfigBuilder["Config Builder Chain"]
        style ConfigBuilder fill:#161b22,stroke:#30363d,color:#e6edf3
        Root["RootConfigBuilder"]
        CfgBuilder["IConfigBuilder impl"]
        CUD["CUDConfigBuilder<br>(auto-generate CRUD)"]
    end

    subgraph FinalConfig["SmartSqlConfig"]
        style FinalConfig fill:#161b22,stroke:#30363d,color:#e6edf3
        Database["Database<br>(Write + Read sources)"]
        SqlMaps["SqlMaps<br>(XML statement definitions)"]
        TypeHandlers["TypeHandlerFactory"]
        TagBuilders["TagBuilderFactory"]
        CacheMgr["CacheManager"]
        IdGens["IdGenerators<br>(SnowflakeId, etc.)"]
        Pipeline["IMiddleware Pipeline"]
        Filters["FilterCollection"]
    end

    XML --> CfgBuilder
    Native --> CfgBuilder
    Options --> CfgBuilder
    Root --> CfgBuilder
    CfgBuilder --> CUD
    CUD --> FinalConfig

    style XML fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Native fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Options fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Root fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CfgBuilder fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CUD fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Database fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SqlMaps fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style TypeHandlers fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style TagBuilders fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CacheMgr fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IdGens fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Pipeline fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Filters fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

SmartSqlBuilder 是流式入口点。它链接配置源、构建 SmartSqlConfig、组装中间件管道,并将所有内容注册到 SmartSqlContainer 中。

XML SQL 管理详解

每个 SQL 语句都位于一个 SmartSqlMap XML 文件中。每个文件声明一个 Scope(命名空间),并包含 <Statements><Caches><ResultMaps> 和其他定义。

语句结构

xml
<SmartSqlMap Scope="User" xmlns="http://SmartSql.net/schemas/SmartSqlMap.xsd">
    <Caches>
        <Cache Id="UserCache" Type="Lru">
            <FlushOnExecute Statement="Update"/>
            <FlushOnExecute Statement="Delete"/>
        </Cache>
    </Caches>
    <Statements>
        <Statement Id="Query">
            SELECT T.* FROM T_User T
            <Where>
                <IsNotEmpty Prepend="And" Property="UserName">
                    T.UserName = @UserName
                </IsNotEmpty>
            </Where>
        </Statement>
    </Statements>
</SmartSqlMap>

语句通过其完整 ID 引用:Scope.Id(例如 User.Query)。

XML 动态标签

SmartSql 提供了一组丰富的 XML 标签用于条件 SQL 构建。这些是动态查询的构建块:

标签用途示例
<Where>将子元素包装在 WHERE 中,去掉前导 AND/OR<Where><IsNotEmpty Prepend="And" ...>
<Set>将子元素包装在 SET 中,去掉前导逗号<Set><IsProperty Prepend="," ...>
<IsNotEmpty>如果属性不为 null/空则包含子 SQL<IsNotEmpty Property="Name">Name=@Name</IsNotEmpty>
<IsEmpty>如果属性为 null 或空则包含子 SQLIsNotEmpty 的反义
<IsEqual>将属性值与 CompareValue 比较<IsEqual Property="Status" CompareValue="1">
<IsNotEqual>如果属性不等于值则包含<IsNotEqual Property="Status" CompareValue="0">
<IsGreaterThan>数字或字符串比较<IsGreaterThan Property="Age" CompareValue="18">
<IsLessThan>数字或字符串比较<IsLessThan Property="Price" CompareValue="100">
<IsProperty>如果属性存在于请求中则包含<IsProperty Property="Status">Status=@Status</IsProperty>
<IsTrue> / <IsFalse>布尔条件<IsTrue Property="IsActive">
<Switch> / <Case>多分支条件<Switch Property="OrderBy"><Case CompareValue="1">ORDER BY Name</Case></Switch>
<For>遍历集合<For Property="Ids" Open="(" Separator="," Close=")">@Id</For>
<Include>引用可重用的 SQL 片段<Include RefId="QueryParams"/>
<Env>特定环境的 SQL<Env Name="Production">LIMIT 1000</Env>
<Range>检查属性值是否在范围内<Range Property="Age" Min="18" Max="65">

标签层次结构

mermaid
graph TD
    subgraph TagHierarchy["Tag Class Hierarchy"]
        style TagHierarchy fill:#161b22,stroke:#30363d,color:#e6edf3
        ITag["ITag<br>(interface)"]
        AbsTag["Tag<br>(abstract base)"]
        Dynamic["Dynamic<br>(container with child evaluation)"]
        Where["Where"]
        SetTag["Set"]
        IsNotEmpty["IsNotEmpty"]
        IsEmpty["IsEmpty"]
        IsEqual["IsEqual"]
        IsProperty["IsProperty"]
        Switch["Switch"]
        ForTag["For"]
        Include["Include"]
        SqlText["SqlText<br>(raw SQL text)"]
    end

    ITag --> AbsTag
    AbsTag --> Dynamic
    AbsTag --> IsNotEmpty
    AbsTag --> IsEmpty
    AbsTag --> IsEqual
    AbsTag --> IsProperty
    Dynamic --> Where
    Dynamic --> SetTag
    Dynamic --> Switch

    style ITag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style AbsTag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Dynamic fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Where fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SetTag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IsNotEmpty fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IsEmpty fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IsEqual fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IsProperty fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Switch fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ForTag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Include fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SqlText fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

读写分离

SmartSql 内置支持读写数据源分离和加权负载均衡。DataSourceFilterMiddleware 使用 InitializerMiddleware 确定的 DataSourceChoice

  • 写语句(INSERT、UPDATE、DELETE)始终发送到写数据源。
  • 读语句(SELECT)发送到读数据源,按权重选择。
  • 显式路由:你可以在语句上指定 ReadDb="ReadDb-2" 来将其固定到特定的读副本。

WeightFilter 使用加权随机选择算法在可用读副本之间进行选择。

缓存架构

SmartSql 支持两级缓存:

  1. 内置内存缓存,具有 LRU 和 FIFO 淘汰策略
  2. Redis 缓存,通过 SmartSql.Cache.Redis 扩展

缓存失效是声明式的 -- 你指定哪些语句刷新缓存:

xml
<Cache Id="UserCache" Type="Lru">
    <FlushOnExecute Statement="Update"/>
    <FlushOnExecute Statement="Delete"/>
</Cache>

CachingMiddleware 拦截读操作并在可用时返回缓存结果。对于写操作,它刷新相关缓存。在事务中缓存会自动绕过。

动态仓库(DyRepository)

SmartSql.DyRepository 扩展使用 IL emit 在运行时生成仓库接口的代理实现。接口方法通过命名约定自动映射到 SQL 语句。

mermaid
sequenceDiagram
autonumber
    participant App as Application
    participant Proxy as Dynamic Proxy<br>(IL-emitted)
    participant SM as ISqlMapper
    participant Pipeline as Middleware Pipeline
    participant DB as Database

    App->>Proxy: IUserRepository.Query(user)
    Proxy->>Proxy: Resolve Scope from interface(e.g., "User")
    Proxy->>Proxy: Resolve StatementId from method name(e.g., "Query")
    Proxy->>Proxy: Build RequestContext with parameters
    Proxy->>SM: Query(requestContext)
    SM->>Pipeline: Invoke pipeline
    Pipeline->>DB: Execute SQL
    DB-->>Pipeline: DataReader
    Pipeline-->>App: IList

泛型仓库接口 IRepository<TEntity, TPrimary> 提供内置 CRUD 操作(Insert、Update、Delete、GetEntity、Query、QueryByPage、GetRecord、IsExist),通过 CUDConfigBuilder 映射到自动生成的 SQL。

反序列化链

当查询结果从数据库返回时,ResultHandlerMiddleware 委托给反序列化工厂,后者按顺序尝试反序列化器:

mermaid
graph LR
    subgraph DeserChain["Deserializer Chain"]
        style DeserChain fill:#161b22,stroke:#30363d,color:#e6edf3
        MR["MultipleResultDeserializer"]
        VT["ValueTupleDeserializer"]
        VType["ValueTypeDeserializer"]
        Dyn["DynamicDeserializer"]
        Entity["EntityDeserializer"]
        Custom["Custom Deserializers"]
    end

    Factory["DeserializerFactory"] --> MR
    MR --> VT
    VT --> VType
    VType --> Dyn
    Dyn --> Entity
    Entity --> Custom

    style MR fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style VT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style VType fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Dyn fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Entity fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Custom fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Factory fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

每个反序列化器会被询问是否能处理目标类型。链的顺序使得更具体的反序列化器优先尝试。末尾的 EntityDeserializer 处理通用 POCO 映射。

扩展项目

项目位置用途
SmartSql.DyRepositorysrc/SmartSql.DyRepository/通过 IL emit 动态生成仓库代理
SmartSql.DIExtensionsrc/SmartSql.DIExtension/ASP.NET Core 依赖注入集成
SmartSql.Optionssrc/SmartSql.Options/appsettings.json 的 Options 模式配置
SmartSql.AOPsrc/SmartSql.AOP/使用 [Transaction] 特性的 AOP 事务支持(AspectCore)
SmartSql.Cache.Redissrc/SmartSql.Cache.Redis/Redis 缓存提供程序
SmartSql.Cache.Syncsrc/SmartSql.Cache.Sync/跨实例缓存同步
SmartSql.TypeHandlersrc/SmartSql.TypeHandler/JSON 和自定义类型处理器
SmartSql.TypeHandler.PostgreSqlsrc/SmartSql.TypeHandler.PostgreSql/PostgreSQL 专用类型处理器
SmartSql.Bulk.SqlServersrc/SmartSql.Bulk.SqlServer/SQL Server 批量插入
SmartSql.Bulk.MsSqlServersrc/SmartSql.Bulk.MsSqlServer/Microsoft.Data.SqlClient 批量插入
SmartSql.Bulk.MySqlsrc/SmartSql.Bulk.MySql/MySQL 批量插入
SmartSql.Bulk.MySqlConnectorsrc/SmartSql.Bulk.MySqlConnector/MySqlConnector 批量插入
SmartSql.Bulk.PostgreSqlsrc/SmartSql.Bulk.PostgreSql/PostgreSQL 批量插入
SmartSql.InvokeSyncsrc/SmartSql.InvokeSync/数据同步核心
SmartSql.InvokeSync.Kafkasrc/SmartSql.InvokeSync.Kafka/基于 Kafka 的同步
SmartSql.InvokeSync.RabbitMQsrc/SmartSql.InvokeSync.RabbitMQ/基于 RabbitMQ 的同步
SmartSql.ScriptTagsrc/SmartSql.ScriptTag/动态 SQL 的脚本标签支持
SmartSql.Extensionssrc/SmartSql.Extensions/通用扩展
SmartSql.Oraclesrc/SmartSql.Oracle/Oracle 数据库提供程序

第三部分:提高效率

开发环境设置

前提条件

  • .NET SDK(目标 netstandard2.0,C# 7.3)
  • IDE:Visual Studio、Rider 或带 C# 扩展的 VS Code
  • MySQL 数据库(用于运行测试)
  • Redis(可选,用于缓存测试)

步骤

  1. 克隆仓库:

    bash
    git clone https://github.com/dotnetcore/SmartSql.git
    cd SmartSql
  2. 还原依赖:

    bash
    dotnet restore SmartSql.sln
  3. 构建整个解决方案:

    bash
    dotnet build SmartSql.sln
  4. 运行单元测试:

    bash
    dotnet test
  5. 运行特定测试项目:

    bash
    dotnet test src/SmartSql.Test.Unit/SmartSql.Test.Unit.csproj
  6. 按名称过滤器运行测试:

    bash
    dotnet test src/SmartSql.Test.Unit/SmartSql.Test.Unit.csproj \
      --filter "FullyQualifiedName~SmartSql.Test.Unit.Tests.CacheTest"

解决方案结构

SmartSql/
├── build/
│   └── version.props          # Version management (currently 4.1.68)
├── doc/                        # Documentation
├── sample/
│   └── SmartSql.Sample.AspNetCore/  # Demo application
├── src/
│   ├── SmartSql/               # Core library (netstandard2.0)
│   ├── SmartSql.DyRepository/  # Dynamic repository proxies
│   ├── SmartSql.DIExtension/   # ASP.NET Core DI integration
│   ├── SmartSql.Options/       # Options pattern config
│   ├── SmartSql.AOP/           # AOP transaction attribute
│   ├── SmartSql.Cache.Redis/   # Redis cache provider
│   ├── SmartSql.Cache.Sync/    # Cache synchronization
│   ├── SmartSql.TypeHandler/   # JSON and custom type handlers
│   ├── SmartSql.Bulk.*/        # Bulk insert providers
│   ├── SmartSql.InvokeSync.*/  # Kafka/RabbitMQ sync
│   ├── SmartSql.ScriptTag/     # Script tag support
│   ├── SmartSql.Extensions/    # General extensions
│   ├── SmartSql.Oracle/        # Oracle provider
│   ├── SmartSql.Test.Unit/     # Unit tests (xUnit)
│   ├── SmartSql.Test.Performance/ # BenchmarkDotNet tests
│   └── SmartSql.Test/          # Integration test helpers
└── SmartSql.sln

关键文件参考

文件路径功能
SmartSqlBuilder.cssrc/SmartSql/SmartSqlBuilder.cs组装整个 SmartSql 运行时的流式构建器
SqlMapper.cssrc/SmartSql/SqlMapper.cs所有查询操作的主要入口点
SmartSqlConfig.cssrc/SmartSql/Configuration/SmartSqlConfig.cs持有所有运行时状态的中央配置对象
ISqlMapper.cssrc/SmartSql/ISqlMapper.cs定义所有 mapper 操作的接口
AbstractMiddleware.cssrc/SmartSql/Middlewares/AbstractMiddleware.cs具有链表链接的所有中间件的基类
InitializerMiddleware.cssrc/SmartSql/Middlewares/InitializerMiddleware.cs解析语句和初始化请求上下文
PrepareStatementMiddleware.cssrc/SmartSql/Middlewares/PrepareStatementMiddleware.cs从 XML 标签构建 SQL 并创建 DbParameters
CachingMiddleware.cssrc/SmartSql/Middlewares/CachingMiddleware.cs缓存读写拦截
TransactionMiddleware.cssrc/SmartSql/Middlewares/TransactionMiddleware.cs事务包装
DataSourceFilterMiddleware.cssrc/SmartSql/Middlewares/DataSourceFilterMiddleware.cs读/写数据源选择
CommandExecuterMiddleware.cssrc/SmartSql/Middlewares/CommandExecuterMiddleware.cs对数据库执行 DbCommand
ResultHandlerMiddleware.cssrc/SmartSql/Middlewares/ResultHandlerMiddleware.cs将 IDataReader 反序列化为类型化结果
Tag.cssrc/SmartSql/Configuration/Tags/Tag.cs所有 XML 标签的抽象基类
Dynamic.cssrc/SmartSql/Configuration/Tags/Dynamic.cs带条件子元素评估的容器标签
Where.cssrc/SmartSql/Configuration/Tags/Where.cs动态 WHERE 子句构建
Set.cssrc/SmartSql/Configuration/Tags/Set.cs动态 SET 子句构建
DataSourceFilter.cssrc/SmartSql/DataSource/DataSourceFilter.cs加权读/写数据源选择
AbstractTypeHandler.cssrc/SmartSql/TypeHandlers/AbstractTypeHandler.cs类型处理器基类
TypeHandlerFactory.cssrc/SmartSql/TypeHandlers/TypeHandlerFactory.cs所有类型处理器的注册表
IRepository.cssrc/SmartSql.DyRepository/IRepository.cs动态仓库基础接口
TransactionAttribute.cssrc/SmartSql.AOP/TransactionAttribute.csAOP 事务拦截器
SmartSqlMapConfig.xmlsample/SmartSql.Sample.AspNetCore/SmartSqlMapConfig.xml示例配置文件
User.xmlsample/SmartSql.Sample.AspNetCore/Maps/User.xml示例 SQL 映射文件

如何添加新中间件

中间件是 SmartSql 中的主要扩展点。以下是添加自定义中间件的分步指南。

第 1 步:创建实现 AbstractMiddleware 的中间件类:

csharp
using SmartSql.Middlewares;
using System.Threading.Tasks;

namespace SmartSql.Middlewares
{
    public class LoggingMiddleware : AbstractMiddleware
    {
        private readonly ILogger _logger;

        // Order determines position in the pipeline.
        // Use a value between existing middleware orders.
        // For example, 150 puts it between PrepareStatement (100) and Caching (200).
        public override int Order => 150;

        public override void SetupSmartSql(SmartSqlBuilder smartSqlBuilder)
        {
            base.SetupSmartSql(smartSqlBuilder);
            _logger = smartSqlBuilder.SmartSqlConfig.LoggerFactory
                .CreateLogger<LoggingMiddleware>();
        }

        protected override void SelfInvoke<TResult>(ExecutionContext executionContext)
        {
            _logger.LogInformation(
                "Executing statement: {FullSqlId}",
                executionContext.Request.FullSqlId);
        }

        protected override Task SelfInvokeAsync<TResult>(ExecutionContext executionContext)
        {
            _logger.LogInformation(
                "Executing statement: {FullSqlId}",
                executionContext.Request.FullSqlId);
            return Task.CompletedTask;
        }
    }
}

第 2 步:在构建 SmartSql 时注册中间件:

csharp
var smartSql = new SmartSqlBuilder()
    .UseXmlConfig()
    .AddMiddleware(new LoggingMiddleware())
    .Build();

PipelineBuilder(在 SmartSqlBuilder.BuildPipeline() 内部使用)按 Order 排序所有中间件并通过 Next 指针链接它们。

关键要点

  • 为同步逻辑重写 SelfInvoke<TResult>,为异步逻辑重写 SelfInvokeAsync<TResult>
  • 调用 InvokeNext<TResult>(executionContext) 将控制传递给下一个中间件。如果不调用,管道将被短路。
  • 使用 Order 属性控制位置。现有顺序为 0、100、200、300、400、500、600。
  • 重写 SetupSmartSql 以接收 SmartSqlBuilder 并提取配置依赖。
  • 使用 Filters(通过 FilterType)附加中间件特定的过滤器钩子。

如何添加新 XML 标签

SmartSql 的 XML 标签是其动态 SQL 能力的核心。以下是添加自定义标签的方法。

第 1 步:创建标签类:

csharp
using SmartSql.Configuration.Tags;

namespace SmartSql.Configuration.Tags
{
    public class IsNull : Tag
    {
        public override bool IsCondition(AbstractRequestContext context)
        {
            object reqVal = EnsurePropertyValue(context);
            return reqVal == null;
        }
    }
}

第 2 步:在 TagBuilderFactory 中注册 TagBuilderTagBuilderFactory 将 XML 元素名称映射到标签构建器函数。你可以通过 XML 配置或编程方式注册:

csharp
// In the TagBuilders section of SmartSqlMapConfig.xml
<TagBuilders>
    <TagBuilder Name="IsNull" Type="MyNamespace.IsNull,MyAssembly"/>
</TagBuilders>

第 3 步:在你的 XML 映射中使用标签:

xml
<Statement Id="GetUser">
    SELECT * FROM T_User
    <Where>
        <IsNull Property="DeletedAt">
            T.DeletedAt IS NULL
        </IsNull>
        <IsNotEmpty Prepend="And" Property="UserName">
            T.UserName = @UserName
        </IsNotEmpty>
    </Where>
</Statement>

标签设计规则

  • 对于简单的条件标签(基于条件包含/排除)继承 Tag
  • 对于包装子元素并管理前缀的容器标签继承 Dynamic(如 WhereSet)。
  • 实现 IsCondition(AbstractRequestContext context) 在子 SQL 应被包含时返回 true
  • 使用 EnsurePropertyValue(context) 安全地获取属性值并遵守 Required 标志。
  • 设置 Prepend 来定义前置关键字(例如 "And""Or""Where""Set")。

如何添加新类型处理器

类型处理器控制 .NET 类型与数据库类型之间的转换。

第 1 步:创建类型处理器类:

csharp
using SmartSql.TypeHandlers;
using SmartSql.Data;
using System;
using System.Data;

public class DateTimeOffsetTypeHandler : AbstractTypeHandler<DateTimeOffset, DateTime>
{
    public override DateTimeOffset GetValue(
        DataReaderWrapper dataReader,
        int columnIndex,
        Type targetType)
    {
        var dateTime = dataReader.GetDateTime(columnIndex);
        return new DateTimeOffset(dateTime, TimeSpan.Zero);
    }

    protected override object GetSetParameterValueWhenNotNull(object parameterValue)
    {
        var dto = (DateTimeOffset)parameterValue;
        return dto.UtcDateTime;
    }
}

第 2 步:注册类型处理器:

csharp
var smartSql = new SmartSqlBuilder()
    .UseXmlConfig()
    .AddTypeHandler(new DateTimeOffsetTypeHandler())
    .Build();

或通过 XML 配置注册:

xml
<TypeHandlers>
    <TypeHandler PropertyType="System.DateTimeOffset, System.Runtime"
                 Type="MyNamespace.DateTimeOffsetTypeHandler, MyAssembly"/>
</TypeHandlers>

类型处理器设计规则

  • 继承 AbstractTypeHandler<TProperty, TField>,其中 TProperty 是 .NET 类型,TField 是数据库字段类型。
  • 重写 GetValue(DataReaderWrapper, int, Type) 从数据读取器读取。
  • 重写 GetSetParameterValueWhenNotNull(object) 为数据库参数转换 .NET 值。
  • 可选重写 Initialize(IDictionary<string, object>) 接受来自 XML 的配置属性。
  • SetParameter(IDataParameter, object) 用于将值绑定到 DbParameter。

如何编写和运行测试

SmartSql 使用 xUnit 进行所有测试。测试基础设施在 src/SmartSql.Test.Unit/ 中。

测试 fixtureSmartSqlFixture 使用 XML 配置初始化 SmartSqlBuilder,注册测试仓库,并填充测试数据。测试使用 IClassFixture<SmartSqlFixture> 来共享 fixture。

运行测试

bash
# All tests
dotnet test

# Single test project
dotnet test src/SmartSql.Test.Unit/SmartSql.Test.Unit.csproj

# Specific test class
dotnet test src/SmartSql.Test.Unit/SmartSql.Test.Unit.csproj \
  --filter "FullyQualifiedName~SmartSql.Test.Unit.Tests.MapperTest"

# Specific test method
dotnet test src/SmartSql.Test.Unit/SmartSql.Test.Unit.csproj \
  --filter "FullyQualifiedName~SmartSql.Test.Unit.Tests.MapperTest.Query"

性能基准测试src/SmartSql.Test.Performance/ 包含 BenchmarkDotNet 测试用于测量性能。使用以下命令运行:

bash
dotnet run -c Release --project src/SmartSql.Test.Performance/SmartSql.Test.Performance.csproj

调试技巧

  1. 启用调试日志:设置日志工厂以查看 SQL 生成和数据源选择:

    csharp
    var smartSql = new SmartSqlBuilder()
        .UseLoggerFactory(loggerFactory)
        .UseXmlConfig()
        .Build();

    PrepareStatementMiddleware 以 Debug 级别记录最终 SQL,DataSourceFilter 记录数据源选择。

  2. 检查管道SmartSqlConfig.Pipeline 属性持有中间件的链表。遍历 Pipeline.Next 查看链顺序。

  3. 检查 XML 标签评估:在 Tag.BuildSql()Tag.IsCondition() 中设置断点来跟踪动态 SQL 如何组装。

  4. 检查 Statement 解析:在 InitializerMiddleware.InitByStatement() 中,语句从配置中解析。如果找不到语句,SmartSqlConfig.GetStatement(fullId) 会抛出 SmartSqlException

贡献工作流

  1. Fork 和分支:Fork 仓库并从 master 创建功能分支。
  2. 进行修改:遵循现有代码风格。项目目标是 netstandard2.0,C# 7.3 -- 避免使用较新的 C# 特性。
  3. 编写测试:在 src/SmartSql.Test.Unit/ 中添加或更新 xUnit 测试。
  4. 构建和测试:运行 dotnet build SmartSql.slndotnet test 确保一切通过。
  5. 提交 PR:向 master 分支发起拉取请求,附带更改的清晰描述。

版本管理

版本定义在 build/version.props 中:

xml
<Project>
    <PropertyGroup>
        <VersionMajor>4</VersionMajor>
        <VersionMinor>1</VersionMinor>
        <VersionPatch>68</VersionPatch>
        <VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
    </PropertyGroup>
</Project>

要发布新版本,在此文件中更新 VersionPatch(或对于破坏性变更更新 VersionMinor/VersionMajor)。

许可证

SmartSql 以 MIT 许可证发布。

如何添加自定义反序列化器

如果 SmartSql 的内置反序列化器无法处理你的结果形状(例如嵌套图对象、具有私有构造函数的 DDD 值对象),你可以添加自定义反序列化器。

第 1 步:实现 IDataReaderDeserializer 接口:

csharp
using SmartSql.Deserializer;
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;

public class GraphDeserializer : IDataReaderDeserializer
{
    public TResult ToSingle<TResult>(ExecutionContext executionContext)
    {
        var dataReader = executionContext.DataReaderWrapper;
        // Custom mapping logic for graph objects
        var result = new TResult();
        // ... populate from dataReader
        return result;
    }

    public IList<TResult> ToList<TResult>(ExecutionContext executionContext)
    {
        var dataReader = executionContext.DataReaderWrapper;
        var results = new List<TResult>();
        while (dataReader.Read())
        {
            results.Add(ToSingle<TResult>(executionContext));
        }
        return results;
    }

    public bool CanDeserialize(ExecutionContext context, Type targetType, bool isMultiple)
    {
        // Return true when this deserializer can handle the target type
        return targetType == typeof(MyGraphEntity);
    }

    // Async variants...
    public Task<TResult> ToSingleAsync<TResult>(ExecutionContext executionContext)
    {
        return Task.FromResult(ToSingle<TResult>(executionContext));
    }

    public Task<IList<TResult>> ToListAsync<TResult>(ExecutionContext executionContext)
    {
        return Task.FromResult(ToList<TResult>(executionContext));
    }
}

第 2 步:注册反序列化器:

csharp
var smartSql = new SmartSqlBuilder()
    .UseXmlConfig()
    .AddDeserializer(new GraphDeserializer())
    .Build();

自定义反序列化器默认添加到链的末尾。DeserializerFactory 遍历所有已注册的反序列化器,使用第一个 CanDeserialize 返回 true 的。

如何添加自定义过滤器

过滤器在中间件执行前后提供钩子。这对于日志记录、指标和审计很有用。

第 1 步:实现适当的过滤器接口:

csharp
using SmartSql.Middlewares.Filters;

public class MetricsFilter : IExecutionFilter
{
    private readonly ILogger _logger;

    public MetricsFilter(ILogger logger)
    {
        _logger = logger;
    }

    public void OnInvoking(ExecutionContext context)
    {
        context.Request.Extend.Set("StartTime", DateTime.UtcNow);
    }

    public void OnInvoked(ExecutionContext context)
    {
        var startTime = (DateTime)context.Request.Extend.Get("StartTime");
        var elapsed = DateTime.UtcNow - startTime;
        _logger.LogInformation(
            "Statement {FullId} executed in {Elapsed}ms",
            context.Request.FullSqlId,
            elapsed.TotalMilliseconds);
    }

    // Async variants
    public Task OnInvokingAsync(ExecutionContext context)
    {
        OnInvoking(context);
        return Task.CompletedTask;
    }

    public Task OnInvokedAsync(ExecutionContext context)
    {
        OnInvoked(context);
        return Task.CompletedTask;
    }
}

第 2 步:注册过滤器:

csharp
var smartSql = new SmartSqlBuilder()
    .UseXmlConfig()
    .AddFilter(new MetricsFilter(logger))
    .Build();

可用的过滤器接口:

  • IExecutionFilter -- 围绕执行生命周期的钩子
  • IStatementFilter -- 围绕语句解析的钩子
  • IPrepareStatementFilter -- 围绕 SQL 准备的钩子
  • ICommandExecuterFilter -- 围绕命令执行的钩子

如何使用示例应用

仓库在 sample/SmartSql.Sample.AspNetCore/ 包含一个可工作的示例应用。这是理解 SmartSql 如何在真实应用中工作的最佳起点。

示例演示的内容

  1. 配置SmartSqlMapConfig.xml 展示了如何配置数据库提供程序、数据源(带读写分离)、类型处理器、标签构建器、ID 生成器和 SQL 映射引用。

  2. SQL 映射Maps/User.xml 文件演示了所有常见模式:动态 WHERE 子句、SET 子句、INCLUDE 引用、SWITCH 语句、带 MultipleResultMap 的分页和带 FlushOnExecute 的 LRU 缓存。

  3. ASP.NET Core 集成Startup.cs 展示了如何使用 services.AddSmartSql() 将 SmartSql 注册到依赖注入容器。

运行示例

bash
cd sample/SmartSql.Sample.AspNetCore
dotnet run

示例使用 SQLite 并在启动时创建数据库架构。不需要外部数据库设置。

常见模式和配方

模式:带总数的分页查询

常见需求是获取一页结果以及总数。SmartSql 通过 MultipleResultMap 处理:

xml
<MultipleResultMaps>
    <MultipleResultMap Id="QueryByPageResult">
        <Result Property="List"/>
        <Result Property="Total"/>
    </MultipleResultMap>
</MultipleResultMaps>

<Statement Id="QueryByPage" MultipleResultMap="QueryByPageResult">
    SELECT T.* FROM T_User T
    <Include RefId="QueryParams"/>
    LIMIT @PageSize OFFSET @Offset;
    SELECT COUNT(1) FROM T_User T
    <Include RefId="QueryParams"/>;
</Statement>

MultipleResultMap 按顺序将每个结果集映射到返回类型的相应属性。

模式:条件更新

仅更新提供的字段:

xml
<Statement Id="Update">
    UPDATE T_User
    <Set>
        <IsProperty Prepend="," Property="UserName">
            UserName = @UserName
        </IsProperty>
        <IsProperty Prepend="," Property="Status">
            Status = @Status
        </IsProperty>
    </Set>
    WHERE Id = @Id
</Statement>

<Set> 标签自动去掉前导逗号。<IsProperty> 仅在请求中存在该属性时才包含该字段。

模式:带集合的 IN 子句

按 ID 列表查询:

xml
<Statement Id="QueryByIds">
    SELECT * FROM T_User
    WHERE Id IN
    <For Property="Ids" Open="(" Separator="," Close=")">
        @Id
    </For>
</Statement>

<For> 标签遍历 Ids 集合,生成 (1, 2, 3) 语法。

模式:可重用 SQL 片段

使用 <Statement> 定义共享 SQL 片段,并使用 <Include> 引用:

xml
<Statement Id="QueryParams">
    <Where>
        <IsNotEmpty Prepend="And" Property="Status">
            T.Status = @Status
        </IsNotEmpty>
        <IsNotEmpty Prepend="And" Property="UserName">
            T.UserName = @UserName
        </IsNotEmpty>
    </Where>
</Statement>

<Statement Id="Query">
    SELECT T.* FROM T_User T
    <Include RefId="QueryParams"/>
</Statement>

<Statement Id="GetRecord">
    SELECT COUNT(1) FROM T_User T
    <Include RefId="QueryParams"/>
</Statement>

模式:显式数据源路由

覆盖自动读写路由:

xml
<Statement Id="Query" ReadDb="ReadDb-2">
    SELECT * FROM T_User WHERE Id = @Id
</Statement>

ReadDb 属性将此语句固定到特定的读副本。

模式:自定义 ID 生成

使用 SnowflakeId 进行分布式 ID 生成:

xml
<IdGenerators>
    <IdGenerator Name="SnowflakeId" Type="SnowflakeId">
        <Properties>
            <Property Name="WorkerId" Value="1"/>
        </Properties>
    </IdGenerator>
</IdGenerators>

<Statement Id="Insert">
    INSERT INTO T_User (Id, UserName)
    VALUES (@Id, @UserName)
</Statement>

在代码中,在调用插入之前设置 ID:

csharp
var id = SmartSqlId.Default.NextId();

CI/CD 建议

为 SmartSql 项目构建 CI/CD 流水线时:

  1. 验证 XML 架构:作为构建步骤,对提供的 XSD 架构(SmartSqlMapConfig.xsdSmartSqlMap.xsd)运行 XML 验证。

  2. 测试 SQL 映射:包含集成测试,执行 XML 映射中的每个 Statement。测试项目中使用的 SmartSqlFixture 模式是一个好的模板。

  3. 版本控制 XML 文件:以与源代码相同的纪律对待 XML SQL 映射。对 SQL 更改使用拉取请求审查。

  4. 数据库测试容器:使用 Testcontainers 或类似工具在 CI 中启动测试数据库,确保 XML SQL 映射针对实际数据库引擎进行验证。

  5. 监控缓存效果:在预发布环境中,记录缓存命中率以验证缓存配置在生产部署前是否有效。


术语表

术语定义
SmartSqlBuilder组装完整 SmartSql 运行时的流式构建器:配置、管道、缓存、类型处理器和过滤器
SmartSqlConfig持有数据库连接、SQL 映射、中间件管道、类型处理器、缓存管理器和所有已解析设置的中央配置对象
ISqlMapper执行 SQL 操作的主要接口(Execute、ExecuteScalar、Query、QuerySingle、GetDataTable、GetDataSet)
SqlMapper管理会话生命周期的 ISqlMapper 具体实现
中间件管道按顺序处理每个 SQL 操作的 IMiddleware 节点链表
StatementXML 中定义的单个 SQL 操作,由 Scope.Id 标识
SqlMap包含 Statements、Caches、ResultMaps 和其他定义的 XML 文件,由其 Scope 标识
ScopeSmartSqlMap XML 文件的命名空间;用作完整语句 ID 的前缀
TagStatement 中条件性构建 SQL 的 XML 元素(例如 <Where><IsNotEmpty>
TagBuilder从 XML 元素创建 Tag 实例的工厂
TypeHandler在参数绑定和结果读取期间在 .NET 类型和数据库类型之间进行转换
TypeHandlerFactory所有 TypeHandler 的注册表,为给定 .NET 类型解析正确的处理器
ResultMap从数据库列到对象属性的映射定义
ParameterMap具有显式类型处理器分配的存储过程参数映射定义
MultipleResultMap定义单个语句的多个结果集如何映射到不同属性
ExecutionContext在管道中携带 SmartSqlConfigIDbSessionAbstractRequestContextResultContext
AbstractRequestContext持有语句引用、SQL 构建器、参数、数据源选择、缓存 ID 和执行选项
DataSourceChoice枚举:ReadWrite,确定使用哪个数据源
WeightFilter加权随机选择读数据源的算法
IDbSession包装连接和可选事务的数据库会话
IDbSessionStore当前线程的会话本地存储(使用 AsyncLocal
DyRepository动态仓库 -- IL 发射的代理,将接口方法映射到 SQL 语句
CUDConfigBuilder为注册的实体类型自动生成 Create/Update/Delete SQL 语句
SnowflakeId内置分布式 ID 生成器(Twitter Snowflake 算法)
AOP 事务通过 [Transaction] 特性使用 AspectCore 的声明式事务管理
DiagnosticSource用于命令执行、会话生命周期和错误的 .NET 诊断事件发射器
FullId语句的组合标识符:Scope.StatementId(例如 User.Query
InvokeSucceedListenerSQL 成功执行后调用的事件监听器,用于缓存刷新和同步
SmartSqlContainer按别名持有所有 SmartSqlBuilder 实例的单例注册表

基于 MIT 许可证发布。