【EF Core】未定义实体类的数据库模型

news/2025/11/16 21:34:21/文章来源:https://www.cnblogs.com/tcjiaan/p/19227431

不知道大伙伴们有没有这样的想法:如果我不定义实体类,那 EF Core 能建模吗?能正常映射数据库吗?能正常增删改查吗?

虽然一般开发场景很少这么干,但有时候,尤其是数据库中的某些视图,就不太想给它定义实体类。好消息,EF Core 还真支持不定义实体类的。可是,你一定会疑惑了,不定义实体类,那还怎么面向对象呢?不急,咱们一个个去探寻真相。

先看看这个自定义的上下文类。

public class MyDbContext : DbContext
{protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlServer("server=...");}protected override void OnModelCreating(ModelBuilder modelBuilder){EntityTypeBuilder ent = modelBuilder.Entity("Student");// 先把它标记为无主键,避免报错
        ent.HasNoKey();}
}

这里我给数据库模型添加了一个叫 Student 的实体,我可没有定义对应的类。然后我们打印一下模型信息,看看能不能建模。

internal class Program
{static void Main(string[] args){using var context = new MyDbContext();// 打印模型信息
        Console.WriteLine(context.Model.ToDebugString());}
}

运行一下,好家伙,你看,还真的能建模了。

Model:EntityType: Student (Dictionary<string, object>) CLR Type: Dictionary<string, object> Keyless

注意 CLR Type 后面的信息。这下懂了,当你不给 EF Core 提供实体类时,它会默认使用字典类型,Key 是字符串,Value 是 object 类型。

 

我们继续验证,既然能建模了,那定义些属性,包括主键。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{EntityTypeBuilder ent = modelBuilder.Entity("Student");// 添加属性ent.Property<int>("StuID");ent.Property<string>("Name").IsRequired(true);ent.Property<int>("Age").IsRequired();ent.Property<string>("Major").IsRequired(false);// 主键ent.HasKey("StuID");
}

Property 方法要指定属性的类型,因为没有对应的 CLR 属性,不然 EF 不知道这个属性是什么类型。这个其实就像影子属性。

现在再看看模型的信息。

Model:EntityType: Student (Dictionary<string, object>) CLR Type: Dictionary<string, object>Properties:StuID (no field, int) Indexer Required PK AfterSave:Throw ValueGenerated.OnAddAge (no field, int) Indexer RequiredMajor (no field, string) IndexerName (no field, string) Indexer RequiredKeys:StuID PK

看来是没问题的。

 

好,进入下一个验证阶段。我们用这个模型去创建数据库(这里我用的是 SQL Server)。

public class MyDbContext : DbContext
{protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlServer("server=(localdb)\\MSSQLLocalDB;database=my_school");}protected override void OnModelCreating(ModelBuilder modelBuilder){……}
}internal class Program
{static void Main(string[] args){using var context = new MyDbContext();……// 创建数据库
        context.Database.EnsureCreated();}
}

运行后,成功创建数据库,包含数据表 Student。

CREATE TABLE [dbo].[Student] ([StuID] INT            IDENTITY (1, 1) NOT NULL,[Age]   INT            NOT NULL,[Major] NVARCHAR (MAX) NULL,[Name]  NVARCHAR (MAX) NOT NULL,CONSTRAINT [PK_Student] PRIMARY KEY CLUSTERED ([StuID] ASC)
);

默认它使用了 Student 为表名。

 

咱们的数据库上下文还少了一个 DbSet<> 属性,为了能访问数据,应当定义此属性。不过,这样定义是错误的。

public class MyDbContext : DbContext
{……public DbSet<Dictionary<string, object>> Students { get; set; }
}

虽然在运行的时候没有抛出异常,但是,Students 属性在 DbContext 初始化时,DbSet<> 没有被正确设置,即内部的 InternalDbSet 类初始化失败。说白了这个 DbSet 类型的属性你不能正常访问。

什么原因?当我们通过 LINQ 查询时,由于 DbSet 本身就是实现了 IQueryable<> 接口的,因此,EntityQueryable 属性会被 IEnumerable.GetEnumerator() 等方法访问。

image

然后我们再看 EntityQueryable 属性的代码。

private EntityQueryable<TEntity> EntityQueryable
{get{CheckState();return NonCapturingLazyInitializer.EnsureInitialized(ref field,this,static internalSet => internalSet.CreateEntityQueryable());}
}

