Skip to content

XML 标签系统

SmartSql 使用 XML 定义 SQL 语句,但与静态 SQL 映射器不同,它提供了一套丰富的标签系统用于动态 SQL 构建。标签在运行时根据请求参数进行评估,以有条件地包含 SQL 片段、迭代集合、按值切换以及注入生成的值。该系统受 MyBatis 动态 SQL 的启发,但针对 .NET 类型和约定进行了定制。

概要

方面详情
接口ITag,具有 IsCondition()BuildSql() 方法
基类Tag 提供基于属性的条件检查和子标签迭代
容器每个 Statement 持有一个 ITag 子项列表,按顺序处理
组合标签DynamicWhereSet 充当容器,根据子匹配前置关键字
工厂TagBuilderFactory 从 XML 元素定义创建标签实例

标签类层次结构

mermaid
classDiagram
    class ITag {
        <<interface>>
        +Statement Statement
        +ITag Parent
        +IsCondition(AbstractRequestContext) bool
        +BuildSql(AbstractRequestContext) void
    }
    class Tag {
        <<abstract>>
        +String Prepend
        +String Property
        +bool Required
        +IList~ITag~ ChildTags
        +IsCondition(AbstractRequestContext)* bool
        +BuildSql(AbstractRequestContext) void
        +BuildChildSql(AbstractRequestContext) void
        #EnsurePropertyValue(AbstractRequestContext) Object
    }
    class SqlText {
        +String BodyText
        +BuildSql(AbstractRequestContext) void
    }
    class IdGenerator {
        +IIdGenerator IdGen
        +bool Assign
    }
    class Now {
        +String Kind
    }
    class UUID {
        +String Format
    }

    ITag <|.. Tag
    ITag <|.. SqlText
    ITag <|.. IdGenerator
    ITag <|.. Now
    ITag <|.. UUID
    Tag <|-- CompareTag
    Tag <|-- Dynamic
    Tag <|-- IsNotEmpty
    Tag <|-- IsEmpty
    Tag <|-- IsNotNull
    Tag <|-- IsProperty
    Tag <|-- IsNotProperty
    Tag <|-- Placeholder
    Tag <|-- OrderBy
    Tag <|-- Range
    Tag <|-- Env
    Tag <|-- Include
    Tag <|-- Switch
    Tag <|-- For

    CompareTag <|-- StringCompareTag
    CompareTag <|-- NumericalCompareTag
    StringCompareTag <|-- IsEqual
    StringCompareTag <|-- IsNotEqual
    StringCompareTag <|-- IsTrue
    StringCompareTag <|-- IsFalse
    StringCompareTag <|-- Switch_Case
    NumericalCompareTag <|-- IsGreaterThan
    NumericalCompareTag <|-- IsLessThan
    NumericalCompareTag <|-- IsGreaterEqual
    NumericalCompareTag <|-- IsLessEqual

    Dynamic <|-- Where
    Dynamic <|-- Set

Statement 如何处理标签

PrepareStatementMiddleware 调用 Statement.BuildSql() 时,每个 statement 遍历其子标签。每个标签根据请求参数检查其条件,如果条件通过则将其 SQL 片段追加到 SqlBuilder

mermaid
flowchart TD
    subgraph Processing["Statement.BuildSql Flow"]
        style Processing fill:#161b22,stroke:#30363d,color:#e6edf3
        Start["Statement.BuildSql(request)"] --> Loop["For each ITag child"]
        Loop --> Check{"tag.IsCondition(request)?"}
        Check -->|Yes| Build["tag.BuildSql(request)"]
        Build --> Append["Append Prepend + Child SQL to SqlBuilder"]
        Append --> Next["Next tag"]
        Check -->|No| Skip["Skip tag"]
        Skip --> Next
        Next --> End{"More tags?"}
        End -->|Yes| Loop
        End -->|No| Done["request.RealSql = SqlBuilder.ToString()"]
    end

    style Start fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Loop fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Check fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Build fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Append fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Next fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Skip fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style End fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Done fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

标签类型参考

条件标签

这些标签根据请求参数检查条件,仅在条件满足时包含其子内容。

IsNotEmpty

当属性值不为 null、不为空字符串,且(对于集合)至少有一个元素时渲染子内容。

xml
<IsNotEmpty Property="UserName">
  AND UserName = @UserName
</IsNotEmpty>

IsEmpty

IsNotEmpty 的反面 -- 当属性为 null、空字符串或空集合时渲染。

IsEqual / IsNotEqual

将属性值(作为字符串)与 CompareValue 属性进行比较。

