十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Oracle实体类创建指南:类型映射、Dapper与EF Core实践

Oracle实体类创建指南:类型映射、Dapper与EF Core实践 简介面向需要在C#中操作Oracle数据库的.NET开发者主要解决手动编写实体类效率低、易出错的问题。包内是OracleCodeGenerator-master项目包含完整源码、XAML界面、可执行程序以及DBHelper、XMLHelper等辅助类能够通过读取数据库表结构自动生成带数据注释的C#实体类并配合Entity Framework实现ORM映射与CRUD操作。资源共62个文件以cs源文件、dll库、xml配置和xaml界面文件为主压缩包仅2.99MB结构紧凑便于快速上手。已有211人学习下载。通过学习可以掌握Oracle连接字符串配置、实体类属性命名与类型匹配、主键与表名特性标注等关键实践同时了解DbContext上下文搭建、Code First迁移以及仓储模式扩展思路。目录中还包括完整的项目文件、版本控制配置和说明文本可直接在Visual Studio中打开运行或二次开发适合希望提升数据访问层开发效率的中级.NET程序员。1. 为什么从 Oracle 建实体类不能照搬 SQL Server 那套先讲个真实场景。之前接手一个老项目代码是从另一个组拷过来的里面十几个实体类长得和 SQL Server 时代的模型几乎一样整型主键、自动增长、datetime一把梭。结果一接 Oracle 库项目直接原地爆炸——“表或视图不存在”“ORA-00904 无效标识符”“主键冲突”轮着来。排查到最后问题全出在实体类这一层。很多人觉得实体类不就是“一张表对应一个类一个字段对应一个属性”吗这话放在 MySQL、SQL Server 上大体没错但放到 Oracle 上坑就没断过。Oracle 的元数据模型、类型体系、主键生成机制和 SQL Server 根本不是一套逻辑。表名默认是大写、字段默认是大写除非你建表时专门加了双引号主键不会自动增长要么靠序列Sequence要么靠 12c 之后的 Identity 列NUMBER类型不带明确精度时你到底映射成int还是decimal全看业务脸色。这些差异最终都会集中反映到“怎么创建实体类”这件事上。这篇文章覆盖的是我在实际项目里反复用到的几条路径先查 Oracle 数据字典把字段结构摸清然后手工写实体类、用 Dapper 做轻量映射、用 EF Core 做完整 ORM 映射最后是 T4 和脚本批量生成实体的方案。适合谁看正在从 SQL Server/MySQL 转 Oracle 的 .NET 开发者、被旧库逼着手写一堆实体类的老哥还有想写代码生成脚本但不知道从哪下手的读者。看完你至少能少踩一半的坑。2. 建实体前先把 Oracle 数据字典“盘”清楚不管你是手写实体类还是用工具自动生成第一步永远是搞清楚目标表的结构。Oracle 里查表结构不是用DESCRIBE看一眼就完事的尤其在表多、字段多、没人维护注释的老库里你必须靠数据字典视图来拿结构化信息。我平时最常用的是这两张视图USER_TAB_COLS和ALL_TAB_COLS。USER_TAB_COLS查的是当前账号 schema 下的表ALL_TAB_COLS能查你有权访问的所有表区别是多了一个OWNER字段。如果你的连接账号和表的属主不是同一个 schema用USER_TAB_COLS很容易出现“明明表存在却查不到任何行”的情况。这时候换成ALL_TAB_COLS再按OWNER过滤即可。下面这个查询基本是我的固定起手式一次把字段名、类型、长度、精度、可空性、注释全拿齐SELECT t.column_name, t.data_type, t.data_length, t.data_precision, t.data_scale, t.nullable, c.comments FROM all_tab_cols t LEFT JOIN all_col_comments c ON t.owner c.owner AND t.table_name c.table_name AND t.column_name c.column_name WHERE t.owner YOUR_SCHEMA AND t.table_name EMPLOYEE ORDER BY t.column_id;跑出来的结果里DATA_TYPE会告诉你这个字段是VARCHAR2、NUMBER还是DATEDATA_PRECISION和DATA_SCALE是数字字段的总精度和小数位数这俩是决定映射成int、long还是decimal的关键依据NULLABLE直接对应实体属性要不要用可空类型。COMMENTS字段就是列的注释能写进实体文档注释里属于那种“平时没人看、关键时刻救命”的信息。主键信息也要单独查。一个表的主键由哪些列组成光看ALL_TAB_COLS看不出来得关联约束视图SELECT col.column_name, col.position FROM all_constraints cons JOIN all_cons_columns col ON cons.owner col.owner AND cons.constraint_name col.constraint_name WHERE cons.owner YOUR_SCHEMA AND cons.table_name EMPLOYEE AND cons.constraint_type P ORDER BY col.position;我为什么坚持先跑数据字典再写代码因为实体类的核心价值就是“数据库结构的镜像”。你连列的顺序、类型、注释都没盘清楚后面写的映射规则全是空中楼阁。而且这一步还能顺带发现一些隐蔽问题比如某张表居然有三个NUMBER字段精度还各不相同这在实体类里就必须拆成不同 C# 类型。先摸清家底比什么都重要。3. 手写实体类的类型映射规则这张对照表我用了三年拿到字段元数据之后最核心的工作就是定类型映射。Oracle 和 C# 的类型不是一一对应的尤其是NUMBER一套类型通吃整数、小数、布尔映射错了轻则精度丢失重则运行期直接抛异常。下面是我项目里实际使用的映射规则可以当参考。Oracle 数据类型C# 属性类型要点VARCHAR2 / NVARCHAR2string长度只是约束通常不需要对应到 MaxLengthCHAR / NCHARstring定长字段查出来可能带空格自己注意 TrimNUMBER(p, scale0)decimal金额、比率等小数场景NUMBER(p, 0) 且 p9int常见 ID、状态码NUMBER(p, 0) 且 p9long超过 21 亿就得用 longNUMBER(1)int 或 bool看业务约定0/1 建议 intFLOATdouble科学计算类用DATEDateTimeOracle 的 DATE 是带时分秒的别只当天TIMESTAMP(p)DateTime精度到纳秒但映射后还是 DateTimeTIMESTAMP WITH TIME ZONEDateTimeOffset跨时区场景才需要CLOB / NCLOBstring大文本读取时注意大对象开销BLOBbyte[]二进制文件常见RAW(n) / LONG RAWbyte[]通常存放散列值等SYS_REFCURSOR不直接映射存储过程出参不是表列这里必须多说一嘴NUMBER。Oracle 允许你写NUMBER却不指定精度和 scale这种字段实际存储时全靠插入数据“自由发挥”。遇到这种列我从不拍脑袋映射成int默认统一走decimal。原因很简单decimal能表示整数和小数范围也大就算底层数据超了int的边界至少不会让人猝不及防地溢出。精度方面C# 的decimal是 28~29 位有效数字基本覆盖 Oracle 的常见精度性能上的那点差距在业务系统里基本可以忽略。可空性的处理也有讲究。字段NULLABLE是Y实体属性就必须用可空版本DateTime?、int?、decimal?。不然查出一条 NULL 记录ORM 在给属性赋值时先炸给你看。Dapper 尤其明显非空类型接DBNull直接给你一个InvalidCastException你还要回头去对字段。这种错误不致命但很耗时间我后来养成的习惯是所有可空字段一律用NullableT绝不偷懒。命名上还有一个容易忽略的细节Oracle 默认列名是大写比如EMPLOYEE_ID。实体属性用正文风格写没问题问题在于很多 ORM 做映射时列名匹配是大小写敏感的。Dapper 相对宽松EF Core 默认也还行但如果你的 SQL 里写了带引号的别名或者连接字符串里启用了某些大小写敏感设置那就要严格对应。最省事的方法就是 SQL 里显式起别名后面 Dapper 部分我再展开。4. Dapper 模式下实体类的偏门细节SQL 别名和参数占位符如果你的项目不想上重量级 ORMDapper 是个很舒服的选择。它只有一个核心思想你自己写 SQLDapper 负责把结果集塞进实体类。但正因为“你自己写 SQL”Oracle 的方言细节就全暴露在你面前了。我印象最深的是两个点参数占位符和列别名。先说参数占位符。Oracle 的绑定参数不用而是用冒号:。同一个查询在 SQL Server 里写WHERE EMPLOYEE_ID id到 Oracle 必须改成WHERE EMPLOYEE_ID :id。Dapper 本身不关心这个它只是把参数按名字传进去但 SQL 语法必须由你来适配。我第一次迁移时一整页 SQL 全是扫了半小时才改完。public class Employee { public int EmployeeId { get; set; } public string EmployeeName { get; set; } public DateTime? HireDate { get; set; } public decimal Salary { get; set; } } using (var conn new OracleConnection(connString)) { var sql SELECT EMPLOYEE_ID AS EmployeeId, EMPLOYEE_NAME AS EmployeeName, HIRE_DATE AS HireDate, SALARY AS Salary FROM EMPLOYEE WHERE EMPLOYEE_ID :id; var emp conn.QueryFirstOrDefaultEmployee(sql, new { id 1001 }); }注意上面 SQL 里我全部写了别名。Dapper 在反射匹配实体属性时本质上是拿查询结果集的列名和属性名做对比它没有内置“下划线转驼峰”这类智能映射。如果你直接SELECT EMPLOYEE_ID而实体属性叫EmployeeIdDapper 会匹配失败然后把属性留成默认值而且不报错。这是特别坑的地方——查询明明成功了数据却是空的。想彻底避免这个问题一个是 SQL 里手动起别名另一个是给 Dapper 写自定义的ITypeMap。从维护成本看起别名最直观SQL 一清二楚新同事接手也看得懂。再讲一个类似的问题Oracle 习惯用下划线命名列但 C# 属性通常是 PascalCase。你可以在实体类上用数据注解标注列名Dapper 也支持但我个人更倾向在 SQL 层解决因为 SQL 本来就是你要维护的东西多写几个别名不会增加多少负担反而让结果列和实体属性一一对应出问题一眼就能看出来。Dapper 还有一个比较隐蔽的坑就是 Oracle 驱动在读取某些类型时的行为。比如NUMBER(1)字段在 Oracle 里是数字 0/1Dapper 默认读到的是decimal你要是在实体里写了bool属性大概率会报转换错误。我的处理方式是先认清底层类型Oracle 的NUMBER(1)本质上还是数字不是布尔所以要么实体属性用int要么 SQL 里用CASE把它转成1/0然后在属性上自己做转换。这一步没有银弹业务约定不同方案就不同但底线是别让 ORM 去猜。5. EF Core Oracle 的实战配置连接串、主键和日期精度如果你的项目用的是 EF Core那要聊的就不是“要不要映射”而是“怎么让映射真正跑起来”。Oracle 官方提供的 provider 叫Oracle.EntityFrameworkCore在 NuGet 里搜Oracle.EntityFrameworkCore就能找到。版本匹配是个大坑不同版本的 EF Core 要对应不同版本的 Oracle provider比如 EF Core 8 通常搭配 8.x 版本的 provider版本不匹配轻则出警告重则直接起不来。装包之前先看一眼项目目标框架再去 NuGet 的依赖信息里核对兼容性。连接字符串和 SQL Server 的差别也不小。典型的 Oracle 连接串长这样User Idyour_user;Passwordyour_password;Data Sourceyour_host:1521/your_service_name;如果你用的是Data Source里的 TNS 别名那需要确保本机配置了tnsnames.ora。实测下来我更喜欢用 Easy Connect 格式少依赖一个配置文件部署时省心。DbContext 写起来和 SQL Server 差不多但实体配置里要注意几个 Oracle 特有的点。第一个是表名和列名。Oracle 默认是大写EF Core 在生成 SQL 时默认不带引号所以大小写都能对上万一你的表是带引号创建的小写表名那就必须在实体上强制指定[Table(employee)] [Column(employee_id)] public class Employee { public int EmployeeId { get; set; } public string EmployeeName { get; set; } public DateTime HireDate { get; set; } }第二个是主键生成。Oracle 传统主键不是自增而是序列。EF Core 配置里要告诉它“这个值由数据库侧的序列生成”modelBuilder.EntityEmployee(entity { entity.HasKey(e e.EmployeeId); entity.Property(e e.EmployeeId) .ValueGeneratedOnAdd() .HasDefaultValueSql(EMPLOYEE_SEQ.NEXTVAL); });ValueGeneratedOnAdd告诉 EF Core 插入后要从数据库回读生成的值HasDefaultValueSql则让 Oracle 在 INSERT 时调用序列。这个配置别漏了否则 EF Core 会把实体里那个默认的0当主键直接插入运气好报主键冲突运气不好插入一堆 0 主键的脏数据重启服务才能发现。第三个是日期字段精度。Oracle 的DATE类型精度到秒而 C# 的DateTime带更细的刻度。EF Core 查询时如果参数带毫秒匹配索引可能会出现精度不一致的问题。我的常规做法是在 Fluent API 里显式指定列类型entity.Property(e e.HireDate).HasColumnType(DATE);这样生成的 SQL 参数会按DATE的语义走索引命中率有保障。老项目里这种由“类型不匹配”导致的性能问题非常隐蔽你在数据库里看执行计划一切正常但从 EF Core 跑就是慢多半就是这里埋的雷。如果你不想一个个实体手写配置可以用 EF Core 的反向工程命令直接扫描库结构生成实体和 DbContextdotnet ef dbcontext scaffold User Idyour_user;Passwordyour_password;Data Sourceyour_host:1521/your_service_name Oracle.EntityFrameworkCore这个命令能把整库的表都生成出来但它的生成规则偏向保守比如NUMBER无精度时默认生成decimal列名全转 PascalCase 也可能丢掉数据库原始的命名风格。所以生成之后我通常还要过一遍手写配置把前面那些特殊列、序列生成、可空类型做一次“人工校正”。反向工程适合打底不适合一键交付。6. 批量自动生成实体T4 和字典脚本孰优孰劣表一多手写实体类就不现实了。Oracle 这种老库动辄几十上百张表每张表十几个字段靠人肉敲不仅效率低还容易复制粘贴出错。这时候就得考虑自动化。我试过两条路线一种是纯 SQL 脚本拼接生成 C# 代码另一种是 VS 里的 T4 模板。先说纯 SQL 脚本。思路很直接拿第 2 节的数据字典查询当数据源用 SQL 把字段元数据拼成 C# 属性代码。比如你想为每张表生成属性和可空类型可以写这样的查询SELECT public || CASE WHEN t.data_type VARCHAR2 THEN string WHEN t.data_type NUMBER THEN CASE WHEN t.data_scale IS NOT NULL AND t.data_scale 0 THEN decimal WHEN t.data_precision IS NOT NULL AND t.data_precision 9 THEN int WHEN t.data_precision IS NOT NULL AND t.data_precision 9 THEN long ELSE decimal END WHEN t.data_type IN (DATE, TIMESTAMP) THEN DateTime WHEN t.data_type IN (CLOB, NCLOB) THEN string WHEN t.data_type IN (BLOB, RAW) THEN byte[] ELSE string END || CASE WHEN t.nullable Y THEN ? ELSE END || || LOWER(t.column_name) || { get; set; } AS property_code FROM all_tab_cols t WHERE t.owner YOUR_SCHEMA AND t.table_name EMPLOYEE ORDER BY t.column_id;这个脚本生成的只是属性片段还要在外面套一层public class Employee的壳子。优点是很轻任何能连数据库的客户端都能跑不依赖开发环境和 IDE缺点是一次只能处理单张表生成后还需要手动整理格式。我一般拿它来快速生成“初稿”然后再导入到正式实体文件里微调。T4 模板则是更工程化的方案。你可以在 Visual Studio 里建一个.tt文件模板内部用 C# 代码连接 Oracle执行元数据查询再把结果循环输出成实体类。T4 的好处是生成结果直接进项目天然支持批量一次模板搞定所有表坏处是模板本身要调试而且对不熟 T4 语法的同事有门槛。我第一次写 T4 时光是把OracleConnection在模板里跑通就花了不少时间模板报错的信息还不直观全是一堆编译细节。我的真实建议是如果只是偶尔生成几张表用 SQL 拼接脚本如果要经常全库刷新实体值得花半天时间把 T4 调通。另外还有一种折中方案就是把 SQL 脚本的结果输出成.cs文件再手动复制进项目。反正核心目标是“不手敲字段”具体用哪条路取决于你项目里表数量的变化频率。别一上来就整最重的方案很多团队根本用不上。7. 项目实测三个边缘 case 的完整排查链路最后分享三个我在实际项目中踩过的边缘 case每一个都从“现象”到“定位”到“解决”走完全程。这类问题在文档里很难查全但遇到了就是白给好几个小时。Case 1明明表存在却说“表或视图不存在”现象Dapper 查 Oracle 表报ORA-00942: table or view does not exist。但用 Navicat 能看到这张表。排查链路先确认连接账号和表属主是否同一个 schema。很多公司为了权限隔离应用账号只有读权限表属主是另一个账号。此时无 schema 前缀查询会默认找当前账号的同名表找不到就报错。验证方法是查ALL_TABLES看这张表的OWNER是谁。再验证连接串里有没有配对User Id。如果OWNER确实是别的账号SQL 里必须写成OWNER.TABLE_NAME。还有一种少见情况表名或列名创建时用了小写加引号查询时也要带引号但实际项目里这种表极其罕见基本遇到第一个原因居多。Case 2Dapper 读取 NUMBER(1) 列属性类型被判死刑现象实体属性是bool IsDeleted查询报InvalidCastException底层是OracleDecimal无法转成bool。排查链路先用数据库客户端确认字段类型果然是NUMBER(1)。Oracle 的NUMBER(1)只是“容量为 1 位数字”不是布尔类型。ODP.NET 驱动读它时返回的是decimalDapper 再把decimal转bool就走不通了。我当时没有急着改实体而是先看这个列在业务里到底有哪些值确认只有 0 和 1 后才决定方案要么实体改成intSQL 里不动要么实体保持boolSQL 里写CASE WHEN IS_DELETED 1 THEN 1 ELSE 0 END。项目里这个字段到处参与业务计算我最后选了int方案改动最小。核心心得是类型转换问题不要硬刚看清业务数据再定。Case 3EF Core 插入主键冲突100% 复现现象新记录插入时主键重复每次都报唯一约束冲突但表中明明没有相同 ID。排查链路先看 INSERT 语句发现 EF Core 生成的是INSERT INTO EMPLOYEE (EMPLOYEE_ID, ...) VALUES (0, ...)。问题清楚了——实体配置里没告诉 EF Core 主键由序列生成。EF Core 看到EmployeeId是 int 主键默认认为应用层提供主键值所以把 0 塞了进去。解决方法是给EmployeeId配置ValueGeneratedOnAdd().HasDefaultValueSql(EMPLOYEE_SEQ.NEXTVAL)让 EF Core 知道这个值由数据库侧生成。改完后 INSERT 语句里不再包含主键列冲突消失。这个 case 提醒我反向工程生成的模型也要复查主键生成配置特别是带序列的老表几乎都要手动补。三个 case 有一个共同点问题都不在“语法”层面而在“数据库特性与 ORM 默认行为”之间的空隙里。遇到这类问题我的排查顺序永远是先看实际 SQL 和实际字段类型再谈改代码。你只有先弄清楚 Oracle 底层到底在做什么实体类的坑才不会反复踩。最后分享一个小技巧拿到一张新表时除了看数据字典最好也跑几条真实数据看看尤其注意那些NUMBER字段的取值分布它们往往比 DDL 更诚实地告诉你应该映射成什么类型。本文还有配套的精品资源点击获取
返回列表