5. 实体定义全解
后端手册实体CodeFirst大约 6 分钟约 1733 字
一个 POCO 类 = 一张表:继承哪个基类决定了有没有审计字段、租户隔离和数据范围过滤;[SugarTable]/[SugarColumn]/[SugarIndex] 决定表结构;[SysTable]/[LogTable]/[Tenant] 决定落在哪个库。
1. 基类继承链与选型
EntityBaseId → Id(雪花Id, 主键)
└─ EntityBase : IDeletedFilter → Id + CreateTime/UpdateTime/CreateUserId/CreateUserName
+ UpdateUserId/UpdateUserName/IsDelete/DeleteTime
├─ EntityBaseData : IOrgIdFilter → + CreateOrgId/CreateOrgName/UpdateOrgId/UpdateOrgName
├─ EntityTenant : ITenantIdFilter → + TenantId + Navigate(SysTenant)
└─ EntityTenantBaseData : IOrgIdFilter, ITenantIdFilter → + 机构字段 + TenantId + Navigate
└─ EntityTenantId : ITenantIdFilter → Id + TenantId(极简租户表)
└─ EntityTenantOrgId → Id + TenantOrgId
└─ EntityTenantOrg → EntityBase + TenantOrgId| 基类 | 审计字段 | 租户隔离 | 数据范围 | 适用场景 |
|---|---|---|---|---|
EntityBaseId | ✗ | ✗ | ✗ | 纯关联表(如 SysUserRole) |
EntityTenantId | ✗ | ✓ | ✗ | 轻量关联表 |
EntityBase | ✓ | ✗ | ✗ | 全租户共享的配置/字典类(SysConfig、SysDictType、SysDictData) |
EntityTenant | ✓ | ✓ | ✗ | 租户内共享(SysUser、SysPos、SysOrg、SysRole) |
EntityBaseData | ✓ | ✗ | ✓ | 非租户化的业务数据 |
EntityTenantBaseData | ✓ | ✓ | ✓ | 绝大多数业务表推荐 |
EntityTenantOrg | ✓ | — | ✓ | 按"租户机构"隔离 |
过滤器接口(Admin.NET.Core/Entity/IEntityFilter.cs):
public interface IDeletedFilter { bool IsDelete { get; set; } } // 软删除
public interface ITenantIdFilter { long? TenantId { get; set; } } // 租户
public interface IOrgIdFilter { long? CreateOrgId { get; set; } } // 机构(数据范围)全局过滤器在 SqlSugarSetup.SetDbAop() 注入,超管自动跳过:
if (App.User?.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString()) return;
dbProvider.QueryFilter.AddTableFilter<IDeletedFilter>(u => u.IsDelete == false);
dbProvider.QueryFilter.AddTableFilter<ITenantIdFilter>(u => u.TenantId == long.Parse(tenantId));
SqlSugarFilter.SetOrgEntityFilter(dbProvider); // 机构集合 / 仅本人
SqlSugarFilter.SetCustomEntityFilter(dbProvider);// 自定义 IEntityFilter2. 表级特性
| 特性 | 作用 |
|---|---|
[SugarTable(null, "系统用户表")] | 表名取类名、表注释;第一个参数写死表名则不用类名 |
[SysTable] | 强制归属主库 1300000000001(SqlSugarConst.MainConfigId) |
[LogTable] | 归属日志库 1300000000002(SqlSugarConst.LogConfigId) |
[Tenant("configId")] | 归属指定业务分库(SqlSugar 自带特性) |
[SugarIndex("i_{table}_a", nameof(Account), OrderByType.Asc)] | 建索引,{table} 自动替换为表名;IsUnique = true 建唯一索引 |
[IncreTable] | 只有它参与增量建表(配合 EnableIncreTable) |
[IgnoreTable] | 不建表(视图必须加) |
[SplitTable] | 分表(SqlSugar 自带,配 SplitTableInsertAsync 等仓储方法) |
3. 列级特性
| 特性 | 说明 |
|---|---|
[SugarColumn(ColumnDescription="账号", Length=32)] | 注释 + 长度(varchar(32)) |
[SugarColumn(IsPrimaryKey = true, IsIdentity = false)] | 主键(基类 Id 已带) |
[SugarColumn(DefaultValue = "100")] | DDL 默认值 |
[SugarColumn(IsNullable = true)] | 可空(属性声明为 string?/long? 时框架自动推断) |
[SugarColumn(IsIgnore = true)] | 不映射列(如 SysPos.UserList) |
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)] | 大文本(如 SysDictData.ExtData) |
[SugarColumn(DecimalDigits = 1)] | 小数位 |
[SugarColumn(IsOnlyIgnoreUpdate = true)] | 更新时不更新此列(创建类字段) |
[Navigate(NavigateType.OneToOne, nameof(DictTypeId))] | 一对一导航(通常配 [JsonIgnore]) |
[Navigate(NavigateType.OneToMany, nameof(SysDictData.DictTypeId))] | 一对多导航 |
业务特性(写在字段上,参与自动填充 / 校验 / 序列化):
| 特性 | 位置 | 作用 |
|---|---|---|
[OwnerUser] | CreateUserId | 数据范围=仅本人时,自动加 CreateUserId = 当前用户 过滤 |
[OwnerOrg] | CreateOrgId/UpdateOrgId | 数据范围=机构时,自动加机构过滤 |
[DataMask(3, 4)] | 手机号等 | 查询后自动脱敏 |
[Dict("dict_code")] | 枚举/字符串 | 入参必须落在字典取值范围内 |
[IdCardNo] | 身份证 | 身份证格式校验 |
[MaxValue(9999)] / [MinValue] | 数值 | 上下界校验 |
[RequiredIF(nameof(Type), 1)] | 任意 | 条件必填 |
[DateTimeRange] | List<DateTime> | 时间范围校验(起止、区间、未来/过去) |
[BindSerial("OrderNo")] | 流水号字段 | 插入时自动取流水号 |
[ImportDict("code", nameof(Target))] | 导入字段 | Excel 导入时字典文本→值 |
[JsonConverter(typeof(MaskConverter))] | Output | 输出脱敏 |
[IgnoreUpdateSeedColumn] | 种子字段 | 种子更新时忽略此列 |
4. 审计字段自动填充(DataExecuting AOP)
SqlSugarSetup.SetDbAop() 的 dbProvider.Aop.DataExecuting 统一处理,业务代码不要手工赋值。
InsertByObject
├─ 主键 long 且为 0/null → YitIdHelper.NextId()(雪花 Id)
├─ 主键 Guid 且为 null → Guid.NewGuid()
├─ CreateTime 为空/DateTime.MinValue → DateTime.Now
├─ TenantId 为空 → 取 Claims[TenantId],兜底 SqlSugarConst.DefaultTenantId
├─ CreateUserId → Claims[UserId]
├─ CreateUserName → Claims[RealName]
├─ CreateOrgId → Claims[OrgId]
└─ CreateOrgName → Claims[OrgName]
UpdateByObject
├─ UpdateTime → DateTime.Now
├─ DeleteTime → IsDelete == true 时写 DateTime.Now
├─ UpdateUserId → Claims[UserId]
├─ UpdateUserName → Claims[RealName]
├─ UpdateOrgId → Claims[OrgId]
└─ UpdateOrgName → Claims[OrgName]
最后统一:entityInfo.SetSerialProperty() // [BindSerial] 流水号非 Web 线程(定时任务、事件总线、单元测试)里
App.User == null,创建者字段不会被填充,TenantId需用SqlSugarConst.DefaultTenantId或手工指定。
5. 完整实体示例
namespace Admin.NET.Application.Entity;
/// <summary>
/// 系统职位表(真实文件:Admin.NET.Core/Entity/SysPos.cs)
/// </summary>
[SugarTable(null, "系统职位表")]
[SysTable]
[SugarIndex("i_{table}_n", nameof(Name), OrderByType.Asc)]
[SugarIndex("i_{table}_c", nameof(Code), OrderByType.Asc)]
public partial class SysPos : EntityTenant
{
/// <summary>
/// 名称
/// </summary>
[SugarColumn(ColumnDescription = "名称", Length = 64)]
[Required, MaxLength(64)]
public virtual string Name { get; set; }
/// <summary>
/// 编码
/// </summary>
[SugarColumn(ColumnDescription = "编码", Length = 64)]
[MaxLength(64)]
public string? Code { get; set; }
/// <summary>
/// 排序
/// </summary>
[SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
public int OrderNo { get; set; } = 100;
/// <summary>
/// 状态
/// </summary>
[SugarColumn(ColumnDescription = "状态", DefaultValue = "1")]
public StatusEnum Status { get; set; } = StatusEnum.Enable;
/// <summary>
/// 在职人员(不映射列)
/// </summary>
[SugarColumn(IsIgnore = true)]
public List<SysUser> UserList { get; set; }
}带导航 + 一对多的字典类型(Admin.NET.Core/Entity/SysDictType.cs 简化):
[SugarTable(null, "系统字典类型表")]
[SysTable]
[SugarIndex("i_{table}_n", nameof(Name), OrderByType.Asc)]
[SugarIndex("i_{table}_c", nameof(Code), OrderByType.Asc)]
public partial class SysDictType : EntityBase
{
[SugarColumn(ColumnDescription = "名称", Length = 64)]
[Required, MaxLength(64)]
public virtual string Name { get; set; }
[SugarColumn(ColumnDescription = "编码", Length = 64)]
[Required, MaxLength(64)]
public virtual string Code { get; set; }
[SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
public int OrderNo { get; set; } = 100;
[SugarColumn(ColumnDescription = "状态", DefaultValue = "1")]
public StatusEnum Status { get; set; } = StatusEnum.Enable;
/// <summary>字典值集合(一对多)</summary>
[Navigate(NavigateType.OneToMany, nameof(SysDictData.DictTypeId))]
public List<SysDictData> Children { get; set; }
}业务表(推荐写法,租户 + 机构 + 增量表):
// Admin.NET.Application/Entity/Leave.cs(简化)
[SugarTable("Leave", "请假单表")]
[IncreTable] // 参与增量建表
public class Leave : EntityBase // 或 EntityTenantBaseData
{
[SugarColumn(ColumnDescription = "请假类型", Length = 32)]
[MaxLength(32)]
public string LeaveType { get; set; } = string.Empty;
[SugarColumn(ColumnDescription = "开始时间")]
public DateTime StartTime { get; set; }
[SugarColumn(ColumnDescription = "请假天数", DecimalDigits = 1)]
public decimal Days { get; set; }
[SugarColumn(ColumnDescription = "请假原因", Length = 500)]
[MaxLength(500)]
public string? Reason { get; set; }
[SugarColumn(ColumnDescription = "申请人ID")]
public long ApplicantId { get; set; }
[SugarColumn(ColumnDescription = "业务状态")]
public int Status { get; set; } = 0;
}视图实体(Admin.NET.Application/Entity/TestViewSysUser.cs):
[SugarTable(null, "用户表视图"), IgnoreTable] // 必须加 IgnoreTable,防止被建成表
public class TestViewSysUser : EntityBase, ISqlSugarView
{
[SugarColumn(ColumnDescription = "账号")]
public virtual string Account { get; set; }
[SugarColumn(ColumnDescription = "机构名称")]
public string? OrgName { get; set; }
public string GetQueryableSqlString(SqlSugarScopeProvider db)
{
return db.Queryable<SysUser>()
.LeftJoin<SysOrg>((u, a) => u.OrgId == a.Id)
.Select((u, a) => new TestViewSysUser { Id = u.Id, Account = u.Account, OrgName = a.Name })
.ToMappedSqlString();
}
}6. 步骤清单:新增一张业务表
- 选基类(默认
EntityTenantBaseData:审计 + 租户 + 数据范围全都有) - 贴
[SugarTable(null, "中文注释")],判断是否要[IncreTable] - 每个字段写
/// <summary>+[SugarColumn(ColumnDescription = "...", Length = N)] - 加
[Required]/[MaxLength]做入参校验(这些特性会被AddXxxInput继承) - 高频查询字段加
[SugarIndex("i_{table}_x", nameof(X), OrderByType.Asc)] - 配
[Navigate]做导航(记得加[JsonIgnore]避免循环引用) - 需要种子数据则写
SeedData/XxxSeedData.cs - 打开
Database.json的EnableInitTable(或EnableIncreTable+[IncreTable]),重启
⚠️ 注意事项 / 常见坑
- 忘记
[SugarTable]= 不建表。InitTable只筛u.IsDefined(typeof(SugarTable), false)。 - 属性写成
string?/long?会自动推断为可空列(见SetDbConfig的EntityService里NullabilityInfoContext),不必重复写IsNullable。 [OwnerUser]/[OwnerOrg]只影响查询过滤,不影响写入;写入由DataExecuting负责。- 更新默认走
IgnoreColumns(true)(忽略 null 列),字段想被显式置空请用UpdateColumns(u => new { u.X }, true)。 - 软删除 ≠ 真删除。
EntityBase的表默认查不到IsDelete=true的行,需要跨租户/跨删除态查询时用ClearFilter()或ClearFilter<ITenantIdFilter>()。 - 枚举列存的是 int,
EnumToNumber: true时 Swagger 也显示数值;字典场景建议用int+[Dict("code")]。 - 视图类必须
[IgnoreTable],否则会被当成表创建。
小结
选基类 → 贴 [SugarTable] → 写 [SugarColumn] → 补 [SugarIndex]/[Navigate] → 开 EnableInitTable 重启。审计字段、雪花 Id、租户、机构过滤全部由框架 AOP 自动完成,业务代码只关心业务字段。