ARTICLE DETAIL

资讯详情

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

C# 操作 Oracle 实体类生成器:从数据字典到 Dapper

C# 操作 Oracle 实体类生成器:从数据字典到 Dapper 简介面向需要在C#项目中对接Oracle数据库的.NET开发者这份资源提供了OracleCodeGenerator实体类生成工具的完整RAR工程包。工具基于Entity Framework思路通过读取数据库表/视图元数据自动生成带特性注解的C#实体类可显著减少手动编写数据访问层代码的工作量。压缩包共62个文件约2.99MB以18个.cs源码文件为核心包含MainWindow.xaml界面、ViewModel、DBHelper与XMLHelper等辅助类另有.dll类库、.xml配置及.sln/.csproj工程文件结构完整可直接打开调试。已有211人学习下载。源码覆盖了连接字符串构建、[Key]/[Table]注解用法、DbContext封装及Code First迁移思路适合希望掌握C#Oracle数据访问或学习桌面工具开发的初中级程序员参考。1. C# 操作 Oracle 时实体类不该再手写一篇生成器的拆解笔记接手 MES 工单模块那阵子数据库里有 30 多张表Oracle 字段又是ORDER_NO、CREATE_DT这种大写加下划线的风格手写实体类写到凌晨三点才完成两张表。后来我写了一个小工具直接从 Oracle 数据字典读表结构生成带注释、带主键标记的实体类几分钟搞定全套。这篇文章把这条“从数据库到 C# 实体类”的完整链路拆开讲元数据怎么读、类型怎么映射、生成后的实体类怎么配合 Dapper 落地以及我实际项目中踩过的几个坑。适合正在用 C# 做上位机、MES、ERP 等 Oracle 后端系统的开发者尤其适合想摆脱重复劳动的这部分人。2. 从 Oracle 数据字典到 C# 类型读表结构的 SQL 与映射表2.1 为什么用数据字典而不是手写字段清单做实体类生成器第一步是拿到准确、完整的表结构。有人在代码里维护一个字段清单数组循环生成但 Oracle 的表结构经常被 DBA 调整加字段、改长度、补注释你维护的清单和数据库实际结构很快就对不上了后来变成一场“到底谁是对的”的玄学。正确做法是直接查 Oracle 自带的元数据视图。用得最多的是这三个视图作用关键列USER_TAB_COLUMNS当前用户下表的所有列定义COLUMN_NAME、DATA_TYPE、DATA_LENGTH、DATA_PRECISION、DATA_SCALE、NULLABLEUSER_COL_COMMENTS列的注释COLUMN_NAME、COMMENTSUSER_CONSTRAINTS / USER_CONS_COLUMNS主键约束信息CONSTRAINT_TYPE、COLUMN_NAME、POSITION为什么用User_前缀而不是All_或DBA_普通业务账号通常只有自己 schema 的权限User_视图不需要额外授权查出来就是当前登录用户拥有的表。如果要跨 schema 读取才改用All_Tab_Columns并过滤OWNER字段。我做生成器时默认和业务库用同一个账号连接所以User_前缀最稳妥。另外还需要一个查询所有业务表的语句用来决定生成哪些实体类。我一般过滤掉系统表SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME NOT LIKE BIN$% ORDER BY TABLE_NAME过滤条件里的BIN$%是 Oracle 回收站里的表名格式如果不排除生成器会把一堆已删除的表也扫出来生成一串根本没用的类。2.2 读取表结构的核心 SQL 与 C# 封装先看读取列定义和注释的 SQL一条 join 搞定SELECT tc.COLUMN_ID, tc.COLUMN_NAME, tc.DATA_TYPE, tc.DATA_LENGTH, tc.DATA_PRECISION, tc.DATA_SCALE, tc.NULLABLE, cc.COMMENTS FROM USER_TAB_COLUMNS tc LEFT JOIN USER_COL_COMMENTS cc ON tc.TABLE_NAME cc.TABLE_NAME AND tc.COLUMN_NAME cc.COLUMN_NAME WHERE tc.TABLE_NAME :tableName ORDER BY tc.COLUMN_ID逻辑说明COLUMN_ID是字段在表里的物理顺序ORDER BY它保证生成实体类时属性顺序和表结构一致。LEFT JOIN注释表是因为 Oracle 允许某些列没有注释这时候COMMENTS会是 NULL生成器里要给它默认空字符串。DATA_PRECISION和DATA_SCALE是判断 NUMBER 类型映射成 int、long 还是 decimal 的关键不能省。再查主键SELECT col.COLUMN_NAME FROM USER_CONSTRAINTS cons JOIN USER_CONS_COLUMNS col ON cons.CONSTRAINT_NAME col.CONSTRAINT_NAME WHERE cons.TABLE_NAME :tableName AND cons.CONSTRAINT_TYPE P ORDER BY col.POSITIONCONSTRAINT_TYPE P只取主键约束ORDER BY col.POSITION保证复合主键的顺序正确。之所以单独查主键是因为生成实体类时我想给主键属性自动加上[Key]特性后面对接 Dapper 或 EF Core 都能少写配置。对应的 C# 读取方法我用OracleDataReader直接取不引入额外 ORMpublic ListColumnMeta LoadColumns(string tableName) { const string sql SELECT tc.COLUMN_NAME, tc.DATA_TYPE, tc.DATA_LENGTH, tc.DATA_PRECISION, tc.DATA_SCALE, tc.NULLABLE, cc.COMMENTS FROM USER_TAB_COLUMNS tc LEFT JOIN USER_COL_COMMENTS cc ON tc.TABLE_NAME cc.TABLE_NAME AND tc.COLUMN_NAME cc.COLUMN_NAME WHERE tc.TABLE_NAME :tableName ORDER BY tc.COLUMN_ID; var list new ListColumnMeta(); using (var cmd new OracleCommand(sql, _conn)) { cmd.Parameters.Add(new OracleParameter(tableName, tableName.ToUpperInvariant())); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new ColumnMeta { ColumnName reader[COLUMN_NAME].ToString(), DataType reader[DATA_TYPE].ToString(), DataLength Convert.ToInt32(reader[DATA_LENGTH]), Precision reader[DATA_PRECISION] DBNull.Value ? 0 : Convert.ToInt32(reader[DATA_PRECISION]), Scale reader[DATA_SCALE] DBNull.Value ? 0 : Convert.ToInt32(reader[DATA_SCALE]), IsNullable reader[NULLABLE].ToString() Y, Comments reader[COMMENTS]?.ToString() ?? }); } } } return list; }代码里几个参数细节命令参数用:tableName而不是tableName这是 Oracle 的绑定变量风格和 SQL Server 不一样写错会直接报 ORA-00936 缺表达式。tableName强制ToUpperInvariant()因为 Oracle 默认把没加引号的表名存成大写强制大写能避免“明明有表却查不到”的诡异情况。Precision和Scale对非 NUMBER 字段本来就是空值读到 DBNull 要先判空否则Convert直接抛异常。ColumnMeta是生成器中间层的核心数据结构字段名和 Oracle 列名一一对应后面写生成器时直接从这个列表取数。2.3 类型映射表Oracle 类型到底对应什么 C# 类型拿到元数据后最见功夫的一步是类型映射。Oracle 的 NUMBER 很特殊不带精度时等价于 .NET 的 decimal带精度时可能是 int、long。我的映射表经过几个项目验证Oracle 类型C# 类型判断条件NUMBERdecimal兜底NUMBER(P, S)int / long / decimalS0 且 P9 用 intP18 用 long否则 decimalVARCHAR2 / NVARCHAR2 / CHAR / NCHARstring直接映射DATEDateTime直接映射TIMESTAMP / TIMESTAMP(6)DateTime直接映射TIMESTAMP WITH TIME ZONEDateTimeOffset保留时区精度不丢FLOAT / BINARY_FLOAT / BINARY_DOUBLEdouble直接映射CLOB / NCLOBstring大数据文本BLOB / RAWbyte[]二进制ROWIDstring一般用不到兜底NUMBER 的精度判断单独拎出来这里是大多数实体类出错的地方public static string MapOracleType(ColumnMeta col) { string dt col.DataType.ToUpperInvariant(); if (dt NUMBER) { if (col.Scale 0 col.Precision 0) { if (col.Precision 9) return int; if (col.Precision 18) return long; } return decimal; } switch (dt) { case VARCHAR2: case NVARCHAR2: case CHAR: case NCHAR: case CLOB: case NCLOB: case ROWID: return string; case DATE: case TIMESTAMP: return DateTime; case TIMESTAMP WITH TIME ZONE: return DateTimeOffset; case FLOAT: case BINARY_FLOAT: case BINARY_DOUBLE: return double; case BLOB: case RAW: case LONG RAW: return byte[]; default: return string; } }判断顺序的逻辑NUMBER 单独处理其他类型走 switch。Oracle 的 DATE 和 TIMESTAMP 底层实现不同但 .NET 端都用 DateTime 承载C# 的 DateTime 精度是 100ns容纳 Oracle 的微秒级时间戳没有压力而TIMESTAMP WITH TIME ZONE如果映射成 DateTime时区偏移就丢了所以映射成DateTimeOffset更准确。提示如果列允许 NULL 且映射结果是值类型我建议在生成阶段直接改成int?、DateTime?。数据库里允许为空的列远比想象的多这一步不做后面用 DataReader 读出来十个里有八个要炸。3. 照着模板生成实体类从列元数据到 .cs 文件3.1 生成器的整体流程与目录规划元数据读取和类型映射就绪后生成器本身反而简单它是一个“模板 循环”的组合。流程是连接 Oracle读取需要生成的表列表逐表读取列元数据映射 C# 类型拼接代码最后写 .cs 文件。我不会做图形界面命令行传一个表名或者逗号分隔的表名列表跑完直接输出文件。这样在 Visual Studio 的外部工具里能调用也能集成到 CI 脚本里。输出目录按命名空间分层Entities目录放实体类Repositories目录放仓储类如果项目需要接口层再补IRepository。文件命名规则也很重要WIP_ORDER表生成WipOrder.csMAT_STOCK生成MatStock.cs一下就能看出对应关系。命名空间我一般放在生成器的配置项里不同模块可以输出到不同命名空间避免整个项目所有实体类堆在一个 namespace 里。3.2 实体类模板从注释到 [Key] 特性有了ColumnMeta列表之后生成实体类的核心方法是拼字符串。C# 里用StringBuilder可读性比满屏string.Format好生成几百行代码时性能也够用public static string GenerateEntity(string tableName, ListColumnMeta columns, Liststring primaryKeys) { var sb new StringBuilder(); string className ToPascalCase(tableName); sb.AppendLine(using System;); sb.AppendLine(using System.ComponentModel.DataAnnotations;); sb.AppendLine(using System.ComponentModel.DataAnnotations.Schema;); sb.AppendLine(); sb.AppendLine($namespace YourProject.Entities); sb.AppendLine({); sb.AppendLine($ /// summary); sb.AppendLine($ /// 数据表: {tableName}); sb.AppendLine($ /// /summary); sb.AppendLine($ [Table(\{tableName}\)]); sb.AppendLine($ public class {className}); sb.AppendLine( {); foreach (var col in columns) { string propName ToPascalCase(col.ColumnName); string csharpType MapOracleType(col); if (col.IsNullable IsValueType(csharpType)) { csharpType ?; } sb.AppendLine( /// summary); sb.AppendLine($ /// {col.Comments}); sb.AppendLine( /// /summary); if (primaryKeys.Contains(col.ColumnName.ToUpperInvariant())) { sb.AppendLine( [Key]); } if (propName ! col.ColumnName) { sb.AppendLine($ [Column(\{col.ColumnName}\)]); } sb.AppendLine($ public {csharpType} {propName} {{ get; set; }}); sb.AppendLine(); } sb.AppendLine( }); sb.AppendLine(}); return sb.ToString(); }模板里有几个细节值得单独说。ToPascalCase是下划线转驼峰的命名函数把CREATE_DT变成CreateDt。Oracle 字段名大多是“大写 下划线”风格直接拿来当属性名既难看又容易踩 C# 保留字。我在实现里做了一个保留字集合遇到string、class、event这些词属性名会变成string这种转义写法同时补[Column]特性显式标注数据库列名保证 ORM 映射正确public static string ToPascalCase(string name) { var parts name.Split(_); var sb new StringBuilder(); foreach (var part in parts) { if (part.Length 0) continue; sb.Append(char.ToUpperInvariant(part[0])); sb.Append(part.Substring(1).ToLowerInvariant()); } string result sb.ToString(); if (_csharpKeywords.Contains(result)) result result; return result; }这段逻辑不复杂但_csharpKeywords集合一定要全。我维护了 C# 的全部保留字和上下文关键字比如event、operator、params。有一个漏网的生成出来的代码就编不过CI 里第一次跑编译直接翻车。[Table]、[Key]、[Column]这三个特性来自System.ComponentModel.DataAnnotations和Schema命名空间是 .NET 自带的不依赖第三方 ORM。它们的作用是后续无论接 EF Core、Dapper.Contrib 还是 SqlSugar都能被识别。Dapper 扩展库会读[Key]和[Table]GetT、InsertT这些方法靠它定位主键和表名所以这些特性不是装饰是实打实的功能。3.3 从表名到输出文件跑一个真实例子生成器的主入口做成控制台程序命令行参数是表名列表EntityGen.exe --tablesWIP_ORDER,MAT_STOCK --output./Entities控制台打印每个表的生成结果并在Entities目录下生成WipOrder.cs、MatStock.cs。下面是WIP_ORDER表生成出来的实体类关键片段using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace YourProject.Entities { /// summary /// 数据表: WIP_ORDER /// /summary [Table(WIP_ORDER)] public class WipOrder { /// summary /// 工单ID /// /summary [Key] public long Id { get; set; } /// summary /// 工单号 /// /summary public string OrderNo { get; set; } /// summary /// 计划数量 /// /summary public decimal PlanQty { get; set; } /// summary /// 完成数量 /// /summary public decimal? FinishQty { get; set; } } }生成结果里有两处明显是生成器干的而不是手写的[Key]是主键查询自动打上的FinishQty是可空 decimal因为表里该列允许 NULL。生成器跑完后我习惯做三步验证第一步编译整个项目看语法错误第二步写一个冒烟测试用 Dapper 查一条真实数据映射到WipOrder断言主键和关键字段值第三步用SqlPlus描述表结构对比生成的实体类属性确认没有漏字段。三步走完这个表才算被吃透。4. 实体类落地用 Dapper 把增删改查封装成泛型仓储4.1 为什么选 Dapper 而不是 EF Core 或纯 ADO.NET实体类生成出来只是第一步真正让它干活的是数据访问层。我在 C# 上位机和 MES 系统里习惯用 Dapper理由有三点第一它只是一个扩展方法库不抢控制权项目里现有 ADO.NET 代码可以平滑共存第二对 Oracle 的支持不需要额外配置底层是Oracle.ManagedDataAccess.Core就能用第三绕开了 EF Core 在 Oracle 下的模型映射和迁移机制出问题容易排查。EF Core 当然更“正规”但拿到生成的实体类后还得配置 DbContext、配置主键策略、处理表名大小写对已经有大量存储过程和手写 SQL 的老系统来说学习成本和重构成本都偏高。而纯 ADO.NET 写增删改查代码重复得让人头疼。Dapper 是中间那条最省力的路。4.2 泛型仓储增删改查的完整实现下面这份泛型仓储配合生成器产出的实体类可直接用。我用反射读取实体的属性名作为列名再用 Dapper 的参数化机制绑定值public class OracleRepositoryT where T : class { private readonly string _tableName; private readonly string _keyName; private readonly OracleConnection _conn; public OracleRepository(OracleConnection conn) { _conn conn; var attrs typeof(T).GetCustomAttributes(typeof(TableAttribute), false); _tableName attrs.Length 0 ? ((TableAttribute)attrs[0]).Name : typeof(T).Name; var keyProp typeof(T).GetProperties() .FirstOrDefault(p p.GetCustomAttributes(typeof(KeyAttribute), false).Any()); _keyName keyProp?.Name ?? Id; } public T GetById(object id) { var sql $SELECT * FROM {_tableName} WHERE {_keyName} :id; return _conn.QuerySingleOrDefaultT(sql, new { id }); } public IEnumerableT GetList(string whereClause, object param null) { var sql $SELECT * FROM {_tableName}; if (!string.IsNullOrWhiteSpace(whereClause)) sql WHERE whereClause; return _conn.QueryT(sql, param); } public int Insert(T entity) { var props typeof(T).GetProperties() .Where(p p.Name ! _keyName) .ToList(); var colNames string.Join(,, props.Select(p p.Name)); var paramNames string.Join(,, props.Select(p : p.Name)); var sql $INSERT INTO {_tableName} ({colNames}) VALUES ({paramNames}); return _conn.Execute(sql, entity); } public int Update(T entity) { var props typeof(T).GetProperties() .Where(p p.Name ! _keyName) .ToList(); var setClause string.Join(,, props.Select(p ${p.Name} :{p.Name})); var sql $UPDATE {_tableName} SET {setClause} WHERE {_keyName} :{_keyName}; return _conn.Execute(sql, entity); } public int Delete(object id) { var sql $DELETE FROM {_tableName} WHERE {_keyName} :id; return _conn.Execute(sql, new { id }); } }这里有一个关键点SQL 里的参数占位符是:idDapper 的匿名对象属性名是id大小写正好对上。Oracle 的绑定变量名不区分大小写所以new { id }能被正确匹配。如果换 SQL Server要把:id改成id这个差异是 Dapper 使用者第一次接触 Oracle 时最常见的翻车点。Insert和Update都假定实体类属性名与数据库列名一致。这就是生成器带来的红利属性名是列名的驼峰版本并靠[Column]特性保留原名仓储层不需要再做列名映射。如果手写的实体类命名不规范这个仓储就得改成反射读取[Column]特性复杂度上一个台阶。再补充一点主键是用[Key]特性识别出来的万一实体类没有构造函数里有默认值Id兜底。但如果是复合主键这个仓储就不够用了要么加一个GetByCompositeKey方法要么干脆让生成器只处理单主键表。实际业务里 90% 的表是单主键我后来选了后者把复合主键表在生成时就标记为“跳过仓储”省下来的复杂度是值得的。4.3 用生成器配合仓储一个完整的查询示例生成实体类 Dapper 仓储组合起来业务层写查询时几乎不重复 SQL。比如查最近一周创建的工单using (var conn new OracleConnection(connectionString)) { var repo new OracleRepositoryWipOrder(conn); var orders repo.GetList( CREATE_DT :beginTime AND CREATE_DT :endTime, new { beginTime DateTime.Today.AddDays(-7), endTime DateTime.Today.AddDays(1) }); foreach (var order in orders) { Console.WriteLine(${order.OrderNo}: {order.PlanQty}/{order.FinishQty}); } }这个示例说明三件事第一whereClause 拼进 SQL参数值用匿名对象传不会有 SQL 注入风险第二Oracle 时间比较传 C# 的DateTime是安全的底层驱动自动转成正确的绑定变量第三FinishQty是可空 decimal打印时用字符串插值不崩但直接做算术要先判空。我的习惯是复杂查询多表 join、窗口函数不往仓储里塞直接写 SQL 扔给 Dapper因为那种查询根本谈不上复用只有单体简单增删改查才用仓储省掉的是最枯燥的那部分代码。5. 避坑清单NUMBER 精度、空值和保留字的五个实战坑5.1 NUMBER(1) 被映射成 bool读数据直接报错现象Oracle 表里有IS_DELETED NUMBER(1)生成器把这一列映射成bool运行时报错类型转换失败改了好一会儿才发现是这一列。原因Oracle 没有布尔类型业务上用 0/1 表示真伪。但驱动把 NUMBER(1) 转成 bool 的规则并不统一不同版本的Oracle.ManagedDataAccess行为有差异有的能转有的直接抛异常。不能把“看起来像 bool”当成“一定可以转成 bool”。解决生成器的映射表里把NUMBER(1)显式映射成short而不是 bool。业务层需要 bool 时用IsDeleted entity.Deleted ! 0手动转换底层一定不会崩。这是血泪教训换来的从那以后生成器对精度为 1、刻度为 0 的 NUMBER 单独拦截不再依赖驱动行为这种玄学。5.2 数据字典查出来全是空表名大小写没对上现象传入小写表名wip_order查USER_TAB_COLUMNS返回 0 行改成大写WIP_ORDER就正常。原因Oracle 在创建表时如果表名没加双引号会默认转成大写存进数据字典。只有建表时用了CREATE TABLE wip_order它才会保留小写。生成器如果传进去的是小写而数据字典里存的是大写WHERE TABLE_NAME :tableName就匹配不上。解决读取元数据入口处统一做一轮归一化tableName.Trim().ToUpperInvariant()。但强制大写只适用于按大写建表的场景如果数据库里确实有小写表光转大写也不行。我的做法是“先精确匹配查不到再转大写重查”两轮查询适配两种建表风格。5.3 DBNull 读进值类型属性实体类没可空化现象列表查询本来好好的某天某行数据的数值字段是 NULL直接抛InvalidCastException。翻日志发现实体类的PlanQty是decimal数据库存了 NULL。原因生成器做类型映射时没把“允许 NULL 的列”处理成可空类型。Dapper 在reader.GetValue时把 DBNull 塞给非空值类型没有隐式转换直接炸。解决生成阶段判断col.IsNullable 属性是值类型时给类型加?。注意 string 和 byte[] 是引用类型本身能接受 NULL不用加。可空类型和 Dapper 的配合是成熟的decimal?、int?都能正确映射不需要额外配置。这条是所有“生成式实体类”最容易翻车的地方因为数据库里允许 NULL 的列远比想象的多连主键以外的所有业务字段都不要假设它有值。5.4 字段名撞上 C# 保留字编译不过现象Oracle 表里有个字段叫COMMENT生成的实体类属性名Comment编译报 CS1041 之类的问题提示标识符不合法。原因Oracle 的保留字列表和 C# 不一样。COMMENT、LEVEL在 Oracle 里常见但string、class、event才是 C# 的关键字。生成器如果只做驼峰转换不做 C# 保留字检测就会生成非法代码。解决命名函数里加一个保留字集合转换后的属性名如果在集合里就改成string这种转义写法同时加[Column(COMMENT)]特性保证读写列时用的还是数据库真实列名。注意这也要求仓储层写 SQL 时读[Column]特性否则SELECT *没问题INSERT时列名就对不上了。5.5 类型映射表漏了 TIMESTAMP WITH TIME ZONE现象表里有SYNC_TIME TIMESTAMP WITH TIME ZONE生成的实体类属性是 string查询映射时 Dapper 报类型转换错误底层返回的是OracleTimeStampTZ结构不能隐式转 string。原因映射 switch 没覆盖这个类型走了 default 分支返回 string。Oracle 驱动返回的类型和 VARCHAR2 完全不同强转自然失败。解决映射表显式加上TIMESTAMP WITH TIME ZONE - DateTimeOffset。虽然 C# 里日常很少用DateTimeOffset但它是唯一能无损保留时区信息的类型。如果确实不关心时区也可以映射成DateTime但要自己做好 UTC 偏移取舍别让驱动帮你转因为驱动转出来的结果经常和业务预期差一个小时。这条提醒我每次新增 Oracle 类型之前先翻驱动文档的类型映射矩阵而不是等运行时报错再来补。6. 进阶让生成器再往前走一步——批量 Insert 与数组绑定6.1 批量 Insert一次提交 1000 行单行 Insert 用仓储没问题但导入工单明细、批量同步库存这类场景一次要写几千行一条条 Execute慢到让人怀疑人生。Oracle 的数组绑定特性可以解决这个问题我把这个能力封装成仓储的扩展方法public int BulkInsertT(ListT entities) where T : class { var props typeof(T).GetProperties() .Where(p p.Name ! _keyName) .ToList(); var colNames string.Join(,, props.Select(p p.Name)); var paramNames string.Join(,, props.Select(p : p.Name)); var insertSql $INSERT INTO {_tableName} ({colNames}) VALUES ({paramNames}); using (var cmd new OracleCommand(insertSql, _conn)) { cmd.ArrayBindCount entities.Count; foreach (var p in props) { var arr entities .Select(e p.GetValue(e)) .ToArray(); cmd.Parameters.Add(new OracleParameter(p.Name, arr)); } return cmd.ExecuteNonQuery(); } }这段代码的关键在cmd.ArrayBindCount entities.Count。设置了这个值Oracle 就知道每个参数数组里有多少条记录一次执行相当于循环执行了 N 条 INSERT网络往返只有一次。注意这里用new OracleParameter(p.Name, arr)时参数数组的每个元素类型要和数据库列类型匹配NUMBER 用 decimal 数组DATE 用 DateTime 数组字符串用 string 数组。类型不匹配时Oracle 驱动会尝试隐式转换某些版本会丢精度。单批次控制在 1000 到 2000 行比较稳再多内存占用偏高性能和收益比会下降。实际使用时调用仍然很直观var list new ListWipOrder(); // 从 Excel 或上游接口填充数据 int rows repo.BulkInsert(list);从那以后我每次接 Oracle 数据导入需求都强制自己走一遍“生成实体类 - 反射取属性 - 数组绑定批量提交”这条流水线。实体类生成器帮我省下的时间早就超过了写它的那几天至少不用再为手写实体类里的低级错误加班到深夜了。希望这篇笔记能帮你在自己的项目里少踩几个坑早一点把时间花在真正难的事情上。本文还有配套的精品资源点击获取
返回列表