简介:EF Core 3.1 连接达梦8数据库的示例项目,面向需要在 .NET Core 项目中接入国产数据库的开发者。压缩包内含完整的 ConsoleApp1 控制台解决方案,通过 Program.cs 与 Class1.cs 演示 DbContext 配置、UseDAMENG 连接字符串设置以及基础的增删改查操作,可以直接对照移植或在此基础上扩展业务逻辑。压缩包共 83 个文件,约 1.79MB,包含 7 个 C# 源文件、33 个运行库 dll、8 个 JSON 配置以及编译生成的 exe/pdb 等,结构就是 VS 常见的解决方案布局,便于按 bin/obj 等目录快速定位代码与输出。已有 179 人浏览学习,适合正在接触 EF Core 与达梦8 适配、需要一份可运行范例来验证配置与数据访问环节的开发者,省去从零摸索驱动版本和连接参数的时间。
1. EFCore 3.1 连接达梦 8:这套连接方案能让老项目少走三周弯路
EFCore 3.1 连接达梦 8 数据库,说白了就是让 .NET Core 3.1 的旧项目能用上国产数据库。很多团队卡在这条路上,本质问题是驱动选型不一致:有人用了官方驱动配不通,有人用了 MySQL 兼容协议结果迁移脚本全是坑。达梦 8 自带一套基于 Oracle 兼容模式的 SQL 方言,但 EFCore 3.1 默认的 SqlServer 或 MySql 驱动根本没法和它直接对话。这篇笔记要把我实际验证过的方案讲清楚——从驱动选型、连接串配置、DbContext 注册,到建表、增删改查和分页,最后落到 5 条高频踩坑记录。适合正在做信创适配、老系统国产化迁移、或者是新项目被迫选达梦的 .NET 开发者。
2. 驱动选型和环境准备:为什么不是所有人都能直接连上达梦 8
2.1 达梦 8 的驱动生态到底有哪几条路
达梦 8 对外提供的 .NET 驱动,最常见的有两类:一类是官方自带的 DmProvider(基于 ADO.NET 的托管驱动),另一类是走达梦的 ODBC 或 OLE DB 接口。与 EFCore 3.1 直接相关的,是 DmProvider 对应的 EFCore 适配层。注意一点:达梦 8 的驱动包在 NuGet 上的命名和版本,跟你本机的达梦版本不完全是一回事,官方经常把驱动和客户端工具打包在一起发布,版本号像8.1.x这样,但 NuGet 上的包名可能写的是Dm.DmProvider。
除了官方驱动,还有一条很多人在用的路:把达梦 8 配置成 MySQL 兼容模式或者 Oracle 兼容模式,然后用对应的 EFCore 第三方驱动(比如 Pomelo.EntityFrameworkCore.MySql)去连。这条路在早期达梦驱动不成熟时确实能跑,但代价是你得先改达梦实例的兼容参数,而且建表语法、自增列行为、索引命名全都会被兼容层扭曲一遍。我的建议是:能上官方驱动就上官方驱动,兼容模式只当你实在搞不到官方包版本时才考虑。
2.2 达梦 8 的 EFCore 3.1 驱动包去哪找
先说最干净的路径:达梦官方提供的 NuGet 包。常见做法是打开项目右键“管理 NuGet 程序包”,搜索Dm.DmProvider或者直接搜DmProvider。如果你搜不到,说明你的 NuGet 源没有包含达梦的官方源,这时候有两种处理方式:一种是把达梦安装目录下drivers文件夹里的.dll手动引用到项目里;另一种是去达梦官网下载对应的 .NET 驱动安装包,装完后在安装目录里能找到DmProvider.dll和Dm.DmProvider.dll这一组文件。
这里要特别提醒:EFCore 3.1 对应的驱动版本,需要的是针对 .NET Standard 2.0 编译的 dll。你如果在达梦安装目录里看到的是net40或net45子目录下的老驱动,那大概率是给 .NET Framework 用的,不能直接拿到 .NET Core 3.1 项目里引用。正确的子目录应该是netstandard2.0。我一般会先拷贝 DmProvider 相关 dll 到项目的libs文件夹,再通过引用添加,这样版本可控。
2.3 没有安装达梦客户端,驱动能不能单独工作
这是一个高频疑问。答案是:能,但有前提。EFCore 3.1 连接达梦 8 时,运行时真正需要的是达梦的客户端组件——DmClient相关 dll 以及它依赖的原生库。如果你在开发机上已经安装了达梦 8 的客户端或服务端,那驱动 dll 会从安装目录的bin里自动加载。但如果你只想在一台干净的 CI 机器或容器里跑连接,就得把dmserver(或达梦的通讯库,比如dmoci.dll、DmSdk里的某些原生库)也一起部署过去。
我踩过的坑是:本地能连,发布到 Linux 容器里就连不上,报错是找不到达梦的某个原生库。后来发现达梦驱动在 Linux 下依赖libdmclient.so这类原生文件,光拷贝托管 dll 是不够的。解决方案是把达梦安装目录下bin里的相关.so文件打包进去,并对好LD_LIBRARY_PATH。如果你不确定具体依赖哪些,可以先在容器里执行ldd DmProvider.dll(或者 Mono 的对应工具)查依赖链,缺什么补什么。
3. 从零搭一个能跑的 EFCore 3.1 达梦 8 工程:连接串与 DbContext 配置
3.1 最小工程文件:包引用和项目结构
假设你现在有一个 .NET Core 3.1 的类库项目(或者直接就是 Web API),要接入达梦 8。第一步是确认项目文件里的目标框架和依赖。手工编辑 csproj 比在 VS 界面里点来点去更直观,下面是一个最小配置:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.32" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.32" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.32"> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> <ItemGroup> <Reference Include="DmProvider"> <HintPath>libs\DmProvider.dll</HintPath> </Reference> <Reference Include="Dm.DmProvider"> <HintPath>libs\Dm.DmProvider.dll</HintPath> </Reference> </ItemGroup> </Project>逻辑说明:EFCore 3.1 的包版本要锁定在 3.1.x 的最后一个补丁版本,不要用 3.1.0 这种过老版本,因为后面修了很多和事务、查询翻译相关的 bug。DmProvider和Dm.DmProvider是达梦官方驱动,我这里假设你把 dll 放到了libs目录。如果你走 NuGet 包安装,只需要保留PackageReference那一组即可。
参数说明:HintPath里的路径是相对的,确保你的 dll 文件真实存在,否则编译能过但运行时会报FileNotFoundException。同时建议把 dll 的“复制本地”属性设为 true(csproj 里默认引用本地的 dll 会自动复制到输出目录)。
3.2 连接串怎么写才稳:服务名、端口、字符集
达梦 8 的连接串,长得像下面这样:
private const string DmConnectionString = "Server=127.0.0.1;Port=5236;UserId=SYSDBA;Password=******;Schema=TESTDB;";逻辑说明:Server是达梦服务的 IP,Port默认是 5236(和 Oracle 的 1521 不一样,别搞混)。UserId达梦默认有SYSDBA这个超级账号,但生产环境千万别直接用,要建业务账号。Schema参数对应的是达梦的模式名,不是数据库名,这个非常容易让新手困惑——达梦里一个实例可以建多个表空间,每个表空间下有多个 schema,EFCore 建表时会把表建到当前登录用户对应的 schema 下,或者是你指定的这个 schema 下。
参数说明:如果你不写Schema,达梦默认会用用户名作为 schema 名,结果就是你可能在SYSDBA模式下建表,后面连接串换了普通用户却查不到表。所以连接串里写Schema=是个好习惯。另外字符集这块,达梦 8 默认可能不是 UTF-8,如果你的项目要存中文,建议连接串里加Encoding=utf-8,否则插入中文乱码的概率极高。
3.3 DbContext 注册:UseDm 方法从哪里来
正常情况下,如果官方驱动自带了 EFCore 适配,你在 DbContext 的OnConfiguring里应该能看到一个UseDm扩展方法。但实际中,很多版本的 DmProvider 没有直接提供UseDm,这时候就需要你自己写一个扩展方法,把达梦的数据库工厂接进 EFCore。
下面是我用过的可行方案:
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using System; public static class DmDbContextOptionsExtensions { public static DbContextOptionsBuilder UseDm( this DbContextOptionsBuilder optionsBuilder, string connectionString) { if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException(nameof(connectionString)); optionsBuilder.UseRelational(connectionString, "Dm"); return optionsBuilder; } }逻辑说明:这个扩展方法的核心是利用 EFCore 3.1 的UseRelational基础能力,把连接串注册进去,并声明这是Dm类型的数据库。EFCore 会根据UseRelational的配置去调用 ADO.NET 层的DmConnection来自动完成解析。注意,这只是让 EFCore 认识达梦的连接串和驱动工厂,不代表所有的 SQL 翻译都正确。达梦的 SQL 方言和 Oracle 有七分像,和 SQL Server 完全不同,这意味着一些 EFCore 的查询表达式在达梦上可能翻译不出来。
参数说明:UseRelational是Microsoft.EntityFrameworkCore.Relational包提供的扩展,你的项目必须引用这个包。字符串参数里的"Dm"是 provider 的名字,只用于内部标识,具体是什么值不影响运行,但建议写成Dm方便排查。
3.4 连接工厂:让 EFCore 能找到达梦的 DbConnection
光有UseDm是不够的,还需要告诉 EFCore 用哪个具体的DbConnection实现。达梦驱动的连接对象是DmConnection(在DmProvider命名空间下)。通过 EFCore 的RelationalConnection机制,可以这样注册:
using Microsoft.Data.SqlClient; // 这里其实不是 SqlClient,只是占位 using System.Data.Common; using DmProvider; // 如果你的 dll 里是这个命名空间 public class DmConnectionFactory : IDbContextTransactionManager { // 这个类只是示例骨架,实际要更完整 }但说实话,这个步骤在实践中最容易出岔子。常见做法是:不要去碰底层的连接工厂,而是在OnConfiguring里直接加一行:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var connection = new DmConnection(connectionString); optionsBuilder.UseDm(connection); }逻辑说明:UseDm的重载接收一个DbConnection实例,EFCore 会直接把你的DmConnection当作内部连接使用,跳过驱动工厂检测那一层,这在驱动没有实现完整IDbProviderFactory的情况下是最稳的路径。代价是每次 DbContext 实例化时都会 new 一个连接,如果你用依赖注入和连接池,这块性能影响不大,因为达梦驱动本身会自己维护连接池。
参数说明:DmConnection的构造函数只接收连接串字符串,报错信息一般会直接说“无效的连接串参数”,这时候优先检查Port写没写、Schema大小写对不对。
4. 增删改查与自动建表:让 EFCore 3.1 和达梦 8 顺畅对话的核心命令
4.1 实体类设计的边界:哪些 C# 类型能安全映射
在设计实体类时,有一些类型映射到达梦 8 会有问题。比如decimal或者double,如果属性精度没指定,达梦默认生成NUMERIC(18, 0),意味着小数全被截断。这是最典型的坑。
建议在实体上强制指定精度:
public class Product { public int Id { get; set; } [Column(TypeName = "NUMERIC(18, 2)")] public decimal Price { get; set; } [Column(TypeName = "VARCHAR2(50)")] public string Name { get; set; } public DateTime CreateTime { get; set; } }逻辑说明:[Column(TypeName = "VARCHAR2(50)")]会让 EFCore 建表时直接生成VARCHAR2(50),而不是达梦里的VARCHAR(虽然 Varchar 和 Varchar2 在达梦里几乎等价,但显式指定会让迁移脚本更可预测)。NUMERIC(18, 2)对应 C# 的decimal,金额和单价这种必须精确到小数的字段一定要这么写。datetime类型达梦默认支持,不需要特殊处理,但要注意时区——达梦的SYSDATE走的是服务器本地时间,如果你程序里用DateTime.Now,两端的时间差可能导致数据对不上。
参数说明:CreateTime不指定类型时,EFCore 3.1 会翻译成TIMESTAMP而不是DATETIME,这本身没问题,但如果你后续用原生 SQL 比较日期,要注意达梦对TIMESTAMP和DATETIME的隐式转换规则不同。
4.2 自动建表:Migration 能不能直接用在达梦上
EFCore 3.1 自带迁移机制,但达梦的兼容性不完美。我第一次尝试时,直接运行dotnet ef migrations add Init然后dotnet ef database update,结果报错说达梦不支持某些语句。原因是达梦的建表脚本里,EFCore 生成的CREATE TABLE语句包含[Id]这种带方括号的标识符——这是 SQL Server 风格,达梦不认。
解决办法是取消全局标识符包裹。在 DbContext 里这样配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>(entity => { entity.ToTable("PRODUCT", "TESTDB"); entity.HasKey(e => e.Id); entity.Property(e => e.Id).HasColumnName("ID").ValueGeneratedOnAdd().UseIdentityColumn(); }); }逻辑说明:ToTable("PRODUCT", "TESTDB")强制把表建到TESTDB这个 schema 下。UseIdentityColumn()是告诉 EFCore 使用自增列,达梦 8 支持IDENTITY自增,但前提是该列的写法要符合达梦语法。如果生成的迁移脚本仍然用方括号,可以写一个自定义的IMigrationsSqlGenerator把方括号替换成双引号——但这个任务比较繁琐,我的建议是:看生成的迁移脚本,把像是[、]、N'这类字符手动修正。
参数说明:ValueGeneratedOnAdd()表示该列由数据库生成值,配合UseIdentityColumn()后,EFCore 会在插入时忽略该列的值。如果你不写这个,直接插入Id=0可能导致主键冲突。
4.3 增删改查代码:事务和批量操作的注意点
基础 CRUD 代码和 SQL Server 下几乎一样,关键差异在事务和批量更新上。下面是一个能直接用的示例:
using (var db = new DmDbContext()) { db.Products.Add(new Product { Name = "测试商品", Price = 19.99m, CreateTime = DateTime.Now }); db.SaveChanges(); var list = db.Products.Where(p => p.Price > 10m).OrderByDescending(p => p.CreateTime) .Skip(0).Take(20).ToList(); var target = db.Products.FirstOrDefault(p => p.Id == 1); if (target != null) { target.Price = 29.99m; db.SaveChanges(); } }逻辑说明:这段代码里有三个关键操作:插入、分页查询、更新。Skip/Take在达梦上的翻译结果不是LIMIT,而是TOP或者ROWNUM相关语法,但 EFCore 3.1 的达梦驱动如果正确,会自动处理。但要注意:FirstOrDefault翻译成达梦方言后,如果表里有多条匹配数据,结果可能不是你预期的那条,建议排序后取第一个。
参数说明:db.Products.Add后SaveChanges会立即执行 INSERT 语句,达梦默认事务隔离级别是读已提交(READ COMMITTED),如果你在事务里先查再改别的事务看不到未提交的数据,这在并发比较高的场景下偶尔会引发困惑。
4.4 手动 SQL 和视图映射:EFCore 里执行原生 SQL
如果你需要调存储过程,或者跑一段复杂的原生 SQL,EFCore 3.1 提供了FromSqlRaw。达梦 8 的存储过程和 Oracle 风格接近,用参数化查询时要特别注意参数名前缀。
var result = db.Products.FromSqlRaw("SELECT * FROM TESTDB.PRODUCT WHERE PRICE > {0}", priceValue).ToList();逻辑说明:FromSqlRaw的第一个参数是原生 SQL,大括号里的{0}是参数占位符,对应priceValue。这比字符串拼接安全,能防注入。注意 SQL 里表名要把 schema 带上前缀,否则达梦可能去当前用户默认 schema 里找表。这条路径不会走 EFCore 的查询翻译管道,所以 SQL 写错直接暴雷,不会给你任何类型转换的缓冲。
参数说明:如果你的 SQL 里需要传多个参数,用{0}、{1}依次排下去。EFCore 会把它们转换成达梦的参数对象,参数名是p0、p1,你在 SQL 里不要试图用:p0去引用,只用占位符就行。
5. 连接达梦 8 的 5 个高频坑:现象、原因、处理办法
5.1 连接串里 Schema 写错导致表找不到
现象:代码跑起来报ORA-00942(表或视图不存在),但你去数据库管理工具里手动执行查询,表确实存在。原因:EFCore 连接串里写的Schema名和实际建表时的 schema 不一致,或者根本没有写 Schema,达梦默认落到了登录用户的 schema 下。解决:把连接串里的Schema=参数和ToTable里写的 schema 统一,遵守一个原则——所有 SQL 都显式带上 schema 前缀。我一般在连接串里写Schema=TESTDB,同时实体映射里也用TESTDB,两边完全对齐,问题立刻消失。
5.2 中文乱码:从入库就错了,不是显示层问题
现象:从达梦里查出来的中文显示成??,管理工具里看也是乱码。原因:达梦实例的字符集不是 UTF-8,连接串里也没指定Encoding=utf-8,导致客户端和服务器用不同的编码解释字符串。解决:连接串里加Encoding=utf-8,同时在建库时把达梦的字符集选成UTF-8。如果已经建库了,改库字符集很麻烦,我建议是让 DBA 确认实例的CHARSET,如果是GB18030,你的连接串里就不要写 utf-8,写成Encoding=GB18030才匹配。
5.3 自增列插入后拿不到 Id
现象:SaveChanges()执行成功,但实体的Id属性还是 0。原因:达梦的IDENTITY列如果没有用达梦的序列机制,EFCore 可能不知道如何回填主键。解决:在OnModelCreating里显式配置.UseIdentityColumn(),并且在实体类的Id属性上不要自己赋值。如果达梦 8 的版本在IDENTITY支持上不完整,可以考虑改用达梦的序列(SEQUENCE)+BeforeInsert触发器方案。但优先检查你的达梦版本是不是真的支持IDENTITY——8.1.1.46 以上的版本才比较稳。
5.4 迁移生成的 SQL 带方括号,达梦直接报语法错误
现象:dotnet ef migrations add能过,但database update时报Syntax error,细看 SQL 发现全是[TableName]。原因:EFCore 3.1 默认的 SqlServer 迁移代码生成器会产出 SQL Server 风格标识符,达梦不兼容。解决:不用迁移,改用EnsureCreated()在建库时自动建表,或者手动把迁移脚本里的方括号替换成双引号再执行。EnsureCreated()和Migration是两个路线,选一个一直用,别混着用,否则表结构对不上。
5.5 批量更新报“命令参数过多”或性能极差
现象:用db.Products.UpdateRange(list)更新 500 条数据,耗时十几秒,有的版本直接报参数个数超限。原因:EFCore 3.1 在达梦上走的是逐条 UPDATE 语句执行,加上达梦驱动的参数块大小默认比较小(比如 800 个左右),很快触顶。解决:拆分批次,每批 100 条,循环SaveChanges前先Clear(),或者直接改用原生 SQLExecuteSqlRaw拼参数批量 UPDATE。我一般会写一个通用的分批执行方法,根据列表长度按 100 切块,既避免参数爆炸,又把事务窗口控制在合理范围。
6. 进阶:达梦 8 的分页优化与执行计划排查技巧
达梦 8 的默认分页翻译在数据量超过十万行时性能会明显下降。EFCore 3.1 生成的Skip/Take自带一种子查询包裹的写法,但这个写法在达梦上可能没有利用到索引,导致全表扫描。我的习惯是:针对大表不直接使用Skip/Take,改成手动写分页 SQL,走达梦的ROWNUM或者FETCH FIRST限制语法。
var sql = @"SELECT * FROM ( SELECT T.*, ROWNUM RN FROM ( SELECT * FROM TESTDB.PRODUCT ORDER BY CREATE_TIME DESC ) T WHERE ROWNUM <= {1} ) WHERE RN > {0}"; var result = db.Products.FromSqlRaw(sql, offset, offset + pageSize).ToList();逻辑说明:这段 SQL 利用达梦的ROWNUM做两层限制,先从内层子查询取出前offset + pageSize行,再在外层过滤掉前offset行。注意内层子查询的ROWNUM一定要在排序的下一层使用,否则达梦会在排序之前截断数据,分页就错位了。执行计划上,如果数据量再大,建议让 DBA 在CREATE_TIME上加索引,否则排序那一步就是瓶颈。
在排查一个查询为什么慢时,我一般会在达梦管理工具里执行EXPLAIN,重点看两列:OPERATOR里是否出现SORT,以及COST值是否异常。如果ROWNUM子查询里出现了全表CSCN(全表扫描),先加索引再回来调整 SQL。这套排查习惯,比在 EFCore 侧反复改Include和AsNoTracking要有效得多——因为大多数达梦慢查询问题和 EFCore 翻译质量无关,是执行计划没走上索引。
最后分享一个我自己的教训:在一个模拟项目X里,我花了一周时间去调 EFCore 和达梦之间的关系,最后发现当初建库时没选对兼容级别,导致某些内置函数行为怪异,后来把达梦实例的兼容级别调到 Oracle 模式,同一个连接串、同一套代码,一切正常。所以如果你遇到玄学问题——比如同一个查询时灵时不灵、某些函数报错没有规律,先别死磕代码,去检查达梦的兼容级别和字符集。希望这篇笔记能让你在达梦 8 和 EFCore 3.1 的适配路上少走弯路,帮你把时间花在真正有业务价值的地方。
本文还有配套的精品资源,点击获取