C#操作Oracle数据库的实体类创建与通用映射方案
2026/9/7 5:57:48 网站建设 项目流程

简介:面向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.NET(Oracle 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固定长度,取出可能带空格,记得Trim
DATEDateTime包含时分秒,时刻注意
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)就是int,NUMBER(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 Framework,T4模板是个不错的选择。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 List<T> Map<T>(IDataReader reader) where T : class, new() { List<T> list = new List<T>(); // 先拿到实体类的所有属性 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 8,Oracle数据库版本是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和参数,返回List<T>。这里有三个关键点想重点说:

  • 连接字符串里推荐加上Pooling=true,这个能极大减少频繁开关连接的开销。高并发场景下没有连接池,你的程序基本会被连库操作拖垮。
  • 参数化查询是底线,不要拼接SQL,无论是防SQL注入还是处理日期格式问题,都能省太多心。
  • Oracle的游标和SQL Server不一样,存储过程输出结果集时必须显式声明REF CURSOR,这个在调用存过的时候要特别留意。

来看一段通用查询的实际代码。

public static class OracleHelper { private static string connStr = ConfigurationManager.ConnectionStrings["OracleConn"].ConnectionString; public static List<T> Query<T>(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.Map<T>(reader); } } } } }

这段代码看着简单,但它把连接生命周期、命令执行、DataReader关闭、实体映射全串起来了。调用方是这样的:

string sql = "SELECT ID, USER_NAME, CREATE_TIME FROM T_USER WHERE STATUS = :status"; var users = OracleHelper.Query<UserInfo>(sql, new OracleParameter("status", 1));

注意Oracle的参数占位符,用冒号而不是@,这一点跟SQL Server截然不同。很多从SQL Server转过来的朋友,第一遍写代码必踩这个坑,编辑器又不提示,运行时直接报“ORA-01036:非法的变量名/编号”,心态容易崩。

4.3 通用插入方法,避免手写VALUES

插入和查询一样,手写INSERT语句也是一场体力活。尤其字段特别多的表,写错一个顺序,数据就进了错误的列,调半天才发现是字段对齐错了。所以我也做了一个通用插入方法:拿实体的属性列表,自动拼出列名和参数名。

public static int Insert<T>(T entity, string tableName) where T : class { PropertyInfo[] props = typeof(T).GetProperties(); List<string> columns = new List<string>(); List<string> paramNames = new List<string>(); List<OracleParameter> paras = new List<OracleParameter>(); 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...FETCH,MySQL有LIMIT,Oracle的传统做法是用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"。解决方案很简单:一律用OracleParameterDateTime类型,不要拼SQL字符串日期。参数化之后,格式问题完全让驱动自己处理,我后来再没怎么见过这个报错。

5.4 DBNull和类型默认值纠缠不清

C#这边数据库NULL落到DataReader里就是DBNull.Value,直接往实体属性赋值会炸。我前面写的ChangeType方法已经判断了Convert.IsDBNull。但还有一个坑:可空值类型,比如int?,如果数据库值是NULL,你需要赋值为null而不是默认的0,否则业务逻辑判断“是否为零”和“是否为空”会搅浑。

实体类定义时有几个原则最好守住:

  • 数据库允许NULL的数值字段,C#这边用int?decimal?
  • 数据库允许NULL的时间字段,用DateTime?
  • 字符串字段不用可空修饰,直接用string,它本身就能接受null

5.5 连接池耗尽和性能问题

Oracle.ManagedDataAccess.Core默认开启连接池,连接字符串里可以设置Min Pool SizeMax Pool Size。我之前遇到过一个线上问题:连接池默认最大值是100,某次并发一高,连接全部占满,后来请求全在排队等连接,接口响应时间直线飚升。

排查过程:先看Oracle的V$SESSION视图,发现大量INACTIVE会话堆积,再回看代码,发现有个地方using没写好,异常路径下连接没释放。修复之后,又在连接字符串里加了Validate Connection=true,这样连接池取出连接时如果发现已断开,会自动重连一次。这里给自己记个教训,也提醒各位:连接对象务必用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,整份代码用起来更顺手。一开始可能觉得多此一举,等实体类多到几百个的时候,你会发现这个设计的价值。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询