从数据库行生成C#类的实现方案与优化问询
问题描述
我有若干C#类,需要将其成员以单独行的形式存储到数据库中,示例类结构如下:
public class Root { public Prius Prius { get; set; } } public class Prius { public int SteeringWheel { get; set; } public Wheels Wheels { get; set; } public Power Power { get; set; } } public class Wheels { public int Front { get; set; } public int Rear { get; set; } } public class Power { public Gasoline Gasoline { get; set; } public Battery Battery { get; set; } } public class Gasoline { public int Capacity { get; set; } } public class Battery { public int Size { get; set; } }
硬性要求
- 单个值需存储在数据库中,并附带额外列;
- 键存在多个版本,查询时仅获取每个键的最新版本条目;
- 仅执行读取操作,应用不向数据库写回数据。
我计划将数据存储为如下表结构:
| Key | Value | Version | Metadata col 1 | More columns... |
|---|---|---|---|---|
| Prius:SteeringWheel | 0 | 1 | test | .... |
| Prius:SteeringWheel | 1 | 2 | test | .... |
| Prius:Wheels:Front | 2 | 1 | test | .... |
| Prius:Wheels:Rear | 2 | 1 | test | .... |
| ...remaining rows | ... | ... | ... | .... |
我计划通过以下步骤从这些值生成C#对象:
- 使用EF Core一次性读取所有值;
- 生成对应的C#对象,之后通过构造函数参数传递给应用中的其他类。
现提出两个问题:
- 基于该方案,如何利用C#/.NET现有特性以最少代码实现?
- 该方案是否为最优选择?若否,请给出替代方案建议。
补充说明
- 已修正重复的wheels属性;
- 希望避免创建EF实体,以便未来增删属性时仅更新代码而无需修改数据库架构,且仅读取键的最新版本。
回答
问题1:最少代码实现方案
1. 读取最新版本数据
利用EF Core的无实体查询能力,直接通过SQL获取每个键的最新版本数据,无需注册EF实体:
// 自定义匿名类型接收查询结果(包含需要的字段) var latestEntries = context.Database.SqlQuery<(string Key, string Value, string MetadataCol1)>(@" SELECT t.Key, t.Value, t.[Metadata col 1] FROM YourTableName t INNER JOIN ( SELECT Key, MAX(Version) AS MaxVersion FROM YourTableName GROUP BY Key ) latest ON t.Key = latest.Key AND t.Version = latest.MaxVersion ").ToList();
2. 映射为C#对象
推荐用JSON中转的方式,依赖System.Text.Json的序列化能力,无需手写复杂反射逻辑:
// 第一步:将键值对转换为嵌套字典 var nestedDict = new Dictionary<string, object>(); foreach (var entry in latestEntries) { var keySegments = entry.Key.Split(':'); var currentLevel = nestedDict; // 逐层构建嵌套字典 for (int i = 0; i < keySegments.Length - 1; i++) { if (!currentLevel.ContainsKey(keySegments[i])) currentLevel[keySegments[i]] = new Dictionary<string, object>(); currentLevel = (Dictionary<string, object>)currentLevel[keySegments[i]]; } // 转换值类型(匹配目标属性类型) var targetType = GetTargetPropertyType(typeof(Root), keySegments); currentLevel[keySegments.Last()] = Convert.ChangeType(entry.Value, targetType); } // 第二步:序列化为JSON后反序列化为目标对象 var json = JsonSerializer.Serialize(nestedDict); var root = JsonSerializer.Deserialize<Root>(json); // 辅助方法:递归获取目标属性的类型 private Type GetTargetPropertyType(Type currentType, string[] keySegments) { foreach (var segment in keySegments) { var prop = currentType.GetProperty(segment); currentType = prop.PropertyType; } return currentType; }
这种方式代码量极少,且利用.NET原生序列化能力,避免了手动反射的维护成本。
问题2:方案是否最优?替代建议
现有方案的优缺点
优点
- 数据库架构完全灵活:增删C#类属性无需修改表结构,仅需调整代码;
- 单属性版本追溯方便:可单独查看某个属性的历史版本。
缺点
- 查询效率低:每次需聚合关联获取最新版本,数据量大时性能瓶颈明显;
- 类型安全风险:
Value存储为字符串,类型转换需额外处理错误; - 映射复杂度高:嵌套结构的键拆分、类型转换逻辑随类结构复杂度上升。
替代方案建议
方案1:单表存储完整JSON(带版本)
将整个Root对象序列化为JSON字符串,存储到带版本的表中:
| Id | Version | ContentJson | Metadata col 1 | More columns... |
|---|---|---|---|---|
| 1 | 1 | {"Prius":{"SteeringWheel":0,...}} | test | .... |
| 1 | 2 | {"Prius":{"SteeringWheel":1,...}} | test | .... |
优势
- 映射代码极简:直接反序列化JSON为对象,无需处理嵌套键;
- 查询更高效:按
Id和最大Version查询即可,无需复杂关联; - 类型安全:序列化/反序列化过程自动处理类型校验。
适用场景
- 无需单独查询单个属性值,仅需加载整个对象;
- 类结构整体变动不频繁,或变动时无需单独追踪单个属性版本。
方案2:使用文档型数据库(如MongoDB)
如果允许切换数据库,文档型数据库天然适配嵌套结构:
- 直接将
Root对象作为文档存储,每个文档带版本号; - 查询时过滤最新版本文档,直接反序列化为C#对象。
优势
- 完全匹配C#类的嵌套结构,无需手动拆分键值;
- 原生支持复杂对象序列化,映射代码几乎为零;
- 版本管理更灵活,可通过数据库自带的版本控制功能实现。
方案3:优化键值存储的查询性能
如果必须保留键值结构,可做以下优化:
- 给
(Key, Version)建立复合索引,提升最大版本查询的速度; - 用Dapper替代EF Core,无实体查询更轻量,代码更简洁:
var latestEntries = connection.Query<(string Key, string Value)>(@" SELECT t.Key, t.Value FROM YourTableName t WHERE t.Version = ( SELECT MAX(Version) FROM YourTableName WHERE Key = t.Key ) ").ToList();
内容的提问来源于stack exchange,提问作者Vishal
相关产品推荐
相关产品推荐

