核心需求:业务表不改动结构,前端可以动态新增字段、录入、查询、导出,常见 3 种方案,按落地复杂度、性能、查询需求选型:
适用:客户要自定义属性、商品扩展属性、档案自定义字段、表单自定义字段等场景
方案对比
表格
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| EAV 实体 – 属性 – 值 | 主表 + 扩展字段定义表 + 扩展值表 | 标准范式,结构干净,支持字段类型、校验、枚举;易权限控制 | 联表多,复杂查询慢;聚合统计麻烦 | 企业 CRM、档案、客户、订单,字段数量不多,查询不算极端复杂 |
| JSON 字段(推荐大多数 .NET 项目) | 主表加一个 ExtensionData JSONB/JSON 字段,存键值对象 | 开发最快,单表查询,EF Core 原生支持;查询简单 | 无法数据库层面加索引(部分数据库支持 JSON 索引);字段元数据要单独存;约束弱 | 中小系统、自定义字段数量不稳定、快速迭代,最常用 |
| 动态建表(不推荐) | 运行时 ALTER TABLE 新增列 | 查询性能最好,原生字段 | DDL 锁表、迁移麻烦、版本控制灾难、多租户冲突 | 极少用,不建议业务系统首选 |
工业级项目优先:JSON 字段 + 独立元数据表(兼顾开发效率和字段管理:类型、标签、是否必填、下拉选项、排序)
方案一:JSON 扩展字段(EF Core 最简落地)
1. 两张核心表
- 元数据表:CustomField(定义字段) 用来管理这个业务类型有哪些扩展字段:字段名、显示名、类型(文本 / 数字 / 日期 / 下拉)、是否必填、下拉选项、归属业务类型(比如
Customer、Order)
public class CustomField
{
public long Id { get; set; }
/// <summary>归属业务对象:Customer / Order / Product</summary>
public string BizType { get; set; }
/// <summary>前端key,英文唯一标识</summary>
public string FieldKey { get; set; }
/// <summary>显示名称</summary>
public string Label { get; set; }
/// <summary>类型:Text/Number/Date/Select/Bool</summary>
public string FieldType { get; set; }
public bool Required { get; set; }
/// <summary>下拉选项JSON</summary>
public string? Options { get; set; }
public int Sort { get; set; }
}
- 业务主表,增加 JSON 扩展字段
public class Customer
{
public long Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
// 扩展字段,EF Core 映射为JSON
public Dictionary<string, object> ExtensionData { get; set; } = new();
}
2. EF Core 配置(不同数据库)
PostgreSQL(JSONB,支持索引,首选)
modelBuilder.Entity<Customer>()
.Property(p => p.ExtensionData)
.HasColumnType("jsonb");
PG 可以给 jsonb 建索引,支持 @>、? 等查询。
MySQL
modelBuilder.Entity<Customer>()
.Property(p => p.ExtensionData)
.HasColumnType("json");
SQL Server
modelBuilder.Entity<Customer>()
.Property(p => p.ExtensionData)
.HasColumnType("nvarchar(max)");
SQL Server 2016+ 支持
JSON_VALUE函数查询。
3. 写入示例
var customer = new Customer
{
Name = "张三",
ExtensionData = new Dictionary<string, object>
{
["factory"] = "昆明加工厂",
["contractNo"] = "HT20261001",
["deliveryDate"] = DateTime.Now
}
};
db.Customers.Add(customer);
await db.SaveChangesAsync();
4. 查询扩展字段(重点)
方式 A:内存过滤(简单场景,数据量大不推荐)
var list = db.Customers.AsEnumerable()
.Where(x => x.ExtensionData.TryGetValue("factory", out var v) && v.ToString() == "昆明加工厂")
.ToList();
方式 B:数据库原生 JSON 函数(推荐,可走索引)
SQL Server:
SELECT * FROM Customer WHERE JSON_VALUE(ExtensionData, '$.factory') = '昆明加工厂'
PG:
SELECT * FROM Customer WHERE ExtensionData @> '{"factory":"昆明加工厂"}'
EF Core 可以封装扩展方法,拼接 JSON 查询表达式。
5. 前端联动逻辑
- 前端先根据
BizType查询CustomField拿到字段定义,动态渲染表单 - 提交时把动态表单的值打包进
ExtensionData - 回显:主表数据 + 字段定义,循环渲染
6. 缺点 & 坑
- 数据库无法做外键、原生唯一约束;校验逻辑放 C# 代码层
- 排序、复杂统计如果基于扩展字段,性能差
- 字段删除后,历史数据 JSON 里脏 key 需要清理
方案二:EAV 实体属性值表(规范方案,适合强自定义、需要统计筛选)
三张表:
CustomField字段定义(同上)CustomFieldValue字段值表
public class CustomFieldValue
{
public long Id { get; set; }
public long BizId { get; set; } // 主业务Id,CustomerId
public string BizType { get; set; } // 业务类型
public long FieldId { get; set; } // 关联CustomField
// 统一字符串存储,读取时按字段类型转换
public string Value { get; set; }
}
✅ 优点:
- 可以给 FieldId、BizId 建索引,筛选条件稳定
- 可以做字段级权限、字段审计、单独字段历史变更日志
- 报表、筛选条件可以统一封装查询组件
❌ 缺点:
- 查一条主数据需要左联 N 行扩展值,数据量大时性能下滑
- 分页、排序、多条件组合查询写起来繁琐
优化:查列表时,先查主表,再批量查扩展值,内存组装(减少多次循环查询)
EAV 查询示例思路:
// 1. 分页查客户主表
var customers = db.Customers.Skip(0).Take(20).ToList();
var ids = customers.Select(x=>x.Id).ToList();
// 2. 批量查该批客户的所有扩展字段值
var fieldValues = db.CustomFieldValues
.Where(v => v.BizType == "Customer" && ids.Contains(v.BizId))
.ToList();
// 3. 分组组装到客户对象
很多 CRM、低代码平台底层就是 EAV 模型。
方案三:混合方案(生产最稳,推荐商用项目)
元数据单独表 CustomField + 业务数据用 JSON 存储
- 字段定义、类型、校验、下拉、权限走
CustomField - 数据值存在主表 JSON
- 高频筛选的扩展字段:同步冗余到主表独立列(双写)
举例:客户有一个高频筛选字段
Source(客户来源),虽然是用户自定义的,但是经常用来筛选统计,就新增独立字符串字段,新增 / 修改时同步写 JSON + 独立列,兼顾灵活和性能。
通用配套能力(业务必做)
- 字段类型校验:根据
FieldType对传入值做类型转换、必填校验、长度校验 - 字段版本 / 禁用:字段不要物理删除,加状态,历史数据保留
- 导出支持:导出时,读取
CustomField定义,把扩展字段动态追加到 Excel 列 - 权限控制:不同角色是否可见、可编辑某个扩展字段
- 多租户场景:CustomField 增加 TenantId,租户之间字段隔离
- 查询封装:封装统一查询条件解析器,前端传
fieldKey,op,value,自动翻译成 JSON 查询或者 EAV 查询
架构选型建议
- 内部管理系统、CRM、档案,字段不多、筛选不极端 → JSON 方案(开发快)
- 专门低代码平台、大量自定义字段、高频按扩展字段筛选报表 → EAV
- 客户需求不稳定,同时部分字段要高频筛选 → 混合方案(JSON + 冗余高频列)
常见坑提醒
- ❌ 不要直接用字符串拼接 SQL 做 JSON 查询,防注入
- ❌ 不要让前端随便传入任意 key,必须校验
FieldKey存在于 CustomField - ❌ EAV 不要不加索引,
BizType + BizId联合索引是必须的 - ❌ 动态 ALTER TABLE 运行时加列,上线、迁移、多租户极易翻车