尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#连接Oracle增删改查最佳实践:使用Oracle.ManagedDataAccess

C#连接Oracle增删改查最佳实践:使用Oracle.ManagedDataAccess 简介C#与Oracle数据库交互是常见企业开发场景一套可直接运行的增删改查源码能显著降低入门门槛。资源基于Visual Studio 2010与ODP.NET编写面向需要快速掌握C#操作Oracle的初中级开发者完整覆盖连接Oracle、创建表、添加、查询、更新、删除数据并集成Windows窗体界面展示结果。压缩包共28个文件体积仅34KB核心为9个.cs源文件包括登录窗体、主窗体、数据访问层等模块同时配有8张效果截图、3个资源文件及app.config、项目工程配置等目录结构清晰可直接在VS2010中打开调试。资料已有334人浏览学习实用度获得一定验证。相比零散代码这套完整解决方案可帮助理解OracleConnection、OracleCommand、OracleDataReader等核心API的实际用法并作为扩展存储过程、事务处理等功能的基础模板适合边看边练或作为企业项目起步参考。 先说结论C#连Oracle做增删改查最稳的方案就是直接用Oracle官方提供的Oracle.ManagedDataAccess.NET Framework或Oracle.ManagedDataAccess.Core.NET Core/.NET 5别再碰System.Data.OracleClient了——那东西从.NET Framework 4.0开始就被官方标记为过期驱动老、问题多网上搜出来的很多旧教程还在教它照着写基本是在给自己挖坑。我之所以想说透这件事是因为这个需求在平时太常见了C#上位机要连数据库存检测数据Web API要给业务系统做数据接口桌面工具要从Oracle导数据做报表——都是增删改查但一搜资料十个有八个讲得不清不楚要么驱动版本对不上要么连接字符串格式有问题要么跑起来报字符集错误。这篇我就把自己实际用过的方案、代码和踩过的坑整理出来适合正要上手C#连Oracle的开发者也适合做完一遍但总觉得“不踏实”的人对照检查。1. 选型是第一道分水岭认清驱动与客户端的真实关系很多初学者被Oracle的安装包体积吓到过以为C#连Oracle必须装一整套Oracle数据库客户端其实不是。这个认知一旦错了后面处处被动。1.1 托管驱动与“免安装”原理Oracle.ManagedDataAccess以及它的.NET Core版本是纯托管代码实现的数据访问驱动它不依赖本机安装的Oracle Client也不需要配置TNS_ADMIN环境变量。它内部直接实现了Oracle网络协议Oracle Net相当于把原来Oracle Client那一层通信逻辑用C#重写了。所以你的程序里只需要引用一个DLL就能直连数据库服务器。这一点在部署的时候优势非常明显目标机器不用装几GB的Oracle客户端也不用拷一堆DLL到系统目录。尤其是C#上位机项目客户现场往往是一台干净的工控机你不可能要求对方为了跑个测数据保存功能先去装Oracle客户端。1.2 各驱动方案横向对比驱动方案是否需要安装客户端适用范围现状System.Data.OracleClient需要.NET Framework 2.0~4.0已过时不推荐Oracle.DataAccessODP.NET非托管需要.NET Framework全版本需要区分32/64位部署繁琐Oracle.ManagedDataAccess不需要.NET Framework 4.5.2官方推荐当前主流Oracle.ManagedDataAccess.Core不需要.NET Core 3.1 / .NET 5官方推荐跨平台这里有个特别容易踩的坑如果项目是AnyCPU编译的用Oracle.DataAccess非托管版会随机出现“未能加载文件或程序集”或“Oracle.DataAccess.Client.OracleConnection”找不到的错误。因为非托管组件分x86和x64两套AnyCPU在64位系统上跑64位进程在32位系统上跑32位进程两边的原生DLL必须匹配。而ManagedDataAccess没有这个烦恼它全是托管代码AnyCPU随便跑。我建议新项目一律用ManagedDataAccess系列。1.3 NuGet安装版本别贪新用Visual Studio的NuGet包管理器搜索“Oracle.ManagedDataAccess.Core”安装最新稳定版即可。但注意一个细节如果你的目标是.NET Framework 4.6.1或更老请安装Oracle.ManagedDataAccess不带.Core这个包支持.NET Framework同时也有32位/64位兼容的托管实现。装完以后项目引用里会出现Oracle.ManagedDataAccessfor .NET FrameworkOracle.ManagedDataAccess.Client命名空间核心命名空间都是Oracle.ManagedDataAccess.Client。不要和System.Data.OracleClient弄混。2. 连接字符串一条正确配置能规避80%的坑Oracle的连接字符串常见写法有两种一种是用rdp描述符另一种是用EZ Connect简易连接格式。我强烈建议直接采用EZ Connect方式字符串短、可读性高还不需要配置tnsnames.ora。2.1 EZ Connect格式Data Source192.168.1.100:1521/ORCL;User Idscott;Passwordtiger;解释一下各段含义192.168.1.100数据库服务器IP或主机名1521Oracle监听端口默认是1521ORCLOracle服务名Service Name不是SID。绝大多数现代化Oracle环境用的是服务名少数老库才用SID。连接SID时格式是Data Source192.168.1.100:1521/ORCL其实服务名和SID在EZ Connect里都这么写关键看服务器上配的是什么。如果你在服务器上执行lsnrctl services能看到类似ORCL has 1 service handler(s)的输出那ORCL就是服务名直接用。2.2 常见连接串参数及其影响参数示例作用备注Poolingtrue/false连接池开关默认true一般不用动Min Pool Size1最小连接数避免频繁建连Max Pool Size100最大连接数并发高时调整Connection Timeout15连接超时秒数默认15Persist Security Infofalse是否保留密码建议false防泄漏一个更完整的连接串Data Source192.168.1.100:1521/ORCL;User Idscott;Passwordtiger;Connection Timeout30;Max Pool Size50;2.3 一个容易忽略的坑防火墙与监听程序报“ORA-12541: TNS:no listener”时先别急着怀疑连接串。先用命令行工具tnsping测试一下tnsping 192.168.1.100:1521/ORCL如果没有安装Oracle客户端可以用PowerShell测试端口是否通Test-NetConnection -ComputerName 192.168.1.100 -Port 1521很多时候ORA-12541是因为数据库服务器防火墙没放行1521端口或者Oracle监听服务OracleOraDB12Home1TNSListener停了而不是代码问题。这个检查顺序能帮你省下大量排查时间。3. 增删改查基础代码、参数化与事务的正确姿势驱动和连接就绪后剩下的就是写标准的ADO.NET代码。Oracle.ManagedDataAccess的API和SqlClient非常相似会写SqlConnection的人基本零成本迁移。3.1 查询选DataReader还是DataAdapter先说结论只读取一次并逐条处理用DataReader需要填充DataTable或批量展示用DataAdapter。DataReader是单向只读流必须保持连接打开才能读取DataAdapter查完会自动填充到DataTable之后可以断开连接。对于简单的列表展示用DataAdapter更省心。一个典型查询示例using (var conn new OracleConnection(connString)) { conn.Open(); string sql SELECT EMPNO, ENAME, SAL FROM EMP WHERE DEPTNO :deptNo; using (var cmd new OracleCommand(sql, conn)) { cmd.Parameters.Add(:deptNo, OracleDbType.Int32).Value 10; using (var reader cmd.ExecuteReader()) { while (reader.Read()) { int empNo reader.GetInt32(0); string name reader.GetString(1); decimal sal reader.GetDecimal(2); Console.WriteLine(${empNo} - {name} - {sal}); } } } }注意这里参数名用了:deptNoOracle和SQL Server不同SQL Server用Oracle用:。这是一个新手极易写错的地方写错会直接报“ORA-01036: illegal variable name/number”。3.2 插入参数化是底线别拼接SQL插入数据时务必使用参数化这不只是防SQL注入还关系到中文和特殊字符能不能正确写入。using (var conn new OracleConnection(connString)) { conn.Open(); string sql INSERT INTO EMP(EMPNO, ENAME, SAL) VALUES(:empNo, :ename, :sal); using (var cmd new OracleCommand(sql, conn)) { cmd.Parameters.Add(:empNo, OracleDbType.Int32).Value 7788; cmd.Parameters.Add(:ename, OracleDbType.Varchar2).Value 张三; cmd.Parameters.Add(:sal, OracleDbType.Decimal).Value 5000m; int rows cmd.ExecuteNonQuery(); Console.WriteLine($影响行数: {rows}); } }OracleDbType.Varchar2对应Oracle里的VARCHAR2类型。如果你传字符串时发现中文变成了问号“?”先怀疑两点一是NLS_LANG环境变量或注册表里的字符集设置不一致二是数据库字符集和客户端字符集不匹配。ManagedDataAccess默认使用UTF-8编码如果你的数据库字符集是ZHS16GBK且连接串没做特殊指定可能会出现“ORA-12705: Cannot access NLS data files”或乱码。解决办法是在连接串中加上Unicodetrue用于UTF-8或者让DBA统一数据库字符集为AL32UTF8。很多时候乱码的根因是表本身的字符集设置而不是C#代码的问题。注意不要用字符串拼接SQL。哪怕只是测试一旦SQL里包含单引号例如人名ONeil拼接就会出语法错还会带来注入风险。参数化之后这些问题都不存在。3.3 更新与删除事务和受影响行数必不可少基础更新和删除代码同样套路using (var conn new OracleConnection(connString)) { conn.Open(); var tx conn.BeginTransaction(); try { string updateSql UPDATE EMP SET SAL :sal WHERE EMPNO :empNo; using (var cmd new OracleCommand(updateSql, conn, tx)) { cmd.Parameters.Add(:sal, OracleDbType.Decimal).Value 6000m; cmd.Parameters.Add(:empNo, OracleDbType.Int32).Value 7788; int affected cmd.ExecuteNonQuery(); if (affected ! 1) { throw new Exception(没有找到要更新的记录); } } string deleteSql DELETE FROM EMP WHERE EMPNO :empNo; using (var cmd new OracleCommand(deleteSql, conn, tx)) { cmd.Parameters.Add(:empNo, OracleDbType.Int32).Value 7902; cmd.ExecuteNonQuery(); } tx.Commit(); } catch { tx.Rollback(); throw; } }这里有几个经验点检查受影响行数。Update/Delete后至少要判断affected 0的情况这能及时发现“条件写错导致全表更新/删除”的严重事故。事务必须配合using或用try-catch确保Rollback。如果中间某个步骤抛异常不Rollback的话连接会一直占用事务资源释放连接时还会隐式回滚可能掩盖问题。事务粒度要合理。一次事务别包几十万条INSERT会锁表很久并撑爆undo表空间。常见的CRUD操作一个业务逻辑一组事务就好。4. 我踩过的坑从报错信息到数据错乱的真实排错过程有些坑不亲历一次看文档根本意识不到。我把自己遇到过的几个典型问题列出来附带排查思路。4.1 “ORA-12154: TNS:could not resolve the connect identifier” 真凶不一定是连接串第一次遇到这个问题时我反复检查连接串怎么看都是对的数据库也能ping通。后来才发现程序集配置里存在一个oracle.manageddataaccess.client配置节里面有一段version number4.122.1.0和settings其中有一个setting nameTNS_ADMIN value.../被错误地指到了一个不存在的路径导致解析连接标识时跑偏。排查建议先打开项目的App.config或web.config搜oracle.manageddataaccess.client看有没有多余的TNS_ADMIN设置。如果有删除或改为正确路径。ManagedDataAccess默认在无TNS_ADMIN配置时直接用EZ Connect格式根本不会去查tnsnames.ora。4.2 插入中文变问号却不是编码问题有一回调试C#上位机界面输入中文插入Oracle后变成“???”我第一反应是连接串加Unicodetrue。但加了之后还是问号。最后查了一下Oracle数据库当前字符集SELECT * FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER NLS_CHARACTERSET;结果是ZHS16GBK。理论上GBK支持中文奇怪的是表字段是VARCHAR2按理也能存中文。后来发现真正的原因出在Oracle的客户端NLS_LANG设置上程序运行机器上的环境变量NLS_LANG被设置成了AMERICAN_AMERICA.US7ASCII。US7ASCII是纯英文编码中文传进去直接被转成问号。清掉这个环境变量后问题消失。所以遇到中文乱码排查顺序是数据库表字段类型是否为VARCHAR2、NVARCHAR2NVARCHAR2更稳但会占双倍字节数据库字符集是否支持中文运行环境中是否设置了NLS_LANG并且值是否是SIMPLIFIED CHINESE_CHINA.ZHS16GBK或AL32UTF8C#代码里是否正确使用参数化传值连接串是否需要Unicodetrue4.3 NULL值的坑DBNull.Value与空字符串是两码事Oracle里NULL和空字符串有严格区别。C#里string.IsNullOrEmpty(str)能同时判断两种但写入Oracle时如果你把一个空字符串直接赋给参数Oracle会把它当成NULL还是空串取决于字段类型和驱动行为。为了避免歧义写入前务必做一次空值转换cmd.Parameters.Add(:remark, OracleDbType.Varchar2).Value string.IsNullOrEmpty(remark) ? DBNull.Value : (object)remark;读取时也一样reader[remark]可能返回DBNull直接调用ToString()会得到空字符串吗不会它会得到空字符串但类型仍是DBNull如果做类型判断会出错。稳妥写法string remark reader[remark] DBNull.Value ? : reader[remark].ToString();4.4 批量插入性能优化OracleBulkCopy与数组绑定往Oracle批量插几万条数据一条条INSERT会慢到让人怀疑人生。C#里有两个常用解法OracleBulkCopy如果数据源是DataTable和数组绑定Array Binding。OracleBulkCopy示例using (var conn new OracleConnection(connString)) { conn.Open(); using (var bulk new OracleBulkCopy(conn)) { bulk.DestinationTableName EMP; bulk.ColumnMappings.Add(EMPNO, EMPNO); bulk.ColumnMappings.Add(ENAME, ENAME); bulk.BulkCopyTimeout 120; bulk.WriteToServer(dataTable); } }数组绑定则是把同一SQL语句的参数改成数组一次性提交多条using (var cmd new OracleCommand(INSERT INTO EMP(EMPNO, ENAME) VALUES(:empNo, :ename), conn)) { cmd.BindByName true; cmd.ArrayBindCount 3; cmd.Parameters.Add(:empNo, OracleDbType.Int32).Value new int[] { 1, 2, 3 }; cmd.Parameters.Add(:ename, OracleDbType.Varchar2).Value new string[] { A, B, C }; cmd.ExecuteNonQuery(); }实测下来数组绑定对几万到十几万条的数据集性能提升非常明显比逐条ExecuteNonQuery快一个数量级以上。但要注意Oracle的绑定数组大小有限制默认上限是65535超过就得分批处理。5. 分页、存储过程与常见业务场景的实战封装增删改查只是基础实际项目里总绕不开分页查询、调用存储过程等需求。5.1 分页查询用ROW_NUMBER() OVER更通用Oracle没有MySQL的LIMIT语法分页最通用的写法是用ROWNUM或ROW_NUMBER() OVER。推荐后者因为排序参数更灵活SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER (ORDER BY EMPNO DESC) rn FROM EMP t ) WHERE rn BETWEEN 1 AND 20;在C#里封装一个带分页参数的查询方法时只需要把页码pageIndex和页大小pageSize转成rn的起始和结束值int start (pageIndex - 1) * pageSize 1; int end pageIndex * pageSize; sql SELECT * FROM (SELECT t.*, ROW_NUMBER() OVER (ORDER BY EMPNO DESC) rn FROM EMP t) WHERE rn BETWEEN :start AND :end; cmd.Parameters.Add(:start, OracleDbType.Int32).Value start; cmd.Parameters.Add(:end, OracleDbType.Int32).Value end;5.2 调用存储过程留意OracleDbType与返回值存储过程在Oracle里一般分为纯数据操作型和带输出结果集型。带结果集的过程C#端要先把OracleCommand.CommandType设为CommandType.StoredProcedure然后注册一个类型为OracleDbType.RefCursor的输出参数来接收结果集using (var cmd new OracleCommand(PKG_EMP.GET_EMP_LIST, conn)) { cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.Add(p_deptno, OracleDbType.Int32).Value 10; cmd.Parameters.Add(p_cursor, OracleDbType.RefCursor).Direction ParameterDirection.Output; using (var reader cmd.ExecuteReader()) { while (reader.Read()) { // 读取数据 } } }RefCursor是Oracle特有的游标类型和SQL Server的DataTable完全不是一回事。这里最容易踩的坑是忘记把CommandType改为StoredProcedure结果把存储过程名当成SQL语句执行报ORA-00900。5.3 封装一个简单的Repository基类实际项目里我不建议到处写裸的OracleConnection和OracleCommand。哪怕团队就一个人也值得封装一个极简的数据访问基类统一管理连接创建、参数处理、异常日志。我习惯这样封装最小可用版public class BaseRepository { protected string connString; public BaseRepository(string connectionString) { connString connectionString; } protected DataTable ExecuteQuery(string sql, params OracleParameter[] parameters) { using (var conn new OracleConnection(connString)) using (var cmd new OracleCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); var adapter new OracleDataAdapter(cmd); var dt new DataTable(); adapter.Fill(dt); return dt; } } protected int ExecuteNonQuery(string sql, params OracleParameter[] parameters) { using (var conn new OracleConnection(connString)) using (var cmd new OracleCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } }然后具体的业务Repository继承它把SQL和参数传入即可。这套封装足够撑起一个中小型项目的CRUD不需要引入重量级ORM。如果你偏爱ORMDapper也支持Oracle只需在连接上稍作调整但EF Core对Oracle的正式支持一直不太顺小项目不值得为它引入额外的配置复杂度。6. 与C#上位机、实际生产环境的结合经验搜这个主题的人里很大一部分是做C#上位机的。上位机软件的数据库操作模式和纯Web后端不太一样有几个点需要特意提一下。6.1 界面卡顿与数据操作的死锁陷阱上位机里如果直接在UI线程执行耗时的数据库查询界面会假死。很多人的第一反应是加异步——但异步不是银弹。Oracle连接池是有限的如果你的UI线程在等待查询结果另一个线程又试图往同一个表插入数据两个操作互相等待对方释放连接就可能出现连接池耗尽反而更卡。我的建议是数据库操作统一放到非UI线程Task.Run或async/await并且操作之间的连接尽量短占。每次查询执行完立刻释放连接using语句不要长时间持有连接做复杂逻辑。连接字符串里设置合理的Max Pool Size比如上位机并发不高设10~20就够。6.2 日志与异常消息中的敏感信息处理生产环境里数据库异常信息可能包含连接串、SQL语句等敏感信息。日志里别直接输出整个OracleException的Message建议只记录错误码和简化后的信息尤其不要记录User Id和Password。我一般这样处理catch (OracleException ex) { // 只记录错误码不记录连接信息 logger.Error($Oracle error code: {ex.Number}, message: {ex.Message.Substring(0, Math.Min(ex.Message.Length, 200))}); }6.3 权限与只读账号给上位机或后端建立专门数据库账号别用system或sys账号。一个只有DML权限的账号足以应付增删改查CREATE USER app_user IDENTIFIED BY password; GRANT CONNECT, RESOURCE TO app_user; GRANT SELECT, INSERT, UPDATE, DELETE ON scott.emp TO app_user;最小权限原则能避免误操作全表更新、删除时把生产数据搞崩。尤其是在上位机对接生产设备数据的场景万一删错一个表恢复成本非常高。6.4 监听不到数据变化时的排查思路如果你把数据插入到Oracle后上位机界面没有实时刷新先检查是不是查询本身用了连接池里的旧连接快照还是真的执行了SQL但结果没变。给查询加一个强制无缓存的特性是不现实的但可以在关键查询中加一个WHERE条件用当前时间戳验证SELECT COUNT(*) FROM EMP WHERE HIREDATE SYSDATE - 1/24;这样可以确认不是Oracle锁或事务隔离级别导致读不到新数据。7. 从一段能跑的代码到一套能维护的代码写CRUD本身不难难的是写得能放生产环境、能长时间稳定跑。结合我的个人经验给你几个“看起来不起眼但能救命”的提醒。第一连接串不要硬编码在代码里。无论是web.config还是appsettings.json把连接串单独配置并加入配置文件的加密或权限控制。一旦数据库密码需要轮换你不想为了改密码重新发布一遍程序。第二SQL语句统一管理。我见过很多项目SQL散落在事件处理函数里动一个查询要全局搜索。建议把SQL常量集中到一个static类里或者用const string语法组织至少保证修改时能一眼看到所有相关语句。第三每个SQL操作都要有对应的日志。不需要记全量参数但至少要记录哪张表、什么类型的操作、影响行数、耗时。这样线上出问题才能快速定位是哪条SQL、在哪个方法里执行。第四Oracle的事务隔离级别。如果不了解就不要显式设置。默认的Read Committed对绝大多数业务场景都够用乱改隔离级别容易导致ORA-08177cant serialize access for this transaction。我遇到过有人为了“安全”把隔离级别改成Serializable结果并发一上来就频繁报错。第五Index与SQL性能。增删改查的性能问题八成出在查询条件上。WHERE条件里的字段如果没有索引数据量一大就会全表扫描。写完查询后在SQL执行计划里看一眼有没有走INDEX RANGE SCAN远比在C#代码里绞尽脑汁优化要有效。最后分享一个小技巧开发时被Oracle的ORA-00001: unique constraint violated这类报错折磨时别只盯着代码先查一下表的主键序列Sequence是否和现有数据冲突。尤其是在手动插入过测试数据后Sequence的当前值可能远小于表中已有的最大ID这个时候重新插入新数据就会撞主键。解决办法是先同步一下SequenceALTER SEQUENCE seq_emp INCREMENT BY 1000; SELECT seq_emp.NEXTVAL FROM DUAL; ALTER SEQUENCE seq_emp INCREMENT BY 1;这类问题不实际踩一遍光靠看文档很难想到。但处理好之后你会发现C#操作Oracle的增删改查并没有传说中的那么难只要选对驱动、写好参数化SQL、管理好连接生命周期它就是一套标准的活计。本文还有配套的精品资源点击获取
返回列表