ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C#操作Oracle实战指南:实体类、Dapper和EF Core踩坑全记录

C#操作Oracle实战指南:实体类、Dapper和EF Core踩坑全记录 简介一份面向 C# 与 Oracle 数据库开发者的源码工具项目基于 OracleCodeGenerator 实现自动创建实体类能够帮助开发者在 Entity Framework 等 ORM 场景中快速生成与数据库表对应的 C# 实体类减少手动编写和维护数据模型的工作量。资源压缩包共 62 个文件以 cs 源码、dll 运行库、xaml 界面、xml 类型映射等类型为主体积仅 2.99MB包含可直接运行的程序和完整解决方案便于学习、运行与二次开发。内容覆盖 Oracle 连接字符串配置、实体类属性映射、主键标注、DbContext 用法等关键知识点并提供了 Repository、Code First 迁移等代码生成的扩展思路。已有 211 人浏览/学习整体轻量而实用对希望提升 C# 访问 Oracle 数据层搭建效率的入门与中级开发者颇有参考价值。 这几年陆陆续续做了不少 C# 和 Oracle 一起用的项目从最开始的 WinForm 上位机到后来的 Web API都会撞上同一件事把数据库表的数据取出来变成能直接操作的对象也就是创建实体类。Oracle 在国内的老系统、制造业、工控后台里出现频率极高C# 这边无论是 .NET Framework 还是新一点的 .NET 6/8操作 Oracle 都要面对一堆细节驱动选哪个、Oracle 的参数为什么是冒号不是 、NUMBER 类型怎么映射、老版本数据库怎么分页。这篇文章我就从“创建实体类”这件事出发把 C# 操作 Oracle 从环境准备、手写实体类到用 Dapper 和 EF Core 落地再到常见的坑完整走一遍。适合正在搭新项目、或者被 oracle.ManagedDataAccess 折磨过的新手参考也适合老手查漏补缺。1. 动手之前先想清楚实体类解决了什么1.1 为什么每次都绕不开这一堆样板代码很多初学者第一次接触 Oracle写出来的代码是这样的new 一个 Connection再 new 一个 Command循环 DataReader按列读值塞给对象。写一个查询还好如果一张表有 20 个字段你就要写 20 行obj.Field reader.GetXxx(reader.GetOrdinal(FIELD))。更难受的是你还要处理 DBNull、类型转换、参数顺序写完之后和数据库表结构耦合得死死的字段一变C# 代码跟着改改漏了直接运行时报错。实体类解决的就是这件事把表结构固定成强类型对象让数据库字段和 C# 属性一一对应。这样业务代码里操作的、传递的、绑定的都是这个对象数据库访问被隔离在数据层上层根本不用关心 SQL 还是存储过程。对 C# 这种强类型语言来说实体类还能享受编译期检查、智能提示、重构不手抖这些福利在裸写 DataReader 时是体会不到的。1.2 三条路线手写、Dapper、EF Core 怎么选创建实体类一般有三条路没有绝对的好坏得看项目场景。手写实体类 Oracle.ManagedDataAccess.Core适合小项目、工具类、上位机内部功能。控制力强没有黑魔法缺点是样板代码多。Dapper 手写实体类适合大部分业务系统。实体类可以手写也可以用一个简单的 SQL 反向生成Dapper 只负责把查询结果映射成实体轻量、透明、性能高。EF Core Oracle.EntityFrameworkCore适合表结构复杂、团队规模大、希望由框架自动生成实体的场景。EF Core 提供反向工程一条命令把整库表生成成实体类还带上下文、配置映射、迁移缺点是版本兼容坑多学习曲线陡。我自己的建议是项目不大数据访问就用原生态和 Dapper项目大、表多再上 EF Core 也不迟。毕竟实体类本身只是第一步后面的并发、事务、缓存才是大头。2. 环境准备和连接串先把地基打牢2.1 NuGet 包别装错C# 连 Oracle很多人还停留在 System.Data.OracleClient 时代那个是旧 .NET Framework 自带的微软早就标注过时了。现在主流选择是 Oracle.ManagedDataAccess.Core这是 Oracle 官方出的托管驱动跨平台.NET Core / .NET 5 都能用纯托管代码不用装 Oracle 客户端这一点比老 ODP.NETOracle.DataAccess省心得多。在项目里装包就一行命令dotnet add package Oracle.ManagedDataAccess.Core如果你是 Visual Studio 的 Package Manager Console就执行Install-Package Oracle.ManagedDataAccess.Core装了它之后命名空间是Oracle.ManagedDataAccess.Client核心类三个OracleConnection、OracleCommand、OracleDataReader。整体写法和 SqlClient 极像语法十秒能上手。如果用 EF Core包名注意要装Oracle.EntityFrameworkCore还有Microsoft.EntityFrameworkCore.Design和Microsoft.EntityFrameworkCore.Tools这三个缺一个反向工程命令可能起不来。另外千万记住Oracle.EntityFrameworkCore 版本必须和当前项目用的 EF Core 版本对应版本差了运行时会直接给你一个 TypeLoadException一脸懵。2.2 连接字符串和 appsettings.json 的配置Oracle 连接串比 SqlServer 多了个 Data Source 的讲究。最简单的写法是host:port/service_nameData Source192.168.1.100:1521/ORCLPDB1;User Idscott;Passwordtiger;其中 ORCLPDB1 是服务名不是 SID这是很多初学者的第一个坑。Oracle 12c 之后默认有 CDB/PDB 结构你连 PDB 要用服务名如果你的库是 11g 那种老结构也可以写 SID 形式Data Source(DESCRIPTION(ADDRESS(PROTOCOLTCP)(HOST192.168.1.100)(PORT1521))(CONNECT_DATA(SIDORCL)));实际开发中连接串一般放 appsettings.json不要硬编码。.NET 6 的写法是{ ConnectionStrings: { OracleConn: Data Source192.168.1.100:1521/ORCLPDB1;User Idscott;Passwordtiger; } }然后通过 IConfiguration 读取。上位机场景如果不想引入太多框架也可以直接用 ConfigurationBuilder 读 JSON 文件反正原则就一个连接串和代码分离避免每次换库重新编译。3. 手写实体类从 EMP 表讲清楚映射规则3.1 表结构设计和实体类怎么对应我这里用 Oracle 最经典的 EMP 表做例子大家都熟。表结构大概是 EMPNO数字主键、ENAME姓名、JOB职位、MGR上级编号、HIREDATE入职日期、SAL薪资、COMM奖金、DEPTNO部门编号。对应到 C# 实体类我一般这样设计public class Emp { public int EmpNo { get; set; } public string Ename { get; set; } public string Job { get; set; } public int? Mgr { get; set; } public DateTime HireDate { get; set; } public decimal Sal { get; set; } public decimal? Comm { get; set; } public int DeptNo { get; set; } }列名映射我习惯直接去掉下划线换成 PascalCase比如DEPT_NO映射成DeptNo查询时用SELECT DEPT_NO AS DeptNo FROM ...或者实体属性叫DEPT_NO也行但这就让 C# 代码变得很丑。可空字段我这里都加了?比如Mgr和Comm在表里允许为 NULL属性必须声明成可空类型否则赋值 NULL 时直接炸。3.2 Oracle 类型到 C# 类型的映射表Oracle 的类型体系比较老派NUMBER 一个类型就能表示整数和小数所以映射成 C# 类型时要注意选择。这是我的经验表Oracle 类型推荐 C# 类型备注NUMBER (整数精度 9)int注意精度容易溢出NUMBER (整数精度 10-18)long大整数场景NUMBER (小数)decimal钱相关必须用 decimalNUMBER(1)int / short别直接映射 bool驱动不一定帮你转VARCHAR2 / NVARCHAR2stringCHAR / NCHARstring注意 CHAR 会补空格记得 TrimDATEDateTimeOracle 的 DATE 带时间TIMESTAMPDateTimeTIMESTAMP WITH TIME ZONEDateTimeOffset有时区需求才用CLOBstring大文本BLOBbyte[]二进制特别注意 NUMBER 映射成 int 的坑。如果表中某个 NUMBER 列精度很高比如 NUMBER(12)C# 属性用 int 就装不下Dapper 和 ADO.NET 在赋值时会抛异常。稳妥起见不确定就全用 decimal 或 long等确定范围再收窄。3.3 DataReader / DataTable 转实体的代码手写映射的标准姿势是读 DataReader 然后逐列取值。用 EMP 表做个查询示例using Oracle.ManagedDataAccess.Client; var connStr Data Source192.168.1.100:1521/ORCLPDB1;User Idscott;Passwordtiger;; var list new ListEmp(); using var conn new OracleConnection(connStr); conn.Open(); using var cmd new OracleCommand(SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO FROM EMP WHERE DEPTNO :deptNo, conn); cmd.Parameters.Add(new OracleParameter(deptNo, OracleDbType.Int32) { Value 10 }); using var reader cmd.ExecuteReader(); while (reader.Read()) { var emp new Emp { EmpNo reader.GetInt32(reader.GetOrdinal(EMPNO)), Ename reader.GetString(reader.GetOrdinal(ENAME)), Job reader.GetString(reader.GetOrdinal(JOB)), Mgr reader.IsDBNull(reader.GetOrdinal(MGR)) ? null : reader.GetInt32(reader.GetOrdinal(MGR)), HireDate reader.GetDateTime(reader.GetOrdinal(HIREDATE)), Sal reader.GetDecimal(reader.GetOrdinal(SAL)), Comm reader.IsDBNull(reader.GetOrdinal(COMM)) ? null : reader.GetDecimal(reader.GetOrdinal(COMM)), DeptNo reader.GetInt32(reader.GetOrdinal(DEPTNO)) }; list.Add(emp); }这里有个容易被忽略的细节NULL 列直接调用GetInt32/GetDecimal会抛InvalidCastException所以可空列必须先用IsDBNull判断再取值。每次手写这么一遍确实烦所以很多项目会再用一个泛型反射方法把 DataReader 转实体但反射也有性能开销小数据量无所谓大数据量循环频繁调用时要注意。3.4 参数别用错Oracle 的参数是冒号很多从 SqlServer 转过来的人会习惯写deptNo在 Oracle 里参数占位符要用冒号:deptNo。这是一个看起来小、实际极其常见的错误网上几乎每天都能看到有人问为什么 ORA-00936、ORA-00911。OracleCommand 添加参数时参数名可以带冒号也可以不带但 SQL 里必须带cmd.Parameters.Add(new OracleParameter(deptNo, OracleDbType.Int32) { Value 10 });SQL 写成... WHERE DEPTNO :deptNo就对了。还有一点Oracle 参数的顺序并不重要重要的是名字必须和 SQL 里的占位符一致这一点比 SqlClient 更友好。4. 用 Dapper 让实体类“活”起来4.1 为什么 Dapper 适合 Oracle C#手写了几个实体的增删改查之后你会觉得大部分代码都在做机械重复。Dapper 是 Stack Overflow 团队出的轻量 ORM不搞复杂的对象关系映射只做一件事把 SQL 查询结果按列名前缀自动映射到实体属性。它性能高、上手快、不会把你困在框架的抽象里特别适合 C# Oracle 这种需要写原生 SQL 的场景。在 NuGet 里装 Dapperdotnet add package Dapper然后原来的 DataReader 循环就变成了using var conn new OracleConnection(connStr); var list conn.QueryEmp( SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO FROM EMP WHERE DEPTNO :DeptNo, new { DeptNo 10 }).ToList();Dapper 会自动把查询结果的列名默认大写 EMPNO和实体属性 EmpNo 做不区分大小写的匹配下划线不做自动转换所以如果你的列名是 EMP_NO要么 SQL 里AS EmpNo要么实体属性就叫 EMP_NO。我用方案是 SQL 里加别名保持实体类干净。4.2 增删改查和分页怎么写Dapper 的增删改直接用 ExecuteSQL 自己写。插入时如果要拿自增主键Oracle 12c 之后可以用 Identity 列配 RETURNING 子句Dapper 用 DynamicParameters 接返回值var sql INSERT INTO EMP (EMPNO, ENAME, SAL) VALUES (:EmpNo, :Ename, :Sal) RETURNING EMPNO INTO :EmpNoOut; var p new DynamicParameters(); p.Add(EmpNo, emp.EmpNo, DbType.Int32); p.Add(Ename, emp.Ename, DbType.String); p.Add(Sal, emp.Sal, DbType.Decimal); p.Add(EmpNoOut, dbType: DbType.Int32, direction: ParameterDirection.Output); conn.Execute(sql, p); var newId p.Getint(EmpNoOut);如果你用老版本 Oracle11g 及以下没有 Identity一般是用序列 触发器那插入主键就靠序列取 NEXTVALRETURNING 同样可以帮你拿回主键套路完全一样只是 SQL 里把主键值换成SEQ_EMP.NEXTVAL。分页这块也有坑。Oracle 12c 开始支持标准OFFSET ... FETCH写起来很人性化var page conn.QueryEmp( SELECT * FROM EMP ORDER BY EMPNO OFFSET :Offset ROWS FETCH NEXT :Limit ROWS ONLY, new { Offset (pageIndex - 1) * pageSize, Limit pageSize }).ToList();但老库11g不支持这个语法只能用 ROWNUM 三层嵌套SQL 又长又难维护SELECT * FROM ( SELECT e.*, ROWNUM rn FROM ( SELECT * FROM EMP ORDER BY EMPNO ) e WHERE ROWNUM :EndRow ) WHERE rn :StartRow建议直接把后一段封装成一个分页工具方法项目里统一调用。4.3 查询结果的 NULL 和类型问题Dapper 对 NULL 的处理比原生 DataReader 友好可空属性int?、decimal?能自动映射为 null不会抛异常。但有一个坑是 Oracle 的 CHAR 类型如果字段定义为 CHAR(10)存的值实际只有“SMITH”五个字母查询出来会带空格补齐到 10 位。Dapper 不会帮你 Trim页面显示时会莫名多一堆空格对比字符串也怎么都比不上。解决方法是实体的属性加一个 setter 处理private string _job; public string Job { get _job?.Trim(); set _job value; }或者干脆在 SQL 里TRIM(JOB) AS JOB最简单。这个细节在 Oracle 的旧系统里特别常见碰到了不要怀疑是驱动 bug就是 CHAR 的固定长度特性。5. EF Core 反向工程自动生成实体类5.1 安装工具和包如果你的表非常多手写实体类明显不现实。EF Core 提供了一个反向工程命令基于现有数据库生成实体类和 DbContext一条命令能把几百张表变成类省下的不是一点半点。先安装三个包dotnet add package Oracle.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.EntityFrameworkCore.Tools然后在项目目录执行dotnet ef dbcontext scaffold Data Source192.168.1.100:1521/ORCLPDB1;User Idscott;Passwordtiger; Oracle.EntityFrameworkCore -o Models --table EMP --table DEPT --no-onconfiguring重点看几个参数-o Models指定输出目录--table只生成指定表表名用大写不然可能找不到--no-onconfiguring是不生成带连接串的 OnConfiguring 方法避免把密码硬编码进去。如果你不写--table会把整个库的表全部生成Oracle 里系统表一大堆生成结果非常壮观强烈建议用--table过滤。5.2 生成后要检查的三个映射问题EF Core 生成实体并不代表万事大吉。我实际用了之后发现三个地方必须再检查一遍。第一NUMBER 字段默认映射成 decimal即使这个列实际是整数。比如 EMPNO 生成出来就是public decimal EmpNo { get; set; }如果后续代码想拿它做 int 运算全是强制转换或 toInt()很痛苦。我通常手动改成 int同时注意精度范围。第二Column 特性带上了原始列名和类型。比如[Column(ENAME)] public string ENAME { get; set; }属性名也是大写下划线看起来很丑。可以统一改成 PascalCase只要把[Column(ENAME)]留着EF 就能正确映射。第三实体里所有的约束、外键、索引会生成一堆 Navigation 属性和 Fluent API 配置。如果你的表有关联生成的导航属性会很多业务上用不到的就直接删掉否则序列化接口时可能循环引用、性能下降。删导航属性不影响查询EF 依然可以 Join。5.3 没有主键的表怎么处理反向工程时如果表没有主键Oracle 这边常见的是视图或者历史表EF Core 生成会直接报错“无法为主键实体生成类型”。这种情况有两个思路给表加主键这是最推荐的做法实在不能加的话就在 SQL 里选一个唯一列然后手动在实体类上标注[Key]。还有一个 Oracle 历史遗留问题如果表上有ROWID可以把它加入实体类并标记为 RowVersion这样在分页、去重、以及并发更新时比较方便。新版 EF Core 对 Oracle ROWID 的支持已经比较完善但生成时不会自动加需要你手动补不是所有项目都要用知道有这条路就行。6. 实战踩坑记录这些坑我全踩过6.1 大小写和引号问题Oracle 如果建表时没加双引号表名和列名都会自动转成大写如果加了双引号就会强制使用你写的那个大小写。所以查询时你写小写列名Oracle 会尝试转成大写一般能查到但如果你建表时用了EmpNo这种带引号的小写命名那查询时必须也加双引号且大小写完全一致否则就是 ORA-00904 无效标识符。解决方法是新表统一用不含引号的大写命名实体属性随便你写成 PascalCase只要 SQL 给别名就行。老库如果已经混用了大小写写 SQL 时老老实实加双引号。6.2 NUMBER 类型精度溢出和映射值域我遇到过最无语的一个 bug某个统计字段在 Oracle 里是 NUMBERC# 属性是 int平时数据量小没事某天数据涨了数值超过 int 的最大值 2147483647查询直接报OverflowException。后来我把所有可能变大的 NUMBER 都改成 long 或 decimal。这里给个经验值金额类用 decimalID 类用 int 或 long统计量/数量用 long 或 decimal尽量不要用 float 和 doubleOracle 的 NUMBER 是十进制精度double 是二进制浮点金额计算会出现精度丢失。6.3 DBNull 和空字符串是两回事Oracle 里NULL和一个空字符串在很多场景下是不同概念但 Oracle 老版本里会被当成 NULL 处理这带来一堆隐蔽问题。C# 这边要注意DataReader 读 NULL 会得到DBNull.Value如果你直接赋给 string 属性可能要判断一下再赋值。Dapper 会自动把 NULL 给 null但 EF Core 反向工程生成的可空字符串属性也会有同样的问题所以前端传空字符串时你要决定到底存 NULL 还是空串统一规则不然各种统计对不上。6.4 TIMESTAMP 和 DATE 的读取差异Oracle 中 DATE 本身就带时间精度到秒TIMESTAMP 更精细带小数秒。C# 都映射成 DateTime 没问题但要注意有些驱动在读取 TIMESTAMP 时如果配置不对可能出现毫秒丢失的问题。如果你要做精确时间比较比如比对某个操作的先后顺序建议字段直接用 TIMESTAMP并且 C# 侧不要用DateTime.Now这种精度低的进行对比。更麻烦的是 TIMESTAMP WITH TIME ZONE它映射成 DateTimeOffset如果你误用 DateTime 接收赋值时会丢时区信息。我的建议是没有跨时区需求的系统别用带时区的类型自找麻烦。6.5 常见报错速查最后整理一个排错表都是我实际调试时高频遇到的报错信息原因解决办法ORA-00936 缺少表达式SQL 里用了 参数前缀改成 :参数名ORA-00904 无效标识符列名大小写或引号不对查表结构确认列名是否带引号ORA-01017 用户名/口令无效账号密码错误或连错库检查连接串和用户权限ORA-12514 监听程序无法识别服务服务名/SID 写错确认连接串用的是服务名还是 SIDInvalidCastExceptionNUMBER 转 int 溢出或 NULL 直接取值改 long/decimal加 IsDBNull 判断TypeLoadException: Method not foundOracle.EntityFrameworkCore 与 EF Core 版本不匹配统一升级到对应版本序列化实体循环引用导航属性互相引用忽略或删除不需要的导航属性DEGREE 并行度报错/查询慢老库统计信息过期定期执行EXEC DBMS_STATS.GATHER_TABLE_STATS(...)6.6 一个容易忽略的部署依赖问题如果你用的是 Oracle.ManagedDataAccess.Core 且部署在服务器上注意它不需要安装 Oracle 客户端这是一个巨大的优势。但要注意如果服务器上没有装 Oracle Instant Client某些依赖 Oracle 原生网络库功能比如高级压缩、加密可能会报错。大多数常规查询、Dapper、EF Core 操作都不受影响但如果你用了高级特性部署时先在测试服务器上做一轮完整回归别等现场炸了再排查。还有一个小细节连接池默认是开启的OracleCommand 用完一定要 Dispose 或用 using连接池满了会导致连接泄漏报“连接请求超时”。这一点在高并发的 Web API 项目里很容易踩我见过一个线上服务频繁超时最后排查到是某个方法忘记关连接加上 using 之后立刻恢复正常。做这套东西这么多次我个人体验是实体类本身并不难难的是把 Oracle 的旧特性和 C# 的强类型映射处理好。如果你也在做上位机或工控项目C# 连着 Oracle建议在项目初期就把实体类的命名规范、NULL 处理规则、类型映射表定下来后面所有数据访问代码都照着走。最后再分享一个小习惯每建一张表就顺手写一个SELECT ... AS 属性名的查询做基准测试额外查询一下 0 行和大数值行的情况这样大部分运行期坑在开发阶段就能筛掉。本文还有配套的精品资源点击获取
返回列表