☰
C#实战:一个Demo搞懂Oracle、SQL Server与SQLite连接差异
2026/10/18 9:12:56 网站建设 项目流程

简介:针对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,再往上走还要对接 Oracle,SQL 方言差异和连接驱动弄得人头大。这个 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.SqlClient,Oracle 用的是 Oracle.ManagedDataAccess.Client,SQLite 用的是 System.Data.SQLite。这三种驱动在使用方式上有一个共同点——都实现了 IDbConnection、IDbCommand、IDbDataAdapter 这些通用接口。

连接串配置放在了每个项目的 App.config 里,这样切换数据库环境时只需要改配置不需要改代码。SQL Server 的连接串包含 Server、Database、User Id、Password 四个核心参数,一个典型的写法是下面这样:

<connectionStrings> <add name="SqlDb" connectionString="Server=localhost;Database=DataDemoDb;User Id=sa;Password=your_password;" providerName="System.Data.SqlClient" /> </connectionStrings>

Oracle 的连接串格式稍微不同,需要写清楚数据源的服务名或者 SID,同时要指定驱动类型为 Oracle.ManagedDataAccess.Client。SQLite 最简单也最容易出问题,因为很多人的路径没写对:

<add name="SQLiteDb" connectionString="Data Source=|DataDirectory|demo.db;Version=3;" providerName="System.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 换成了 OracleConnection:

using (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 带冒号前缀,类型用 OracleDbType;SQLite 的 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 对应的是 OracleDataAdapter,SQLite 对应 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 Source=demo.db;Version=3;busy_timeout=5000;

同时尽量合并写操作到单个事务里,减少锁的获取频率。如果并发量持续走高,SQLite 本身就不太适合做高并发写入的库,考虑替换成 SQL Server 或者 PostgreSQL 才是治本方案。

5. 二次开发改造:把 Demo 升级成随手可用的数据访问组件

5.1 抽象仓储接口,摆脱数据库类型硬编码

现在 Demo 里的调用方式是先判断数据库类型再实例化对应的 Repository,这种写法在仓储数量少的时候撑得住,但一旦业务实体增加到几十个,if-else 会膨胀得非常难看。改造第一步是定义一个通用的仓储接口:

public interface IUserRepository { User GetById(int id); List<User> 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 连接串里加 Pooling=true 和 Connect Timeout=15,Oracle 连接串里加 Pooling=true 和 IncrPoolSize=2,SQLite 加 Pooling=True 和 Max Pool Size=10。不同数据库对连接池的默认行为差异很大,显式配置参数能减少上线后遇到连接数打满的概率。

改造完再补一组增删改查的单元测试,重点覆盖参数化查询和事务回滚路径。这个 Demo 原本只做到能运行,二次开发的目标是做到能交付,两者之间的差距就是接口抽象和配置外置这两步。从那以后我每次拿到这类示例工程,都会先列一遍数据库方言差异清单,确认每种数据库的特性函数都收口到了独立文件里再动手改业务代码,血泪经验换来的习惯,算是这轮拆解最值回票价的收获。希望帮到你。

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

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

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

立即咨询