C#操作Oracle数据库的实体类创建与通用映射方案

📅 发布时间:2026/9/7 5:58:02
C#操作Oracle数据库的实体类创建与通用映射方案
简介面向C#开发者的Oracle数据库实体类自动生成工具资源为OracleCodeGenerator项目源码帮助解决手写实体类效率低、字段易遗漏等问题。资源共62个文件以18个cs源码文件为核心包含主窗口界面、数据库连接帮助类、表及连接信息模型等另附9个dll运行库、3个xml映射配置及编译生成文件压缩包整体仅2.99MB。目前已有211人学习下载适合使用Entity Framework对接Oracle的.NET项目开发者。工具支持选择数据库表或视图一键生成对应实体类自动处理属性类型与数据库字段类型映射结合描述中的连接字符串、主键注解和DbContext用法可快速搭建数据访问层。读者还可参考其界面逻辑、数据连接模块与类型映射规则理解从Oracle元数据生成模型类的完整流程并在此基础上扩展出适合自身项目的生成器源码目录结构清晰便于在Visual Studio中直接打开调试对减少重复编码、规范实体层设计有直接帮助。 我这几年一直在做C#相关的企业级系统从WinForm到WPF再到后来的.NET Core数据库这块离不开Oracle。说句实在话C#连Oracle本身不难难的是怎么把查询结果整洁地变成代码里能用的对象——也就是实体类。很多人一开始图省事直接DataTable一把梭前期确实快等项目一复杂到处是DataRow[字段名]这种魔法字符串改个字段名全项目报错等着哭吧。这篇文章就围绕C# Oracle创建实体类这件事把我自己从最早手写映射到后来用反射做通用映射器的完整经验梳理一遍。适合刚接触Oracle的C#开发也适合那些被DataTable折磨到想重构的老哥。你能看到Oracle这数据库跟SQL Server或MySQL在类型上的明显差异也能拿到一套可以直接抄作业的实体映射方案。1. 先搞清楚自己到底在纠结什么1.1 实体类不是ORM的专利我一直觉得实体类这玩意儿就是一张“翻译表”——它把数据库里的关系型数据翻译成C#里的强类型对象。很多人非要等到上了EF Core或者SqlSugar这类ORM才愿意建实体类其实完全没必要。哪怕你就是直接手写ADO.NET把查询结果映射到实体类上代码的可维护性都能高出一大截。我的习惯是哪怕只查一张表我也会建实体类。这不是矫情而是实打实省过命的。你想一下DataTable里取数据靠的是列名的字符串万一数据库改了个列名你是编译期发现不了的只能在运行时等它炸。而实体类强类型映射编译器就能帮你抓出一堆低级错误。说到底实体类的本质是让代码结构跟上业务结构。C#是强类型语言对类型的敏感度天然就高。你让一个int字段在DataTable里待着取出来是object还得拆箱子又慢又丑。封装成实体类之后字段就是属性类型就是类型整个代码的气场都不一样了。1.2 Oracle场景下为什么特别需要“动手做”我知道很多人会问C#连Oracle不是有现成的ODP.NETOracle Data Provider for .NET吗不是有Entity Framework的Oracle官方Provider吗确实有但现实情况往往没那么顺。第一Oracle在C#这边的ORM支持力度确实不如对SQL Server那边亲儿子待遇。EF Core配Oracle虽然能用但坑也不少比如某些类型映射不上、某些SQL生成得不理想。第二很多企业里的老项目压根不给你用ORM的机会DBA那边控得死死的SQL都得人工审你怎么可能还指望自动迁移自动建表第三有些性能敏感的场景比如上位机做实时数据采集几百上千条记录要毫秒级写进数据库走重型ORM反而浪费时间手写轻量映射反而更可控。所以懂怎么自己创建实体类、自己写映射代码在Oracle这个领域属于基本功。这活儿看着不起眼关键时候能救场。2. C#和Oracle的类型对齐这是第一个大坑2.1 类型映射表建议你存起来SQL Server也好MySQL也好它们的类型都比较规整C#对应的类型也相对直观。Oracle就不一样了最核心的区别就是它只有一个NUMBER类型却要扛起整数、小数、高精度数值的所有重担。另外还有个DATE它其实包含了日期和时间。我把自己常用的映射规则整理成了一张表基本够用Oracle类型推荐C#类型说明NUMBER(1)bool视情况/ short很多表用1和0表示布尔映射时手动转NUMBER(5)short / int小整数NUMBER(9)int常规整数注意别超范围NUMBER(18)long / decimal大整数或者分毫类金额数据NUMBER(p, s)decimal带小数点的比如金额、比率FLOATdouble科学计算类精度要求不高的场景VARCHAR2(n)string最常用注意n是字节还是字符NVARCHAR2(n)string存中文更推荐字符语义明确CHAR(n)string固定长度取出可能带空格记得TrimDATEDateTime包含时分秒时刻注意TIMESTAMPDateTime精度更高CLOBstring大文本可能超4000字符BLOBbyte[]二进制文件之类这里面的坑在于Oracle的VARCHAR2如果数据库字符集是AL32UTF8一个汉字可能占3个字节你建表时写的VARCHAR2(20)可能只能存6个汉字。搜热词里那么多Oracle相关的问题这个能排上号。2.2 NUMBER类型为什么又爱又恨NUMBER是Oracle最通用的数值类型可塑性强是它的优点但对于C#开发就是灾难。你根本不知道读出来的数据该转成int还是decimal如果数据库里存的是NUMBER(18)C#这边用int去接数字一大直接overflow程序在运行中突然抛异常。我的经验是单表映射时务必先通过USER_TAB_COLUMNS查一下字段的data_precision和data_scale搞清楚这个NUMBER到底是整数还是小数再去确定C#属性的类型。精度在0到9之间用int精度在10到18之间用long如果有小数位一律decimal。还有个经验是如果你只是自己建表自己用尽量别用NUMBER不带精度指定清楚比如NUMBER(9)就是intNUMBER(18,2)就是decimal这样代码这边就不会猜谜。2.3 大小写、下划线和命名约定Oracle一个非常折磨人的地方在于不加引号建的表和字段存进去全是大写。而C#这边的规范是PascalCase属性名通常叫UserName、CreateTime。数据库表里的字段却是USER_NAME、CREATE_TIME。这俩不弄个映射关系肯定对不上。我跟团队定的规矩是数据库字段用下划线命名实体属性用PascalCase映射时靠代码做字段名到属性名的自动转换。既然要做通用映射器这个转换逻辑就是基础中的基础——去掉下划线首字母大写。这样你写的SQL列名和实体属性之间就有规可循不用每个字段手写Column特性省下一堆重复劳动。3. 手工映射到通用映射器一个演进过程3.1 最笨但最直观的手写映射最早我做C#读取Oracle的时候干的就是最原始的活手写一个实体类然后把DataReader一行行读出来按字段位置赋值给属性。类似这样public class UserInfo { public int Id { get; set; } public string UserName { get; set; } public DateTime CreateTime { get; set; } } // 手写映射 UserInfo user new UserInfo(); while (reader.Read()) { user.Id Convert.ToInt32(reader[ID]); user.UserName reader[USER_NAME]?.ToString(); user.CreateTime Convert.ToDateTime(reader[CREATE_TIME]); }这种方式的优点是直观、性能好、每一行代码自己都心里有数。缺点是表一多就疯了。三五个表还能忍三五十个表光是手写这些赋值语句一天的时间就没了而且还特别容易在某个字段上漏写或者写错下标。一旦数据库加了一个字段所有相关映射代码全要跟着改。我记得有个老项目接近一百张业务表光实体类文件和映射代码就占了项目一半的篇幅。后来实在受不了才下决心写通用映射器。3.2 T4模板自动生成实体类如果你还在用.NET FrameworkT4模板是个不错的选择。T4全称Text Template Transformation Toolkit简单说就是写一个模板文件让Visual Studio在编译前把C#代码帮你生成出来。你可以连上Oracle读取USER_TAB_COLUMNS动态生成所有实体类文件。这个方案我用了挺长一段时间确实省事跑一遍所有实体类就齐了。但T4的问题在于它把代码生成和数据库结构绑得太死数据库变更后你得手动重新运行一下模板忘了跑就等着编译报错。而且模板本身写起来挺繁琐字符串拼接、缩进、类型映射逻辑全混在一起后期维护也费劲。如果你追求的是稳定可控T4可以作为备选工具但我更推荐的还是下面这种反射方案。3.3 基于反射的通用映射方案反射这招搜热词里“c#反射”算是个高频词说明大家对这个确实有兴趣。思路不复杂你只写一份通用的映射代码它通过反射遍历实体类的属性去DataReader里找对应名称的列自动赋值。这样不管你有多少张表、多少个实体类这份映射代码都能复用。这一天我动手做了一个简单的通用映射器核心逻辑非常轻量。public static class EntityMapper { /// 从IDataReader映射到实体集合 public static ListT MapT(IDataReader reader) where T : class, new() { ListT list new ListT(); // 先拿到实体类的所有属性 PropertyInfo[] properties typeof(T).GetProperties(); while (reader.Read()) { T item new T(); foreach (PropertyInfo prop in properties) { // 根据属性名找列名支持下划线转PascalCase string columnName ToColumnName(prop.Name); int ordinal reader.GetOrdinal(columnName); if (ordinal 0 !Convert.IsDBNull(reader[ordinal])) { // 类型转换兼容Oracle NUMBER到decimal/int/long object value reader[ordinal]; prop.SetValue(item, ChangeType(value, prop.PropertyType)); } } list.Add(item); } return list; } private static string ToColumnName(string propertyName) { // 简单处理在每个大写字母前加下划线然后转大写 // UserName - USER_NAME // 如果你的命名规约不同改这里即可 return Regex.Replace(propertyName, ([A-Z]), _$1).TrimStart(_).ToUpper(); } private static object ChangeType(object value, Type targetType) { Type trueType Nullable.GetUnderlyingType(targetType) ?? targetType; if (trueType.IsEnum) { return Enum.ToObject(trueType, value); } return Convert.ChangeType(value, trueType); } }我承认这个写法非常精简可真放到项目里它完全够用。你不需要为每一张表写映射代码只需要保证一个问题实体属性名和表字段名之间的转换规则是一致的比如下划线转PascalCase或者反过来PascalCase转大写下划线。这一份代码我后来在好几个项目里复制粘贴改吧改吧接着用省下来的时间够喝好几箱快乐水了。4. 落地实操一套顺手好用的Oracle通用查询封装4.1 环境准备和驱动选择先交代一下环境。我用的是Visual Studio 2022.NET 8Oracle数据库版本是11g和19c混着用。NuGet包用官方推荐的Oracle.ManagedDataAccess.Core这个包的好处是不用装Oracle客户端托管模式直接连部署省心。我特意提醒一句不要再用老的System.Data.OracleClient微软早就标记过时了官方都不推荐项目跑在32位和64位环境还会出各种幺蛾子。安装命令很简单dotnet add package Oracle.ManagedDataAccess.Core如果你的项目还是在.NET Framework阶段那就装Oracle.ManagedDataAccess用法一样。这算是Oracle官方在.NET这边的亲儿子长期维护出了事起码有人管。4.2 通用查询方法我习惯把数据库操作封装成一个OracleHelper类用的时候只需要传入SQL和参数返回ListT。这里有三个关键点想重点说连接字符串里推荐加上Poolingtrue这个能极大减少频繁开关连接的开销。高并发场景下没有连接池你的程序基本会被连库操作拖垮。参数化查询是底线不要拼接SQL无论是防SQL注入还是处理日期格式问题都能省太多心。Oracle的游标和SQL Server不一样存储过程输出结果集时必须显式声明REF CURSOR这个在调用存过的时候要特别留意。来看一段通用查询的实际代码。public static class OracleHelper { private static string connStr ConfigurationManager.ConnectionStrings[OracleConn].ConnectionString; public static ListT QueryT(string sql, params OracleParameter[] parameters) where T : class, new() { using (OracleConnection conn new OracleConnection(connStr)) { using (OracleCommand cmd new OracleCommand(sql, conn)) { cmd.CommandType CommandType.Text; if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); using (OracleDataReader reader cmd.ExecuteReader()) { return EntityMapper.MapT(reader); } } } } }这段代码看着简单但它把连接生命周期、命令执行、DataReader关闭、实体映射全串起来了。调用方是这样的string sql SELECT ID, USER_NAME, CREATE_TIME FROM T_USER WHERE STATUS :status; var users OracleHelper.QueryUserInfo(sql, new OracleParameter(status, 1));注意Oracle的参数占位符用冒号而不是这一点跟SQL Server截然不同。很多从SQL Server转过来的朋友第一遍写代码必踩这个坑编辑器又不提示运行时直接报“ORA-01036非法的变量名/编号”心态容易崩。4.3 通用插入方法避免手写VALUES插入和查询一样手写INSERT语句也是一场体力活。尤其字段特别多的表写错一个顺序数据就进了错误的列调半天才发现是字段对齐错了。所以我也做了一个通用插入方法拿实体的属性列表自动拼出列名和参数名。public static int InsertT(T entity, string tableName) where T : class { PropertyInfo[] props typeof(T).GetProperties(); Liststring columns new Liststring(); Liststring paramNames new Liststring(); ListOracleParameter paras new ListOracleParameter(); foreach (PropertyInfo prop in props) { object value prop.GetValue(entity); if (value null) { continue; // 跳过为null的字段让数据库默认值生效 } string colName EntityMapper.ToColumnName(prop.Name); string paraName : prop.Name; columns.Add(colName); paramNames.Add(paraName); paras.Add(new OracleParameter(paraName, value)); } string sql $INSERT INTO {tableName} ({string.Join(,, columns)}) VALUES ({string.Join(,, paramNames)}); using (OracleConnection conn new OracleConnection(connStr)) { using (OracleCommand cmd new OracleCommand(sql, conn)) { cmd.Parameters.AddRange(paras.ToArray()); conn.Open(); return cmd.ExecuteNonQuery(); } } }这里有个细节我得单独强调Oracle的Parameter命名不要带冒号传进去。就是说new OracleParameter(:name, value)是错的应该是new OracleParameter(name, value)。但SQL文本里要用:name。这个和SQL Server那套大相径庭SQL Server要求AddWithValue参数名带Oracle只认不带符号的名字反正就是别扭。4.4 分页查询怎么做别再ROWNUM走到黑Oracle分页是高频搜索词了确实容易搞错。SQL Server有OFFSET...FETCHMySQL有LIMITOracle的传统做法是用ROWNUM嵌套子查询。后来12c以上版本引入了FETCH FIRST子句语法稍微现代点但考虑到底下跑的还是11g老库居多我还是更习惯用ROWNUM写法。SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM T_USER ORDER BY CREATE_TIME DESC ) t WHERE ROWNUM :page * :size ) WHERE rn (:page - 1) * :size这个写法用了一个三层嵌套的经典套路最内层做排序中间层先取前N条防止后续大结果集排序太慢最外层再跳过前面的记录。如果你在写实体类映射这块分页SQL里的参数一样用OracleParameter传进去。我试过直接拿DataTable做分页数据量几千条还可以几万条以上内存直接拉满频繁GC能不用就不用了。4.5 返回DataTable与实体类的混合场景不是所有查询都需要实体类的比如一些动态报表、透视表你用实体类反而画蛇添足。这时候可以直接返回DataTable。但我要提醒的是如果你的业务逻辑最终仍然是固定字段的优先用实体类方案DataTable留给那些真正动态的场景。public static DataTable QueryDataTable(string sql, params OracleParameter[] parameters) { using (OracleConnection conn new OracleConnection(connStr)) using (OracleCommand cmd new OracleCommand(sql, conn)) using (OracleDataAdapter adapter new OracleDataAdapter(cmd)) { cmd.CommandType CommandType.Text; if (parameters ! null) { cmd.Parameters.AddRange(parameters); } DataTable dt new DataTable(); adapter.Fill(dt); return dt; } }5. 常见问题与排查技巧实录5.1 报错ORA-00904标识符无效这个报错特别常见而且十有八九是列名不对。前面说过Oracle无引号标识符会默认转大写。如果你SQL里写了userName这种双引号小写列名Oracle会严格按双引号内的内容去找找不到就报ORA-00904。我的排查套路是先跑一句SELECT column_name FROM user_tab_columns WHERE table_name T_USER;看清楚实际列名是大写还是小写、带不带下划线再回头检查实体类的属性名映射逻辑。相信我90%以上的ORA-00904都是大小写不匹配。5.2 报错ORA-01722数字无效这个多半是你把字符串当成数字传给了NUMBER类型的列。比如实体类里定义的是string属性转换成参数后Oracle试图把它往NUMBER列里塞字符串里但凡带个非数字字符立马报这个错。排查方法是看参数值到底传了什么。我曾经Debug看到一个参数值是空字符串Oracle可不认为空字符串等于NULL它会把空字符串往数字列转换直接失败。所以写插入逻辑时字符串值如果是空白最好主动转成DBNull.Value或者干脆跳过这个参数。5.3 报错ORA-01843不是有效的月份日期问题在Oracle里是重灾区。我刚转Oracle时最惊讶的一点是Oracle的DATE类型里居然带时分秒。而很多从SQL Server过来的人脑子里默认DATE就是日期。ORA-01843大部分原因是传字符串时格式不匹配比如你传了2024-01-01而会话的NLS日期格式是DD-MON-RR。解决方案很简单一律用OracleParameter传DateTime类型不要拼SQL字符串日期。参数化之后格式问题完全让驱动自己处理我后来再没怎么见过这个报错。5.4 DBNull和类型默认值纠缠不清C#这边数据库NULL落到DataReader里就是DBNull.Value直接往实体属性赋值会炸。我前面写的ChangeType方法已经判断了Convert.IsDBNull。但还有一个坑可空值类型比如int?如果数据库值是NULL你需要赋值为null而不是默认的0否则业务逻辑判断“是否为零”和“是否为空”会搅浑。实体类定义时有几个原则最好守住数据库允许NULL的数值字段C#这边用int?、decimal?数据库允许NULL的时间字段用DateTime?字符串字段不用可空修饰直接用string它本身就能接受null5.5 连接池耗尽和性能问题Oracle.ManagedDataAccess.Core默认开启连接池连接字符串里可以设置Min Pool Size和Max Pool Size。我之前遇到过一个线上问题连接池默认最大值是100某次并发一高连接全部占满后来请求全在排队等连接接口响应时间直线飚升。排查过程先看Oracle的V$SESSION视图发现大量INACTIVE会话堆积再回看代码发现有个地方using没写好异常路径下连接没释放。修复之后又在连接字符串里加了Validate Connectiontrue这样连接池取出连接时如果发现已断开会自动重连一次。这里给自己记个教训也提醒各位连接对象务必用using包裹别让连接裸奔。6. 实体类设计时的一些建议6.1 属性命名和数据库字段命名规约要统一团队协作的时候最怕一人一个风格。我建议在项目文档里明确一条规范数据库表字段用大写加下划线实体类属性用PascalCase两者之间的对应关系由通用映射器自动处理。这样新人来了不用猜照着写就行。举个例子数据库字段是USER_NAME实体属性就写UserName数据库字段是CREATE_TIME实体属性就写CreateTime。通用映射器里用正则自动互转小团队这套规矩特别好用。6.2 不要盲目追求映射所有字段有的表字段特别多几十个字段全映射出来实体类又大又笨。我建议只映射业务真正用到的字段其他比如审计字段创建人、创建时间、修改人、修改时间如果业务不需要就别进实体类。否则查询性能没提升多少反而对象体积变大序列化和传输都变慢。另一方面如果某天表结构加了字段别急着往实体类里加属性。先问问业务需不需要展示和修改不需要就别理它。这样实体类的变化频率能被你控制住不至于每次数据库变更都引发一次代码改动风暴。6.3 只读属性别参与插入更新实体类里有时候会有一些计算属性或者关联查询字段比如“总金额单价*数量”这种属性应该标记为不参与插入和更新。我在写通用Insert时默认跳过只读属性即可用prop.CanWrite判断一下。这个细节虽然小但能防止一些莫名其妙的数据问题。if (!prop.CanWrite) { continue; // 只读属性不参与映射 }7. 关于Entity Framework等ORM的选择7.1 Oracle下我用过的ORM组合说了这么多手写映射其实我也不是完全抵制ORM。项目合适的时候我也用过EF Core配Oracle还有国产的SqlSugar兼容Oracle做得也还行。比如SqlSugar它对Oracle的分页、联表、批量插入都封装得比较完善小中型项目用它提高效率是很香的。但我的态度很明确如果你打算用ORM也最好先懂底层是怎么映射的。否则遇到性能问题、遇到ORM生成的不理想的SQL你都不知道怎么排查和修正。我自己就是先手写映射踩过坑之后再看ORM的文档和源码简直一目了然很多设计意图瞬间就懂了。7.2 什么时候坚持用手写方案回到主题如果你面临这几个情况手写实体类和通用映射器反而是理性的选择项目里大量使用存储过程还要接收REF CURSOR返回的多结果集数据库是Oracle 11g这种老版本对分页等特性支持有限表字段特别多且变更频繁需要灵活的字段映射控制对性能敏感不能容忍ORM额外生成的SQL开销团队有规范要求需要精确控制每条SQL我的经验是手写方案底子打好之后日常业务开发的效率完全不输ORM而且可控性更强。通用映射器写好一次后面就是复制粘贴SQL非常顺手。最后再分享一个实操小心得写实体类的时候我习惯每个类里加一个静态的TableName字段或者在映射器里维护一个“实体类-表名”的字典。这样通用Insert、Update、Delete方法都能自动找到对应的表不用每次手动传tableName整份代码用起来更顺手。一开始可能觉得多此一举等实体类多到几百个的时候你会发现这个设计的价值。本文还有配套的精品资源点击获取