跳转到内容

数据访问

框架的数据访问基于 SqlSugar。本章讲实体怎么定义、仓储怎么用、查询过滤器做了什么、分页协议长什么样。完整 API 见 Data 包

安装

bash
dotnet add package XiHan.Framework.Data
csharp
[DependsOn(typeof(XiHanDataModule))]
public class MyAppModule : XiHanModule { }

配置节 XiHan:Data:SqlSugarCore,支持 PostgreSQL / MySQL / SQL Server / Oracle / 达梦 / 人大金仓等。

实体基类

按「需要哪些审计能力」选:

基类主键审计软删
SugarEntity<TKey>应用生成
SugarEntityWithIdentity<TKey>数据库自增
SugarCreationEntity<TKey>应用生成创建
SugarModificationEntity<TKey>应用生成创建 + 修改
SugarDeletionEntity<TKey>应用生成创建 + 删除
SugarFullAuditedEntity<TKey>应用生成全套
SugarAggregateRoot<TKey>应用生成全套

每个都有 SugarMultiTenant* 变体,多带一个 Tenant_Id——多租户应用一律用这一系列。

自动注入的列:

说明
Basic_Id主键(注意不是 Id),默认非自增
Row_Version乐观并发标识
Created_Time / Created_Id / Created_By创建审计,IsOnlyIgnoreUpdate
Modified_* / Deleted_* / Is_Deleted修改与删除审计
Tenant_Id多租户列

审计列由 AOP 自动填

走 SqlSugar 的 DataExecuting AOP,业务代码不要手动赋值

仓储

csharp
public class OrderService(IRepositoryBase<Order, long> orders) : ITransientDependency
{
    public async Task<Order?> GetAsync(long id)
        => await orders.GetByIdAsync(id);

    public async Task<Order> CreateAsync(Order order)
        => await orders.InsertReturnEntityAsync(order);
}

仓储基类实现 IScopedDependency约定自动注册,不用手写 DI。

自定义仓储:

csharp
public interface IOrderRepository : IRepositoryBase<Order, long>
{
    Task<bool> ExistsCodeAsync(string code);
}

public class OrderRepository(ISqlSugarClientResolver resolver)
    : SqlSugarRepositoryBase<Order, long>(resolver), IOrderRepository
{
    public Task<bool> ExistsCodeAsync(string code)
        => DbClient.Queryable<Order>().AnyAsync(x => x.Code == code);
}

全局查询过滤器

租户过滤与软删过滤由 ISqlSugarClientResolver + 全局 QueryFilter AOP 统一挂上,业务代码不用写 WHERE TenantId = ?WHERE IsDeleted = false

写操作的过滤是自动的

配置 EnableAutoUpdateQueryFilter / EnableAutoDeleteQueryFilter 默认 true,SqlSugar 的 Updateable<T>() / Deleteable<T>() 工厂内部已经自动挂了一次

仓储里禁止再显式调用 .EnableQueryFilter()

重复挂会把同一份过滤烘进 WHERE 两遍、生成同名参数 @constant1001;一旦叠加 Diff 的 GetDiffTable 重查旧值就崩(MySQL 驱动直接抛,PG 容忍但仍是冗余死条件)。

.EnableDiffLogEvent() 保留,它单独用是安全的。

写路径的租户边界

读共享 ≠ 写共享:全局过滤器为「读共享」放行 TenantId=0 的平台全局行,但写路径不复用这个口径——租户上下文内禁止改写/删除非本租户行(含全局行)。预读守卫会校验取回行的 TenantId,条件写自动追加当前租户 Where

维护全局数据的唯一合法入口是平台态ICurrentTenant.Change(null))。

分页与查询协议

分页请求是两段结构(PageRequestDtoBase):

json
{
  "conditions": {
    "keyword": { "value": "张", "fields": ["userName", "nickName"] },
    "filters": [
      { "field": "status", "operator": 1000, "value": "Enabled" },
      { "field": "createdTime", "operator": 4000, "values": ["2026-01-01", "2026-08-04"] }
    ],
    "sorts": [{ "field": "createdTime", "direction": 1001, "priority": 0 }]
  },
  "page": { "pageIndex": 1, "pageSize": 20 }
}

QueryOperatorEqual(1000) / NotEqual(1001) / GreaterThan(1002) / GreaterThanOrEqual(1003) / LessThan(1004) / LessThanOrEqual(1005) / Contains(2000) / StartsWith(2001) / EndsWith(2002) / In(3000) / NotIn(3001) / Between(4000) / IsNull(5000) / IsNotNull(5001)。

SortDirectionAscending(1000) / Descending(1001)。

pagepageIndex 从 1 开始(小于 1 自动纠正),pageSize 默认 20上限 500(超出自动截断)。

响应是 PageResultDtoBase<T>Items + PagetotalCount / totalPages / hasNext / startRecord 等)+ 可选 ExtendDatas

分页方法要显式标 [HttpPost]

动态 API 会把 GetXxxPageAsync 推导成 GET,请求体绑不上。见 动态 API

建库建表与种子

配置说明
EnableDbInitialization启动时自动建库
EnableTableInitialization启动时 CodeFirst 建表
EnableDataSeeding启动时播种

DbInitializer 表存在就跳过,从不补列

给既有实体加字段后部署必报「列不存在」。要么重建数据库,要么手动 ALTER TABLE框架不是迁移工具。

读写分离

ConnectionConfigs[].SlaveConnectionConfigs 填了从库后,SELECT 自动走从库、写与事务走主库。

HitRate 绑不上 appsettings

它是 SqlSugar 的字段不是属性,配了也恒为 0。框架会把权重 0 的从库归一化为 DefaultSlaveHitRate(默认 10),所以不写也能等权分担读。需要差异化权重用代码钩子 ConfigureConnectionConfigs

数据变更日志

EnableDiffLog 默认 false——不开则 Diff AOP 不挂载,收集到的差异被直接丢弃。开启后每个写操作会多一次 SELECT 用于算差异,且只覆盖走仓储的写。

常见问题

现象原因
部署后报「列不存在」加了字段没重建库
写操作报参数重名仓储里显式调了 .EnableQueryFilter()
查到了别的租户数据实体没继承 SugarMultiTenant* 系列
变更日志空EnableDiffLog 没开
时间列投影崩溃DateTimeOffset 别做单列标量投影,整行取实体

下一步

Released under The MIT License