这里触发了CheckState 方法。

private void CheckState()// ReSharper disable once AssignmentIsFullyDiscarded=> _ = EntityType;

CheckState 方法又访问了 EntityType 属性。

public override IEntityType EntityType
{get{if (field != null){return field;}field = _entityTypeName != null? _context.Model.FindEntityType(_entityTypeName): _context.Model.FindEntityType(typeof(TEntity));if (field == null){if (_context.Model.IsShared(typeof(TEntity))){throw new InvalidOperationException(CoreStrings.InvalidSetSharedType(typeof(TEntity).ShortDisplayName()));}……return field;}
}

其他地方不用看,重点是发现中间有一段是判断实体的类型是否为共享类型。IsShared 方法是 Model 类中定义的。

public virtual bool IsShared([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type)=> FindIsSharedConfigurationSource(type) != null|| Configuration?.GetConfigurationType(type) == TypeConfigurationType.SharedTypeEntityType;public virtual ConfigurationSource? FindIsSharedConfigurationSource(Type type)=> _sharedTypes.TryGetValue(type, out var existingTypes) ? existingTypes.ConfigurationSource : null;

_sharedTypes 是 Model 类的字段,从初始化时,它就把 Dictionary<string, object> 设定为共享类型。

    public static readonly Type DefaultPropertyBagType = typeof(Dictionary<string, object>);private readonly Dictionary<Type, (ConfigurationSource ConfigurationSource, SortedSet<EntityType> Types)> _sharedTypes =new() { { DefaultPropertyBagType, (ConfigurationSource.Explicit, new SortedSet<EntityType>(TypeBaseNameComparer.Instance)) } };

看到没,_sharedTypes 字段从 new 那一刻起,它里面就包含了 Dictionary<string, object> 类型。

共享类型的特点是允许多个实体使用同一个 .NET 类,而字典类型就是默认的共享类型。而使用 DbSet<...> Students { get; set; } 这种格式定义的属性,在 DbContext 类的内部,只用实体类的 Type 作为索引的 Key,没有命名。一旦 Type 是多个实体共享的类型,就破坏了唯一性。这时候必须同时用 Type 和 name 来标识 DbSet 才能做到唯一区分。看看 DbContext 类内部的 DbSet 列表是怎么缓存的。

private Dictionary<(Type Type, string? Name), object>? _sets;

说白了,DbContext 类的内部是用一个字典来缓存 DbSet 的(看,字典类型真是好用,哪儿都能用得上它),其中 Key 是由两个值构成的:实体的类型 + 实体的名字。对于常见的实体类,因为类是唯一的,不共享的,所以,Name 的值可以忽略;而共享类型则不同,多个实体的 Type 是相同的,不用 Name 的话无法区分。所以,对于咱们这个未定义实体类的 Student 实体,只能调用 Set 方法来返回,并且要显式地指定一个名字,名字必须和模型中注册的实体名相同,否则,查询的时候还是找不到映射的。

废话了那么多,正确的属性定义应当是这样的。

public DbSet<Dictionary<string, object>> Students => Set<Dictionary<string, object>>("Student");

 咱们把表映射完善一下。

    protected override void OnModelCreating(ModelBuilder modelBuilder){EntityTypeBuilder ent = modelBuilder.Entity("Student");// 添加属性ent.Property<int>("StuID").HasColumnName("f_stuid");ent.Property<string>("Name").IsRequired(true).HasMaxLength(15).HasColumnName("f_name");ent.Property<int>("Age").IsRequired().HasColumnName("f_age");ent.Property<string>("Major").IsRequired(false).HasMaxLength(30).HasColumnName("f_major");// 主键ent.HasKey("StuID").HasName("PK_stu_id");// 表名ent.ToTable("tb_students");}

然后创建的数据库是这样的。

image

逐渐对味了,这一关可以 pass 了。

 

接下来咱们还要验证一下,增删改查是否可行。

A、先插入五条记录。

// 插入数据
// Name:姓名;Age:年龄;Major:专业
Dictionary<string, object>[] newRecs =
[new(){["Name"] = "刘桂圆",["Age"] = 19,["Major"] = "人力资源管理裁员方向"},new(){["Name"] = "方小同",["Age"] = 20,["Major"] = "调酒师"},new(){["Name"] = "程河洛",["Age"] = 20,["Major"] = "兽医"},new(){["Name"] = "史地分",["Age"] = 22,["Major"] = "堪舆学"},new(){["Name"] = "吴胜隆",["Age"] = 18,["Major"] = "行星科学"}
];context.Students.AddRange(newRecs);
// 提交更改
int n = context.SaveChanges();
Console.WriteLine("已更新{0}条数据", n);

结果如下:

image

 

B、现在看一下数据更新。把 ID=3 的同学的专业改为“土狗工程”。

int n = context.Students.Where(stu => (int)stu["StuID"] == 3).ExecuteUpdate(setter => setter.SetProperty(s => s["Major"], "土狗工程"));
Console.WriteLine("更新了{0}条记录", n);

这个写法可能有大伙伴没看懂,老周解释一下。

首先,用 ExecuteUpdate 方法的好处是只生成一条 UPDATE 语句,提高了效率。如果你先查询 ID=3 的数据,然后修改其属性,然后再提交更改。那样 EF 会先生成 SELECT 语句,返回数据后,在内存中修改,再生成 UPDATE 语句,这样不太必要。

ExecuteUpdate 方法生成的 UPDATE 语句,它的 Where 子句是和 LINQ 查询匹配的,由于我们要改 id=3 的记录,所以要先调用 Where 方法,让 EF 记住有筛选条件,然后再调用 ExecuteUpdate 方法生成 SET 子句。

ExecuteUpdate 是扩展方法,这里我调用的以下重载:

public static int ExecuteUpdate<TSource>(this IQueryable<TSource> source, Action<UpdateSettersBuilder<TSource>> setPropertyCalls)

参数是一只委托,委托有个输入参数,类型是 UpdateSettersBuilder<TSource>。UpdateSettersBuilder 类的功能是帮助框架生成更新语句的 SET 子句。即调用它的 SetProperty 方法给实体的属性赋值。为了保持高度的灵活性和可扩展性,SetProperty 方法采用表达树的方式传参。此处,我使用的是以下重载:

public UpdateSettersBuilder<TSource> SetProperty<TProperty>(Expression<Func<TSource, TProperty>> propertyExpression,TProperty valueExpression
)

根据 C# 语句可以隐式转化为 Expression 的原则,propertyExpression 参数可以简化理解为 Func<TSource, TProperty>,即你要告 EF 我要更新实体的哪个属性,比如,我要更新 Car.Speed 属性,那么就是 c => c.Speed,其中,c 是 Car 实例。不过,这里我们的 Student 实体是没有定义类的,它是个字典,但没关系,我们就告诉框架要更新哪个 Key 的值就好了。也就是 s => s["Major"]。

第二个参数 valueExpression 就是属性的新的值。

所以,综合起来,整个 ExecuteUpdate 方法的调用就是

ExecuteUpdate(setter => setter.SetProperty(s => s["Major"], "土狗工程"
));

等等啊,各位,别忙着按【F5】,上面代码还有个问题,如果你直接运行,就会报类型映射失败的错误。问题就出在这个 Lambda 表达式:s => s["Major"]。我们还记得,它的类型是 Dictionary<string, object>,也就是说,s["Major"] 给表达式树处理引擎提供的类型是 object,而 Major 属性其实要的是 string。

怎么解决呢,简单啊,把它强制转换为 string 就完事了。

ExecuteUpdate(setter => setter.SetProperty(s => (string)s["Major"], "土狗工程"
));

现在运行代码就不会出错了。生成的 SQL 语句如下:

 UPDATE [t]SET [t].[f_major] = @pFROM [tb_students] AS [t]WHERE [t].[f_stuid] = 3

咱们验证一下,看到底改了没有。

image

嗯,已经改了。

 

C、删除数据。现在咱们把 ID=3 的数据记录删除。为了提高效率,我们用 ExecuteDelete 方法。

 int n = context.Students.Where(stu => (int)stu["StuID"] == 3).ExecuteDelete();Console.WriteLine("已删除{0}条记录", n);

ExecuteDelete 方法不需要参数,因为生成 DELTE FROM ... 语句一般只要带个 WHERE 子句作为筛选条件就可以了。所以,要先调用 Where 方法做筛选,然后才调用 ExecuteDelete 方法。不然会把整个表的记录删除。

生成的 SQL 语句如下:

DELETE FROM [t]FROM [tb_students] AS [t]WHERE [t].[f_stuid] = 3

运行代码后,ID=3 的记录就没了。

image

 

D、查询数据。就剩下最后一个操作了。咱们把所有记录查询出来,并输出到控制台。

var stuArr = context.Students.ToArray();
// 打印
var q = from s in stuArrselect new{StuID = (int)s["StuID"],Name = (string)s["Name"],Age = (int)s["Age"],Major = (string)s["Major"]};
foreach(var x in q)
{Console.WriteLine($"{x.Name}({x.StuID})\t{x.Age}岁,{x.Major}专业");
}

context.Students.ToArray 由于调用了 ToArray 扩展方法,所以查询会执行,从而触发翻译为 SQL 语句,并发送到数据库,返回查询结果。生成 SQL 如下:

 SELECT [t].[f_stuid], [t].[f_age], [t].[f_major], [t].[f_name]FROM [tb_students] AS [t]

控制台输出结果如下:

刘桂圆(1)     19岁,人力资源管理裁员方向专业
方小同(2)     20岁,调酒师专业
史地分(4)     22岁,堪舆学专业
吴胜隆(5)     18岁,行星科学专业

在查询的时候,你也可以直接用 SQL 语句,例如查询 ID=1 的记录。

int qid = 1;
var stuArr = context.Students.FromSql($"select * from tb_students where f_stuid = {qid}").ToArray();

建议使用这种格式化的方式来组织 SQL 语句,而不用 Raw SQL 来拼接,因为用格式化字符串构建 SQL 语句默认使用参数化查询,可以避免特殊字符注入攻击,保证安全。

 

经过咱们一系列验证,表明不定义类的实体也可以正常完成增删改查操作的。只是使用 Dictionary<string, object> 类型在书写查询的时候,Key 的名称容易写错。尤其是项目你做了一半,让后别人接盘,这种情况很容易把 Key 写错,导致各种错误。

因此,不定义实体类的方案可以使用,但不建议全用,可以仅在部分表或视图的映射中使用。

好了,今天咱们就水到这里了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/967373.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20232325 2025-2026-1 《网络与系统攻防技术》实验五实验报告

网络攻防信息搜集技术实验报告 1.实验内容1.1域名与IP信息搜集分析; 1.2社交网络IP地址获取与地理位置定位; 1.3靶机环境扫描探测、漏洞评估与攻击路径分析; 1.4搜索个人网上足迹与高级搜索技能实践。2.实验目的 学…

12.docker swarm - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

山东大学 计算机图形学实验 二维网格剖分 Catmull-Clark算法

原理可以参考这里 想自己写的记得注意边界条件:度数<=3的顶点不做位置调整,只和一个面相邻的边点直接取边的中点 #include <iostream> #include <fstream> #include <vector> #include <sst…

从Ubuntu安装Harbor故障到了解AppArmor 与 Seccomp的思考

Ⅰ. 冲突:级联故障下的“表象迷惑”与MTTR损失 我们从一次 Harbor 镜像仓库的启动失败开始。在修复了初期由非标准 Unicode 字符导致的 YAML 解析错误后,系统并没有如期启动,反而陷入了更深层次的循环崩溃,最终在H…

鸿蒙应用开发实战:如何从0到1打造创新应用

以「往来记」为例,揭秘创新应用的构思、设计与实现全流程💡 引言:在成熟市场中寻找创新机会 在当今应用市场趋于饱和的环境下,很多开发者都在思考:如何做出真正有创新性的应用? 通过「往来记」这个项目的完整开…

什么是“组态路径”?

什么是“组态路径”?定义: 组态路径(Configuration Path)是指多个条件变量(因素)以特定组合方式共同作用,导致某一结果变量(如高风险感知)出现的因果路径。 📌 通俗解释: 它不是看“哪个因素最重要”,而是…

2025年11月防冻液厂家推荐榜:五家对比与性能评价一览

入冬前,设备管理者、暖通工程商、冷链物流运营方乃至新能源电池厂,都在为同一件事奔忙:把系统里的“血液”换成可靠的防冻液。零下突袭的寒潮、突发的政策抽检、客户对能耗与环保的双重要求,让“选错防冻液”成为停…

深入探索剖析 JVM 的启动过程

你可曾想过:当你在终端里敲下 java,在 main 方法真正运行之前,JVM 为了“创造一个可运行你的程序的宇宙”,到底经历了哪些步骤?从参数校验、系统资源探测,到选择垃圾回收器,再到类的加载、链接与初始化,这些看…

noip8多校2

11.1511.15 t1 赛时想假2.75h,敲了10k(咋做到的?不知道) 曼哈顿距离转切比雪夫距离。 详见 直接出结论: 将每个坐标 \((x,y)\) 变作 \((x+y,x−y)\) 后,原坐标的曼哈顿距离等于新坐标的切比雪夫距离。 证明: 拆式…

2025年11月防冻液厂家推荐排行:五家实力对比与选购指南

立冬之后,北方夜间温度已逼近冰点,南方昼夜温差也迅速拉大,工业循环冷却、空气能热泵、数据中心冷却、冷链物流、冰雪场馆制冰等场景陆续进入“防冻关键期”。大量工程商、设备运维方、物业后勤和经销商开始集中备货…

2025年11月防冻液厂家推荐对比:五家资质与性能全维度排行

进入11月,华北、华东集中供暖启动,西北、东北工业循环冷却系统面临-20℃以下考验,防冻液采购高峰随之到来。用户普遍面临三大痛点:一是低温粘度升高导致泵耗上升,二是缓蚀体系不匹配造成换热器点蚀,三是环保抽查…

2025年11月防冻液厂家推荐榜:五家主流对比与选购指南

入冬前,设备管理者最怕“一夜冻裂”。北方某数据中心去年因防冻液冰点不足,板式换热器爆裂,停机六小时,直接损失超三百万元;南方食品厂用劣质载冷剂,盘管穿孔,整库货物报废。用户此刻上网搜索“防冻液厂家”,核…

2025年11月冷媒剂厂家榜单:五强技术参数与口碑对比评测

如果你正在给新建冷库、数据中心或新能源电池产线挑选冷媒剂,大概率会被三个问题困住:一是“无腐蚀”口号满天飞,却拿不出第三方检测报告;二是低温型号不少,能在零下六十度仍保持低粘度的寥寥无几;三是厂家都说自…

2025年11月载冷剂厂家推荐榜:技术资质与口碑综合评测

进入2025年第四季度,冷链、数据中心、新能源电池、冰雪运动等温控场景对载冷剂的需求持续升温。用户普遍面临“配方不透明、腐蚀风险高、低温流动性差、环保认证缺失”四大痛点,同时希望一次性锁定“资质齐全、技术迭…

一对一 WebRTC 视频聊天

“最小可运行”的一对一 WebRTC 视频聊天示例(前端+信令服务器),完全按照你给出的 8 条信令流程图实现。 技术栈:前端:原生 JavaScript + socket.io-client 信令服务器:Node.js + socket.io + Express(80 端口…

2025年11月载冷剂厂家推荐榜:五强真实数据与场景化选型指南

进入2025年冬季,北方冷库、数据中心、新能源电池产线陆续进入满负荷运行,载冷剂作为二次冷媒的“血液”,其稳定性直接决定系统能耗与设备寿命。很多工程师在招标阶段发现:同样标注“食品级”或“低电导”的产品,现…

2025年11月载冷剂厂家榜单:性能参数与口碑综合评测

进入2025年第四季度,北方集中供暖启动、冷链物流旺季叠加新能源电池扩产,载冷剂采购需求集中爆发。很多项目工程师面临“三快一严”场景:快速选型、快速比价、快速交付,同时环保与安全审查趋严。用户普遍担心三点:…

20232313 2025-2026-1 《网络与系统攻防技术》实验五实验报告 - 20232313

1.实验内容基本实验内容如下:学会使用msf编码器,veil-evasion,自己利用shellcode编程等免杀工具或技巧 正确使用msf编码器,使用msfvenom生成如jar之类的其他文件 veil,加壳工具 使用C + shellcode编程 通过组合应…

【第7章 I/O编程与异常】Python文件操作与上下文管理器的深度解析(避坑指南)

从 C 到 Python:文件操作与上下文管理器的深度解析(避坑指南) 对于习惯了 C 语言手动管理资源的学习者,Python 的文件操作和上下文管理器常常带来认知混淆:为什么 C 必须手动 fopen/fclose,而 Python 能用 with …

2025年11月乙二醇厂家对比榜:五家主流厂商真实数据与选型要点

进入2025年第四季度,国内工业温控系统进入年度维保与新建项目并行的高峰期,乙二醇作为防冻液母液、载冷剂溶剂及工业传热介质的核心原料,采购量显著抬升。国家能源局11月最新统计显示,工业温控领域乙二醇月需求环比…