首页 / 资讯中心 / 文章详情

C#实战:一个Demo搞懂Oracle、SQL Server与SQLite连接差异

C#实战:一个Demo搞懂Oracle、SQL Server与SQLite连接差异 ★ FEATURED ARTICLE
简介针对C#开发者常见的多数据库操作需求dataDemo.rar打包了一套可直接运行的示例工程覆盖Oracle、MySQL、SQL Server与SQLite四种数据库的访问实现适合需要快速上手ADO.NET或封装数据访问类的初中级程序员参考。压缩包共38个文件包含cs源码、sln解决方案、config配置、exe可执行程序及dll依赖库等整体约623KB结构精简便于按需查阅。其中OracleHelpher.cs、SqlServerHelpher.cs、SqliteHelpher.cs分别演示了不同数据库的连接管理、SQL执行与结果集处理Form1.cs和App.config则展示了界面调用与连接串配置方式。目前已有755人学习下载可供希望对比各数据库差异、借鉴封装思路的开发者直接使用。1. 一个压缩包搞懂 C# 连接三种数据库从 Oracle 到 SQLite 的差异都摆在这里如果你拿到一个名字叫 dataDemo.rar 的压缩包关键词写着 c# oracle sqlserver demo sqlite大概率是一个把关系型数据库访问方式集中演示的示例工程。做数据库应用开发的初期很容易遇到这种场景本地用 SQLite 跑得好好一部署到客户环境就换成 SQL Server再往上走还要对接 OracleSQL 方言差异和连接驱动弄得人头大。这个 Demo 就是拿来把三种数据库的增删改查、事务、参数化查询全部摆在一起对比的。这类资源最适合两类人一类是刚接触 C# 数据库开发想看看同一个功能在不同数据库下代码怎么写的新手另一类是手上正要做数据库迁移或者多数据库适配需要一个参照工程来做方言差异核对的老手。只要能跑起来代码里标注的差异点和连接配置方式能省掉不少翻官方文档的时间。下面从结构、核心代码到改造思路一步步拆开看。2. 工程解剖目录结构与连接管理方式2.1 解决方案的整体组织方式dataDemo 解压之后是一个标准的 Visual Studio 解决方案里面按数据访问方式划分了三个独立项目一个负责 SQL Server、一个负责 Oracle、一个负责 SQLite。每个项目内部都遵循同样的代码组织模式这种刻意为之的对称结构对比起来非常直观。目录大致是数据访问层DAL放数据库操作类实体层Models放数据对象测试入口ConsoleApp做验证输出。这种把不同数据库拆在不同项目里的做法比在一个项目里用条件分支切换数据库要清晰得多。想要对比两种数据库的代码差异直接打开两个文件放到左右分屏就能看到实现细节。工程里保留了原始的 .csproj 文件所以不用手动改引用关系直接双击解决方案文件就能加载。2.2 依赖包与连接串配置三种数据库在 .NET 生态里对应的官方驱动各不相同这是新手最容易卡住的地方。这个 Demo 的每个子项目都通过 NuGet 引用相应的数据访问包SQL Server 用的是 System.Data.SqlClientOracle 用的是 Oracle.ManagedDataAccess.ClientSQLite 用的是 System.Data.SQLite。这三种驱动在使用方式上有一个共同点——都实现了 IDbConnection、IDbCommand、IDbDataAdapter 这些通用接口。连接串配置放在了每个项目的 App.config 里这样切换数据库环境时只需要改配置不需要改代码。SQL Server 的连接串包含 Server、Database、User Id、Password 四个核心参数一个典型的写法是下面这样connectionStrings add nameSqlDb connectionStringServerlocalhost;DatabaseDataDemoDb;User Idsa;Passwordyour_password; providerNameSystem.Data.SqlClient / /connectionStringsOracle 的连接串格式稍微不同需要写清楚数据源的服务名或者 SID同时要指定驱动类型为 Oracle.ManagedDataAccess.Client。SQLite 最简单也最容易出问题因为很多人的路径没写对add nameSQLiteDb connectionStringData Source|DataDirectory|demo.db;Version3; providerNameSystem.Data.SQLite /这里有个值得注意的细节SQLite 的 Data Source 参数用的是竖线包起来的 DataDirectory这种写法可以避免控制台程序和 Web 程序的工作目录不同导致找不到数据库文件。如果直接写相对路径 demo.db在 Visual Studio 里按 F5 调试时能跑但发布后放到别的地方运行就要踩路径的坑。2.3 统一入口连接工厂的设计思路这个 Demo 在连接管理上做了一个很实用的抽象一个 ConnectionFactory 静态类接收数据库类型枚举和连接串返回对应的连接对象实例。public static class ConnectionFactory { public static IDbConnection Create(string dbType, string connectionString) { switch (dbType) { case Oracle: return new OracleConnection(connectionString); case SqlServer: return new SqlConnection(connectionString); case SQLite: return new SQLiteConnection(connectionString); default: throw new NotSupportedException($不支持的数据库类型{dbType}); } } }这段代码的作用不在于实现多复杂的技术而在于把三种数据库的差异收敛到一个入口。上层业务代码只依赖 IDbConnection 接口完全不关心底层是哪种数据库。接口的 ConnectionString 属性、Open 方法、CreateCommand 方法在三种驱动里的行为一致所以调用方代码可以做到与数据库无关。如果拿这个 Demo 做二次开发这个工厂类就是扩展新数据库类型的锚点。3. 核心模块拆解同一套业务逻辑在三种数据库下的写法差异3.1 基础查询SqlCommand 与 SqlDataReader 的使用方式每种子项目里都有一个 UserRepository 类用同样的方法名实现同样的查询逻辑这是整个 Demo 里最值得逐行对照的部分。以最简单的按主键查询为例子SQL Server 的实现方式如下public User GetById(int id) { using (var conn new SqlConnection(_connectionString)) using (var cmd new SqlCommand(SELECT Id, Name, Age FROM Users WHERE Id Id, conn)) { cmd.Parameters.Add(new SqlParameter(Id, SqlDbType.Int)).Value id; conn.Open(); using (var reader cmd.ExecuteReader()) { if (reader.Read()) { return new User { Id reader.GetInt32(0), Name reader.GetString(1), Age reader.GetInt32(2) }; } } } return null; }这段代码的核心套路是先构造 SqlCommand 再添加参数最后执行 ExecuteReader 逐行读取。注意 using 声明嵌套的方式连接、命令、读取器全部自动释放避免忘记关闭连接导致连接池耗尽。SqlParameter 的属性赋值分两步第一步指定参数名和类型第二步给 Value 赋实际值这样写的好处是数据库能提前知道参数类型走索引的时候更可靠。Oracle 版本在整体框架上几乎一致唯一的区别是参数占位符从 变成了冒号驱动类从 SqlConnection 换成了 OracleConnectionusing (var cmd new OracleCommand(SELECT Id, Name, Age FROM Users WHERE Id :Id, conn)) { cmd.Parameters.Add(new OracleParameter(:Id, OracleDbType.Int32)).Value id; conn.Open(); using (var reader cmd.ExecuteReader()) { // 读取逻辑与 SQL Server 版本完全相同 } }参数名前面是冒号这个坑代码写的时候不觉得一旦从 SQL Server 迁到 Oracle全局搜索替换参数占位符就是一项体力活。而 SQLite 版本则又回到了 占位符这恰恰说明了为什么需要这样的 Demo 来做对照——参数化查询的写法在不同数据库之间确实没有统一标准。3.2 分页查询三种数据库的方言差异集中区分页是程序员最常写也最容易写错的场景。SQL Server 2012 以后支持 OFFSET FETCH 语法写法非常干净SELECT Id, Name, Age FROM Users ORDER BY Id OFFSET Offset ROWS FETCH NEXT PageSize ROWS ONLY;Oracle 传统上使用 ROWNUM 做分页但写法很绕需要嵌套子查询外层再套一层查询来筛选行号范围。主流的写法是这样的三层嵌套SELECT * FROM ( SELECT A.*, ROWNUM AS rn FROM ( SELECT Id, Name, Age FROM Users ORDER BY Id ) A WHERE ROWNUM EndRow ) WHERE rn StartRow;最内层先做排序第二层限制最大行号最外层再排除掉起始行之前的记录。这套写法在 Oracle 12c 之前几乎是唯一的选择新版本虽然也引入了 OFFSET FETCH但很多生产环境的数据库还没有升级所以兼容性写法仍然有实际价值。SQLite 的分页是最简洁的LIMIT 加 OFFSET 两个关键字直接搞定。三种数据库的分页逻辑差异很大不能共用一套 SQL。这也是多数据库适配的典型难点。Demo 里专门建了一个分页服务类每个数据库有自己的实现版本上层调用方通过接口调用。如果将来要接 PostgreSQL只需要参照这个模式新增一个方言实现不太需要动业务层。3.3 事务处理异常回滚与提交的完整链路事务处理的代码结构在三种数据库下非常相似都是 BeginTransaction 之后把事务对象赋给命令的 Transaction 属性成功就 Commit出现异常就 Rollback。差异主要体现在方法命名和提交行为的边界上。using (var conn new SQLiteConnection(_connectionString)) { conn.Open(); using (var tx conn.BeginTransaction()) { try { using (var cmd new SQLiteCommand( INSERT INTO Users (Name, Age) VALUES (Name, Age); UPDATE Accounts SET Balance Balance - 100 WHERE UserId UserId;, conn, tx)) { cmd.Parameters.AddWithValue(Name, A同学); cmd.Parameters.AddWithValue(Age, 25); cmd.Parameters.AddWithValue(UserId, 1); cmd.ExecuteNonQuery(); } tx.Commit(); } catch { tx.Rollback(); throw; } } }这段事务代码的要点是 SQLiteCommand 的构造函数第三个参数传入了事务对象这条命令就自动归属到事务里执行。如果漏了这一步命令默认独立提交事务就形同虚设。SqlConnection 和 OracleConnection 的事务用法一致差别在于 Oracle 的隐式事务行为更复杂它在没有显式事务时默认自动提交这一点和 SQLite 不同容易让人误判数据真正落盘的时机。3.4 参数化查询的细节差异参数化查询是防 SQL 注入的底线做法三种数据库都支持但参数对象的构造方式各有习惯。SQL Server 的 SqlParameter 要求带 前缀的参数名类型用 SqlDbType 枚举Oracle 的 OracleParameter 带冒号前缀类型用 OracleDbTypeSQLite 的 SQLiteParameter 又回到 前缀类型用 DbType 通用枚举。// SQL Server 写法 cmd.Parameters.Add(new SqlParameter(Name, SqlDbType.NVarChar, 50) { Value 张三 }); // Oracle 写法 cmd.Parameters.Add(new OracleParameter(:Name, OracleDbType.NVarchar2, 50) { Value 张三 }); // SQLite 写法 cmd.Parameters.Add(new SQLiteParameter(Name, DbType.String) { Value 张三 });三种写法可以清晰看到命名体系和类型体系的差异。SQL Server 和 Oracle 都要求指定长度SQLite 不强制。如果只是做本地小工具这些差异无所谓但如果写的是要发布的生产代码参数类型不一致会导致数据库无法有效利用索引性能差距在数据量上来之后会非常明显。3.5 数据适配器与 DataTable 的批量填充方式除了逐行读取的 DataReader 方式Demo 里还演示了用 DataAdapter 填充 DataTable 的批量加载方案。这种方式在 WinForm 或 WPF 绑定数据列表时很常用代码量也比逐行读取少var dataTable new DataTable(); using (var adapter new SqlDataAdapter(SELECT Id, Name, Age FROM Users, conn)) { adapter.Fill(dataTable); } return dataTable;Oracle 对应的是 OracleDataAdapterSQLite 对应 SQLiteDataAdapter调用方式完全一样都是 Fill 方法。DataAdapter 的内部机制是先打开连接执行查询再把结果填充到内存表最后关闭连接。如果只是填充数据表不需要显式调用 Open 方法适配器内部会自行管理连接状态这算一个小技巧。4. 避坑指南跨数据库开发最容易踩的五个坑4.1 参数占位符写错导致 ORA-01036现象代码在 SQL Server 下正常运行把参数化查询切到 Oracle 后立即报 ORA-01036: illegal variable name/number。原因SQL Server 和 SQLite 的参数占位符是 前缀Oracle 是冒号前缀。很多人全局搜索 替换成冒号时漏掉了参数对象构造里的参数名或者干脆忘记替换 SQL 语句里的占位符。解决更换数据库时SQL 文本和参数集合都要同步调整。最稳妥的办法是把参数名抽成常量或者写一层参数构造的辅助方法统一补前缀从根上降低出错概率。这个 Demo 里三个子项目的 Repository 是独立文件用来做替换后的对账清单很好用。4.2 SQLite 报 unable to open database file现象SQLite 项目首次运行时报 unable to open database file但检查数据库文件确实存在于项目目录里。原因控制台应用程序的工作目录默认是 bin\Debug 或 bin\Release而不是项目源码目录相对路径解析的结果和预期不一致所以连接串指向的位置根本找不到文件。解决在连接串里使用 |DataDirectory| 关键字并在程序启动时把 DataDirectory 设置为实际数据目录。示例如下AppDomain.CurrentDomain.SetData(DataDirectory, AppDomain.CurrentDomain.BaseDirectory);如果数据文件是复制到输出目录的记得在 csproj 里设置 CopyToOutputDirectory 为 PreserveNewest否则改了代码重新编译旧库文件会被覆盖或者丢失。4.3 Oracle 的 TO_DATE 与 SQL Server 的 CONVERT 不通用现象日期范围查询在 SQL Server 里正常部署到 Oracle 环境后返回数据为空或者报日期格式无效。原因SQL Server 写日期条件常用 CONVERT(varchar, CreateTime, 120)Oracle 需要 TO_DATE 加上格式掩码。日期字面量的写法在两种数据库里都不是 ISO 标准混用必然出问题。解决跨数据库项目不要在 SQL 里写数据库特有的日期函数参数化查询直接传 DateTime 类型值是最省心的方式。如果必须在 SQL 里处理日期把日期比较逻辑各自封装在方言文件里不要写在公共代码里。4.4 Oracle 报 ORA-01000 游标超限现象系统连续运行一段时间后Oracle 抛出 ORA-01000: maximum open cursors exceeded重启应用才恢复。原因最常见的是 DataReader 没有及时关闭或者连接没有归还给连接池。Oracle 默认游标数量上限很低大约是 300 到 500连接池里的每个物理连接都会累积游标泄露到一定程度必然报错。解决所有读取操作使用 using 块包裹确保 Dispose 被调用。排查时可以查 V$OPEN_CURSOR 视图看看是哪个连接、哪条 SQL 占了大量游标。这个 Demo 里全部用了 using 写法实际上就是在演示避免这个问题。4.5 SQLite 并发写入报 database is locked现象多个线程同时写入 SQLite 数据库时抛出 database is locked 异常而且重试也不一定成功。原因SQLite 基于文件锁实现并发控制同一时刻只允许一个写者。有些环境里每次连接都开启独立事务频繁加锁解锁会加剧冲突概率。解决在连接串里增加 busy_timeout 参数让写操在锁被占用时等待而不是立即失败Data Sourcedemo.db;Version3;busy_timeout5000;同时尽量合并写操作到单个事务里减少锁的获取频率。如果并发量持续走高SQLite 本身就不太适合做高并发写入的库考虑替换成 SQL Server 或者 PostgreSQL 才是治本方案。5. 二次开发改造把 Demo 升级成随手可用的数据访问组件5.1 抽象仓储接口摆脱数据库类型硬编码现在 Demo 里的调用方式是先判断数据库类型再实例化对应的 Repository这种写法在仓储数量少的时候撑得住但一旦业务实体增加到几十个if-else 会膨胀得非常难看。改造第一步是定义一个通用的仓储接口public interface IUserRepository { User GetById(int id); ListUser GetUsersByPage(int pageIndex, int pageSize); int Insert(User user); void Update(User user); void Delete(int id); }三个子项目的 UserRepository 都实现这个接口上层通过工厂方法或者依赖注入容器来决定运行时使用哪个实现。这样业务层完全不感知底层数据库切换数据库只是改一行注册代码。5.2 用配置文件驱动数据库选择把数据库类型也外置到配置节里改造后的调用方式是从配置读取 providerName然后反射加载对应的程序集var dbType ConfigurationManager.AppSettings[DatabaseType]; var factory new ConnectionFactory(); using (var conn factory.Create(dbType, connectionString)) using (var cmd conn.CreateCommand()) { // 业务代码只看 IDbConnection完全不关心具体数据库 }这种写法在中小规模项目里非常实用不需要引入笨重的 ORM又能保留手写 SQL 的控制力。连接串统一放到 connectionStrings 配置节部署到客户环境时只要改配置不用重新编译。5.3 补充连接池参数与超时控制正式使用前建议把超时和池化参数补上。SQL Server 连接串里加 Poolingtrue 和 Connect Timeout15Oracle 连接串里加 Poolingtrue 和 IncrPoolSize2SQLite 加 PoolingTrue 和 Max Pool Size10。不同数据库对连接池的默认行为差异很大显式配置参数能减少上线后遇到连接数打满的概率。改造完再补一组增删改查的单元测试重点覆盖参数化查询和事务回滚路径。这个 Demo 原本只做到能运行二次开发的目标是做到能交付两者之间的差距就是接口抽象和配置外置这两步。从那以后我每次拿到这类示例工程都会先列一遍数据库方言差异清单确认每种数据库的特性函数都收口到了独立文件里再动手改业务代码血泪经验换来的习惯算是这轮拆解最值回票价的收获。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站