xml
<IsEqual Property="Status" CompareValue="1">
  AND Active = 1
</IsEqual>

IsGreaterThan / IsLessThan / IsGreaterEqual / IsLessEqual

数值比较标签,将属性值解析为 Decimal 并与 CompareValue 比较。

xml
<IsGreaterThan Property="Age" CompareValue="18">
  AND Age > @Age
</IsGreaterThan>

IsNotNull / IsNull

检查属性值是否为 null 或不为 null。

IsProperty / IsNotProperty

检查请求参数中是否存在该属性键(不论值)。

xml
<IsProperty Property="DeptId">
  AND DeptId = @DeptId
</IsProperty>
<IsNotProperty Property="DeptId">
  AND DeptId IS NULL
</IsNotProperty>

IsProperty 还支持 PropertyChanged 跟踪以用于实体代理变更检测。

IsTrue / IsFalse

布尔比较标签,检查属性值是否等于字符串 "True" 或 "False"。

Range

检查数值属性值是否在 [Min, Max] 范围内。

xml
<Range Property="PageNum" Min="1" Max="100">
  -- 分页逻辑
</Range>

Env

根据当前数据库提供程序名称有条件地包含内容。适用于数据库特定的 SQL。

xml
<Env DbProvider="MySql">
  LIMIT @PageSize OFFSET @Offset
</Env>
<Env DbProvider="SqlServer">
  OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
</Env>

组合 / 结构化标签

Where

扩展 DynamicPrepend = "Where"。仅在至少一个子标签条件通过时渲染 WHERE 关键字。第一个匹配的子项获得 WHERE 关键字前置;后续子项使用它们自己的 Prepend 值(通常为 AND)。

xml
<Where>
  <IsNotEmpty Property="Name">
    AND Name = @Name
  </IsNotEmpty>
  <IsNotEmpty Property="Status">
    AND Status = @Status
  </IsNotEmpty>
</Where>
<!-- 产生: WHERE Name = @Name AND Status = @Status -->

Set

扩展 DynamicPrepend = "Set"。在 UPDATE 语句中使用以有条件地包含 SET 子句。

xml
<Set>
  <IsNotEmpty Property="Name">
    Name = @Name,
  </IsNotEmpty>
  <IsNotEmpty Property="Status">
    Status = @Status,
  </IsNotEmpty>
</Set>

Dynamic

基础组合标签。评估子标签并在第一个匹配子项前渲染 Prepend。支持 Min 属性,如果匹配的子项少于最小值则抛出 TagMinMatchedFailException

Switch / Case / Default

switch-case 结构,通过将属性值与 CompareValue 比较来评估子 Case 标签,如果没有 case 匹配则回退到 Default

xml
<Switch Property="SortField">
  <Case CompareValue="Name">ORDER BY Name ASC</Case>
  <Case CompareValue="Date">ORDER BY CreateTime DESC</Case>
  <Default>ORDER BY Id ASC</Default>
</Switch>

For

迭代集合并为每个元素生成参数化 SQL。支持 OpenSeparatorClose 属性来控制分隔符。

xml
<For Property="Ids" Open="(" Close=")" Separator="," Key="id">
  @id
</For>
<!-- 产生: (@Ids_For__0, @Ids_For__1, @Ids_For__2) -->

For 标签同时处理直接值(基本类型、字符串)和复杂对象,为每次迭代创建唯一命名的参数以避免冲突。

Include

通过 RefId 引用另一个语句的 SQL 内容,实现 SQL 片段复用。

xml
<Include RefId="BaseColumns">
  <!-- 引用定义公共列选择的另一个 Statement -->
</Include>

值注入标签

这些标签总是渲染并注入值到参数集合中,而非产生 SQL 文本。

Now

将当前日期/时间注入请求参数。支持 Kind="UTC" 用于 UTC 时间。

xml
<Now Property="CreateTime" />
<!-- 将请求参数 CreateTime 设置为 DateTime.Now -->

UUID

生成新的 GUID 并注入请求参数。支持 Format 属性用于字符串格式化。

xml
<UUID Property="Id" Format="N" />
<!-- 将请求参数 Id 设置为格式化为 "N"(无连字符)的 GUID -->

Placeholder

直接将属性值插入 SQL 字符串(非参数化值)。用于动态表名或列名等无法参数化的情况。

xml
<Placeholder Property="TableName" Prepend="FROM " />
<!-- 产生: FROM Users(字面字符串替换) -->

OrderBy

KeyValuePair<string, string> 或键值对集合生成 ORDER BY 子句,其中 Key 是列名,Value 是方向。

