简介:这份资源面向需要在 C# 项目中对接 Oracle 数据库的开发者,尤其适合希望摆脱手写实体类、借助 ORM 提升数据访问层效率的中级 .NET 程序员。内容围绕 OracleCodeGenerator 工具展开,涵盖 Entity Framework 与 Oracle 提供程序的配合、连接字符串配置、实体类生成与设计原则,以及 DbContext 使用、Code First 迁移和 Repository 模式等扩展思路,可帮助读者快速搭建与 Oracle 表结构对应的模型层。压缩包为 rar 格式,共 62 个文件,约 2.99MB,以 18 个 cs 源码文件为核心,辅以 dll 依赖库、xaml 界面文件、xml 配置与少量 exe、ico 等资源,构成一个可直接运行的 WPF 生成器工程。目前已有 214 人学习下载,适合作为 C# 与 Oracle 交互场景下的实体类生成参考与二次开发基础。
1. 从一张手工维护的实体类表说起
接手一个 C# 加 Oracle 的老项目时,最先让人头疼的往往不是业务逻辑,而是数据访问层里那几十个手写的实体类。表结构一改,字段名一换,编译能过、运行才炸,排查起来像开盲盒。这个项目的定位很明确:围绕 C# 与 Oracle 的组合,把「数据库表结构 → 实体类代码」这条链路自动化,减少手抄字段带来的低级错误。它适合正在维护 Oracle 后台系统、需要频繁同步表结构与 C# 模型的开发者,也适合想理解代码生成器内部怎么拼字符串、怎么读元数据的人。下面按「能干什么、怎么跑、怎么改」的顺序拆开讲。
2. 项目定位与技术选型:为什么是 C# 直连 Oracle 做代码生成
2.1 它解决的不是 ORM 问题,而是模型同步问题
很多人第一反应是「有 Entity Framework 或 Dapper 了,还要实体类生成器干嘛」。这个项目的价值不在运行时映射,而在开发期的模型同步。Oracle 里一张业务表动辄三四十个字段,类型还杂:NUMBER(10)、VARCHAR2(200)、DATE、CLOB。手写实体类时,decimal还是int、DateTime还是string,全靠人记。生成器的做法是读 Oracle 的数据字典视图,把列名、数据类型、精度、可空性一次性拉出来,再按映射规则拼成 C# 类。它不替代 ORM,而是给 ORM 提供准确的模型输入。
技术选型上,这类工具通常走两条路:一是纯System.Data.OracleClient或第三方驱动直连,二是通过ODP.NET(Oracle 官方 .NET 驱动)。项目正文没有指定驱动,按这个场景下合格开发最可能用的做法,我会选Oracle.ManagedDataAccess,因为它免装客户端、部署干净,连接字符串里配好Data Source就能跑。生成端用控制台程序最省事,输出.cs文件到指定目录,再让主项目引用。
2.2 元数据从哪来:USER_TAB_COLUMNS 是主战场
Oracle 的数据字典是生成器的「黑匣子」入口。最常用的是USER_TAB_COLUMNS,它只返回当前 schema 下的表列信息;如果要跨 schema,就换ALL_TAB_COLUMNS并加OWNER条件。关键字段包括TABLE_NAME、COLUMN_NAME、DATA_TYPE、DATA_LENGTH、DATA_PRECISION、DATA_SCALE、NULLABLE、COLUMN_ID。COLUMN_ID决定字段顺序,生成时按它排序,实体类字段顺序就和表结构一致,读代码时不容易错位。
-- 读取指定表的列元数据,按列顺序排列 SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COLUMN_ID FROM USER_TAB_COLUMNS WHERE TABLE_NAME = 'T_ORDER' ORDER BY COLUMN_ID;这段查询是生成器的数据源。DATA_TYPE返回的是 Oracle 类型名,比如NUMBER、VARCHAR2;DATA_PRECISION和DATA_SCALE只在NUMBER上有意义,用来判断该映射成int、long还是decimal。NULLABLE为Y时,值类型字段要加?变成可空,否则插入空值会抛异常。参数上唯一要改的是TABLE_NAME,实际工具里会做成循环,把目标表清单逐个查一遍。
2.3 目录结构:一个控制台加一个模板目录
按常见做法,项目结构大致如下,生成器本身不复杂,重点是模板和映射规则分离:
OracleEntityGen/ ├── Program.cs // 入口,读配置、遍历表、写文件 ├── Config/ │ └── appsettings.json // 连接串、表清单、输出目录 ├── Metadata/ │ └── OracleSchemaReader.cs // 查数据字典,返回列信息集合 ├── Mapping/ │ └── TypeMapper.cs // Oracle 类型 → C# 类型 ├── Template/ │ └── EntityTemplate.cs // 拼类名、属性、命名空间 └── Output/ // 生成的 .cs 文件落这里把「读元数据」「类型映射」「模板拼装」拆成三个类,是为了后面改规则时不动其他部分。比如某张表的NUMBER(1)想映射成bool而不是int,只改TypeMapper就行;想给生成的类加[Serializable],只改模板。这种分层在生成器里很值,因为生成规则会随项目演进反复调。
3. 核心模块拆解:从查表到落盘的完整链路
3.1 连接与读取:把列信息装进一个强类型集合
先定义一个承载列元数据的类,字段和查询结果一一对应。这样后面映射时不用再碰DataTable,类型安全,调试也直观。
public class ColumnInfo { public string ColumnName { get; set; } public string DataType { get; set; } public int DataLength { get; set; } public int? Precision { get; set; } public int? Scale { get; set; } public bool IsNullable { get; set; } public int ColumnId { get; set; } }读取时用OracleCommand执行前面的 SQL,逐行填充。注意DATA_PRECISION和DATA_SCALE在非NUMBER列上可能是DBNull,取值前要判空,否则Convert.ToInt32直接翻车。NULLABLE读出来是字符串Y/N,转bool时用== "Y"判断,别用Convert.ToBoolean,它对"N"的处理和直觉不一致。
public List<ColumnInfo> ReadColumns(string tableName) { var list = new List<ColumnInfo>(); using var conn = new OracleConnection(_connectionString); conn.Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = @"SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COLUMN_ID FROM USER_TAB_COLUMNS WHERE TABLE_NAME = :t ORDER BY COLUMN_ID"; cmd.Parameters.Add(":t", OracleDbType.Varchar2).Value = tableName; using var reader = cmd.ExecuteReader(); while (reader.Read()) { list.Add(new ColumnInfo { ColumnName = reader.GetString(0), DataType = reader.GetString(1), DataLength = reader.GetInt32(2), Precision = reader.IsDBNull(3) ? null : reader.GetInt32(3), Scale = reader.IsDBNull(4) ? null : reader.GetInt32(4), IsNullable = reader.GetString(5) == "Y", ColumnId = reader.GetInt32(6) }); } return list; }参数用:t绑定而不是字符串拼接,一是防注入,二是 Oracle 对字面量大小写敏感,绑定值能避免T_ORDER和t_order查不到的问题。连接串里User Id对应的 schema 决定了USER_TAB_COLUMNS的范围,跨 schema 时记得换视图并加OWNER条件。
3.2 类型映射:NUMBER 的精度决定一切
Oracle 到 C# 的映射里,NUMBER是最容易出错的。NUMBER(1)到NUMBER(9)可以放int,NUMBER(10)到NUMBER(18)用long,再大或者带小数就用decimal。VARCHAR2、NVARCHAR2、CHAR统一映射string;DATE和TIMESTAMP映射DateTime;CLOB映射string;BLOB映射byte[]。这套规则写成switch最清楚。
public static string MapToCSharp(ColumnInfo col) { var t = col.DataType.ToUpperInvariant(); switch (t) { case "NUMBER": if (col.Scale is > 0) return "decimal"; // 有小数位 if (col.Precision is null or > 18) return "decimal"; if (col.Precision <= 9) return "int"; return "long"; case "VARCHAR2": case "NVARCHAR2": case "CHAR": case "NCHAR": case "CLOB": return "string"; case "DATE": case "TIMESTAMP": return "DateTime"; case "BLOB": return "byte[]"; default: return "string"; // 未知类型兜底,生成后人工确认 } }Scale大于 0 说明有小数,必须decimal,用double会在金额计算上丢精度,这是血泪经验。Precision为空的情况出现在没指定精度的NUMBER上,按decimal处理最稳。兜底返回string不是偷懒,是让生成结果至少能编译,未知类型在代码里一眼能看出来,比生成一个错误类型导致运行时异常强。
3.3 模板拼装:类名、属性、可空标记一次成型
模板部分本质是字符串拼装,但有几个细节决定生成质量。类名默认用表名转帕斯卡命名,去掉T_前缀;属性名用列名转帕斯卡;可空列的值类型加?。string和byte[]本身是引用类型,不需要?,但可以加= string.Empty初始化,避免空引用。
public string BuildEntity(string tableName, List<ColumnInfo> columns) { var className = ToPascal(tableName.Replace("T_", "")); var sb = new StringBuilder(); sb.AppendLine("using System;"); sb.AppendLine("namespace Generated.Entities"); sb.AppendLine("{"); sb.AppendLine($" public class {className}"); sb.AppendLine(" {"); foreach (var col in columns) { var csType = TypeMapper.MapToCSharp(col); var propName = ToPascal(col.ColumnName); var nullable = col.IsNullable && IsValueType(csType) ? "?" : ""; sb.AppendLine($" public {csType}{nullable} {propName} {{ get; set; }}"); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); }ToPascal负责把ORDER_NO转成OrderNo,下划线分段后首字母大写。IsValueType判断int、long、decimal、DateTime这些值类型,只有它们才需要?。生成的文件按类名命名,落到Output目录,主项目用「添加现有项」或干脆把目录做成链接引用。这样表结构变了,重跑一次生成器,覆盖旧文件即可。
4. 跑起来与避坑:连接、命名、覆盖三件事
4.1 最小可运行步骤
先把Oracle.ManagedDataAccess通过 NuGet 装进控制台项目,然后在appsettings.json里配连接串和表清单。连接串形如User Id=your_schema;Password=your_pwd;Data Source=host:1521/service_name,Data Source的写法取决于监听配置,service_name和SID别混。表清单用一个字符串数组,循环调用ReadColumns和BuildEntity,最后File.WriteAllText落盘。跑之前确认当前数据库用户对目标表有查询权限,USER_TAB_COLUMNS只显示当前用户拥有的表,权限不够会查不到列,生成出来是空类。
4.2 常见问题排查:现象、原因、解决
现象一:生成的类字段全是string。原因通常是DATA_TYPE读出来带尾随空格或大小写不一致,switch没命中。解决是在MapToCSharp里先Trim().ToUpperInvariant(),再进switch,别假设数据库返回值干净。
现象二:NUMBER列生成int但运行时报溢出。原因是Precision为null时被当成了小精度。解决是把Precision is null单独归到decimal分支,宁可宽不可窄,金额和主键尤其如此。
现象三:可空列生成后插入null仍报错。检查IsNullable判断是不是用了Convert.ToBoolean("N"),它返回true,导致可空标记反了。统一用== "Y"判断,简单可靠。
现象四:重跑生成器把手工加的注释冲掉了。这是覆盖式生成的通病。解决是把手工扩展放到partial类的另一个文件里,生成文件只放属性,扩展文件放方法、特性和注释,重跑不丢。
现象五:表名带小写或特殊字符,类名生成非法。Oracle 默认表名大写,但用双引号建的表会保留大小写。解决是在ToPascal里过滤非字母数字字符,首字符是数字时前面补下划线,保证生成的标识符合法。
提示:生成器第一次跑建议只选一张表,人工核对生成结果和表结构是否一致,确认映射规则无误后再批量跑,避免一次性覆盖几十个文件后才发现规则错了。
5. 二次开发:把生成器改成团队能长期用的工具
生成器跑通只是起点,真正让它留在项目里的是可维护性。我一般会做三件事。第一,把表清单从硬编码挪到配置文件,支持按前缀过滤,比如只生成T_开头的表,避免把日志表、临时表也生成进来。第二,给生成的类加partial关键字,手工扩展放独立文件,这样每次重跑不丢业务方法。第三,加一个「差异检查」模式:生成到内存后和磁盘上已有文件比对,内容一致就跳过写入,减少无意义的文件变更,代码评审时 diff 也干净。
// 差异检查:内容相同则不落盘,避免无意义覆盖 var newContent = generator.BuildEntity(table, columns); var path = Path.Combine(outputDir, $"{className}.cs"); if (File.Exists(path) && File.ReadAllText(path) == newContent) { Console.WriteLine($"skip {className}, no change"); continue; } File.WriteAllText(path, newContent);这段逻辑的价值在团队协作里很明显:生成器重跑不会产生一堆「只改了时间戳」的提交,评审时只看真正变化的字段。参数上outputDir建议放在主项目的Entities/Generated子目录,和手工代码物理隔离,谁改了生成文件一眼能看出来。
再往深一层,可以把类型映射规则也做成配置,比如某张表的STATUS列想映射成枚举而不是int,就在配置里加一条「表名 + 列名 → 自定义类型」的覆盖规则,TypeMapper先查覆盖表再走默认逻辑。这样生成器从「一次性脚本」变成「团队约定」的载体,新表接入时不用再讨论字段类型怎么定。
从那以后我每次改表结构,都强制先跑一遍生成器再提交,生成结果和 DDL 一起进版本库,谁改了什么一目了然。希望帮到你。
本文还有配套的精品资源,点击获取