xml
<OrderBy Property="Sort" />
<!-- 如果 Sort = {Key: "Name", Value: "ASC"},产生: ORDER BY Name ASC -->

IdGenerator

使用已注册的 IIdGenerator(例如 Snowflake)生成唯一 ID 并注入请求参数。

SqlText

SqlText 是持有标签间原始 SQL 文本的叶节点。它还处理自动 IN 子句展开:当检测到 IN @ParamName 语法且参数值为 IEnumerable 时,将参数展开为单独的编号参数。

标签层次图

下图展示了为具体标签提供通用行为的抽象继承链:

mermaid
graph TD
    subgraph Interfaces["Tag Interfaces"]
        style Interfaces fill:#161b22,stroke:#30363d,color:#e6edf3
        ITag["ITag"]
        ITagB["ITagBuilder"]
    end

    subgraph Base["Abstract Base"]
        style Base fill:#161b22,stroke:#30363d,color:#e6edf3
        AbsTag["Tag (abstract)"]
        CmpTag["CompareTag&lt;T&gt;"]
        StrCmp["StringCompareTag"]
        NumCmp["NumericalCompareTag"]
    end

    subgraph Conditional["Conditional Tags"]
        style Conditional fill:#161b22,stroke:#30363d,color:#e6edf3
        INE["IsNotEmpty"]
        IE["IsEmpty"]
        INN["IsNotNull"]
        ISN["IsNull"]
        IP["IsProperty"]
        INP["IsNotProperty"]
        IEq["IsEqual"]
        INEq["IsNotEqual"]
        IGT["IsGreaterThan"]
        ILT["IsLessThan"]
        IGE["IsGreaterEqual"]
        ILE["IsLessEqual"]
        IT["IsTrue"]
        IF["IsFalse"]
        Rng["Range"]
    end

    subgraph Composite["Composite Tags"]
        style Composite fill:#161b22,stroke:#30363d,color:#e6edf3
        Dyn["Dynamic"]
        Whr["Where"]
        St["Set"]
        Sw["Switch"]
        Fr["For"]
        Inc["Include"]
        EnvT["Env"]
    end

    subgraph Inject["Value Injection"]
        style Inject fill:#161b22,stroke:#30363d,color:#e6edf3
        NowT["Now"]
        UUIDT["UUID"]
        PH["Placeholder"]
        OB["OrderBy"]
        IDG["IdGenerator"]
        ST["SqlText"]
    end

    ITag --> AbsTag
    AbsTag --> CmpTag
    AbsTag --> Dyn
    AbsTag --> INE
    AbsTag --> IE
    AbsTag --> INN
    AbsTag --> ISN
    AbsTag --> IP
    AbsTag --> INP
    AbsTag --> Rng
    AbsTag --> Fr
    AbsTag --> Inc
    AbsTag --> EnvT
    AbsTag --> PH
    AbsTag --> OB
    CmpTag --> StrCmp
    CmpTag --> NumCmp
    StrCmp --> IEq
    StrCmp --> INEq
    StrCmp --> IT
    StrCmp --> IF
    NumCmp --> IGT
    NumCmp --> ILT
    NumCmp --> IGE
    NumCmp --> ILE
    Dyn --> Whr
    Dyn --> St
    Dyn --> Sw

    style ITag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style AbsTag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CmpTag fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style StrCmp fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style NumCmp fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Dyn fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Whr fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style St fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style INE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IEq fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Fr fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ST fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ING fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style NowT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style UUIDT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style PH fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style OB fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IDG fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Inc fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style EnvT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Rng fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Sw fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IGT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ILT fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

完整示例

以下是一个展示多种标签类型协同工作的完整 XML 语句:

xml
<Statement Id="QueryUsers">
  SELECT * FROM Users
  <Where>
    <IsNotEmpty Property="Name">
      AND Name LIKE CONCAT('%', @Name, '%')
    </IsNotEmpty>
    <IsEqual Property="Status" CompareValue="1">
      AND IsActive = 1
    </IsEqual>
    <IsGreaterThan Property="MinAge" CompareValue="0">
      AND Age >= @MinAge
    </IsGreaterThan>
    <Env DbProvider="MySql">
      LIMIT @PageSize OFFSET @Offset
    </Env>
  </Where>
  <Switch Property="SortField">
    <Case CompareValue="Name">ORDER BY Name ASC</Case>
    <Default>ORDER BY Id DESC</Default>
  </Switch>
</Statement>

相关页面

参考资料

基于 MIT 许可证发布。