一套C#逻辑跑通SQL Server/Oracle/SQLite:dataDemo拆解
简介dataDemo.rar是一份面向C#开发者的多数据库操作示例工程覆盖Oracle、MySQL、SQL Server与SQLite四种常见数据库适合需要快速掌握ADO.NET或对比不同数据库访问方式的初中级开发人员也可作为数据访问层搭建的参考资料。压缩包共38个文件、约623KB以12个cs源代码文件为主另含sln、csproj工程与config配置、3个exe与3个dll可运行程序、资源与设置文件以及一个db示例数据库结构紧凑便于本地编译和对照调试。目前已有755人学习浏览。示例通过各数据库帮助类封装连接、查询、参数化命令和事务处理逻辑可帮助理解不同提供者下Connection、Command、DataReader、DataAdapter的相似用法与差异同时配合窗体界面、设置文件和示例数据库能直观查看多数据库切换、结果集展示与异常处理的完整流程对实际项目中数据库选型和访问层设计有直接参考价值。1. dataDemo.rar 到底装了什么一份让 SQL Server、Oracle、SQLite 共享同一套 C# 逻辑的参考 Demo接手一个老系统维护任务时我手头同时压着三套环境生产库是 SQL Server新项目要接 Oracle本地联调又只能用 SQLite三边的数据访问代码长得一点都不像。直到同事甩给我一个 dataDemo.rar这包东西才把我从反复翻车的状态里救出来。它不是框架也不是教程而是一组可直接运行的 C# 数据库操作示例同一个查询逻辑分别用 SqlClient、Oracle.ManagedDataAccess、Microsoft.Data.Sqlite 写一遍连接串、事务、参数化、分页全部摆在明面。适合这么用的读者很明确——要写多库兼容数据层的人、刚接手老项目的人、以及想快速验证某条 SQL 在三种数据库里行为是否一致的开发者。这份资源是可以直接抄作业的那种下文我把每一层拆开讲。2. 三种数据库的差异从哪来驱动、类型映射与分页规则很多人拿到 dataDemo.rar 第一反应是找代码但代码只是结果。真正让三套数据库代码长得不一样的是底层的几个固定差异点。搞清差异来源后面调试才不至于靠玄学。2.1 ADO.NET 的统一抽象与各驱动类的命名差异C# 里所有数据库操作都跑在 ADO.NET 的同一套抽象上Connection、Command、DataReader、DataAdapter、Parameter、Transaction。你会发现只要是 .NET 的数据访问代码结构永远是「打开连接 → 创建命令 → 执行 → 读取 → 关闭」。这套抽象十年前长这样现在还是这样dataDemo.rar 里的 demo 之所以能三库对照着写靠的就是这个公共底座。不同的地方在具体类名。SQL Server 用SqlConnectionOracle 用OracleConnectionSQLite 用SqliteConnection它们都实现了IDbConnection接口。所以你可以写一段不关心具体数据库的代码先让连接对象跑起来// 用 ADO.NET 公共接口拿到连接对象不区分具体驱动 public static string GetDatabaseFlavor(IDbConnection conn) { conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT 1; var result cmd.ExecuteScalar(); // 返回 1 就说明连接可用 conn.Close(); // 根据实际类型名返回数据库标识便于日志打印 return conn.GetType().Name switch { SqlConnection sqlserver, OracleConnection oracle, SqliteConnection sqlite, _ unknown }; }这段代码的逻辑是利用CreateCommand和ExecuteScalar这类接口方法把「连接是否通」这件事抽成公共逻辑。ExecuteScalar返回结果集第一行第一列SELECT 1在任何数据库里都成立。参数说明连接字符串由外部传入conn.GetType().Name用于识别驱动类型这个方法在做多库冒烟测试时非常实用。2.2 驱动选型的两个分岔口官方与社区、托管与原生dataDemo.rar 里最有价值的部分不是代码本身而是它把每个数据库该用哪个驱动包标清楚了。新手最容易在这里翻车——用错驱动代码写对了也连不上。SQL Server 这边有两个选择老项目里大量存在System.Data.SqlClient这是 .NET Framework 时代的官方驱动维护已进入停滞状态。新代码建议用Microsoft.Data.SqlClient它持续更新加密、连接池、故障转移这些能力都更完整。如果你的 demo 只是做本地验证System.Data.SqlClient还能跑但别往新项目里带。Oracle 这边比较明确不要用System.Data.OracleClient这个组件早已被标记为过时。现在的主流选择是Oracle.ManagedDataAccess.Client它是托管驱动不需要在目标机器上装 Oracle Client 客户端部署成本低很多。dataDemo 里凡是 Oracle 的连接代码指向的都是这组命名空间。SQLite 有两个流派System.Data.SQLite功能全但偏重内部包含原生库Microsoft.Data.Sqlite是微软官方为 EF Core 配套的轻量驱动API 风格更现代。对 demo 和工具类项目来说Microsoft.Data.Sqlite完全够用而且包体积小。目标数据库推荐驱动NuGet 包名已废弃/不推荐SQL ServerMicrosoft.Data.SqlClientSystem.Data.OracleClient错误用法OracleOracle.ManagedDataAccess.ClientSystem.Data.OracleClientSQLiteMicrosoft.Data.SqliteSystem.Data.SQLite可选但偏重选驱动时还要注意目标框架。.NET Framework 4.x 项目里System.Data.SqlClient是内置程序集换到 .NET 6/8 就得显式引包。dataDemo.rar 里如果看到using System.Data.SqlClient先确认工程的目标框架再决定要不要换成Microsoft.Data.SqlClient。2.3 三个最容易让 SQL 翻车的差异点类型映射、分页、自增键驱动定了接下来是 SQL 本身。同一句「查第二页的十条数据」三种数据库写法完全不同。dataDemo 里把这三个差异点分别做了对照这也是我认为最值得抄的部分。类型映射是第一个坑。比如布尔值SQL Server 用bitOracle 一般用NUMBER(1)存 0/1SQLite 没有独立的布尔类型直接用INTEGER 0/1代替。字符串方面SQL Server 是nvarcharOracle 是NVARCHAR2SQLite 是TEXT。日期时间更麻烦Oracle 的TIMESTAMP精度和 SQL Server 的datetime2不一样如果代码里写死DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)传给 Oracle毫秒部分就被截掉了。概念SQL ServerOracleSQLite字符串nvarchar/ncharNVARCHAR2/NCHARTEXT可变长度二进制varbinaryBLOBBLOB布尔bitNUMBER(1)INTEGER 0/1日期时间datetime2TIMESTAMPTEXT(ISO) / INTEGER(Unix)分页是第二个坑也是最容易让 SQL 直接报错的地方。三种数据库的推荐分页写法分别如下-- SQL Server 2012OFFSET FETCH SELECT Id, Name FROM Users ORDER BY Id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- Oracle 12c同样支持 OFFSET FETCH SELECT Id, Name FROM Users ORDER BY Id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- Oracle 11g 及更老版本ROWNUM 嵌套两层 SELECT * FROM ( SELECT t.*, ROWNUM rn FROM (SELECT Id, Name FROM Users ORDER BY Id) t WHERE ROWNUM 30 ) WHERE rn 20; -- SQLiteLIMIT OFFSET SELECT Id, Name FROM Users ORDER BY Id LIMIT 10 OFFSET 20;注意 Oracle 的老分页写法必须先排序再套ROWNUM否则分页结果不稳定SQL Server 的OFFSET FETCH必须搭配ORDER BY否则直接报错。数据量大时OFFSET类的分页会有深分页性能问题但在 demo 阶段够用了。自增主键是第三个坑。SQL Server 用IDENTITY插入后拿主键用SCOPE_IDENTITY()Oracle 12c 可以用IDENTITY列老版本只能用SEQUENCE插入前先取序列号SQLite 用AUTOINCREMENT或直接依赖RowId取回主键用last_insert_rowid()。dataDemo 里针对这三种取主键的方式分别做了示例这段代码建议直接保留后续写仓储层时能省很多事。3. 最小 Demo 怎么跑工程结构、裸连脚本与 Dapper 封装看完差异点接下来动手把 demo 跑起来。我拆这类数据访问包见到的结构通常是有层次的不是一坨散文件。先看清工程结构再逐个文件跑通最后用一个轻量封装把重复代码收走。3.1 先看压缩包里的工程结构dataDemo.rar 解压后的典型结构大致是这样目录命名按「从原始到封装」的递进关系排列dataDemo/ ├─ 01_PlainAdo/ │ ├─ SqlServerDemo.cs │ ├─ OracleDemo.cs │ └─ SqliteDemo.cs ├─ 02_DapperDemo/ │ ├─ DataService.cs │ └─ Program.cs └─ 03_Common/ ├─ DbConnectionFactory.cs ├─ appsettings.json └─ README.md01_PlainAdo是纯 ADO.NET 写法三个文件分别对应三种数据库代码结构刻意保持平行方便对比差异。02_DapperDemo是用 Dapper 微 ORM 重写同一套逻辑文件数量少了一半。03_Common里是连接工厂和配置示例这是从 demo 走向生产的关键一步。如果你拿到的包结构和上面不完全一样不影响使用核心是抓住「同一个业务逻辑、不同驱动实现」这条主线。建议按 01 → 02 → 03 的顺序运行能感受到代码量从繁琐到简洁的演变过程。3.2 裸连三连同一逻辑用三种驱动各写一遍01_PlainAdo里的三个文件除了类名和连接串前缀其余结构几乎一样。拿查询用户列表来说SqlClient 版本是这样// 01_PlainAdo/SqlServerDemo.cs using System.Data.SqlClient; const string connStr Server127.0.0.1;DatabaseDemo;User Idsa;Password123456;; using var conn new SqlConnection(connStr); conn.Open(); // 参数化查询deptId 是 SQL Server 的参数占位符 using var cmd conn.CreateCommand(); cmd.CommandText SELECT UserName FROM dbo.Users WHERE DeptId deptId; cmd.Parameters.Add(new SqlParameter(deptId, 5)); using var reader cmd.ExecuteReader(); while (reader.Read()) { Console.WriteLine(reader[UserName]); }逻辑说明using var确保连接和命令对象在作用域结束时释放cmd.Parameters.Add把外部传入的部门 ID 作为参数传给 SQL避免字符串拼接带来的注入风险ExecuteReader返回只进流的读取器reader[UserName]按列名取值。Oracle 版本和 SQLite 版本的差异点只有三处连接对象类名换成OracleConnection或SqliteConnection连接串格式不同参数占位符从deptId换成:deptId或保持deptId。很多新手在这里踩坑——不是 SQL 写错而是参数占位符和驱动不匹配导致 ORA-00933 这类让人摸不着头脑的报错。3.3 用 Dapper 把重复代码收走纯 ADO.NET 写三遍之后一个很自然的诉求是「能不能只写一遍业务逻辑让底层自动切换数据库」。dataDemo 里 02 部分给出的答案是 Dapper。Dapper 是一个轻量级微 ORM不引入复杂的实体追踪只做一件事把IDbConnection上的常见操作封装成扩展方法。// 02_DapperDemo/DataService.cs using Dapper; using Microsoft.Data.Sqlite; public IReadOnlyListUser GetUsers(int deptId) { using var conn new SqliteConnection(Data Sourcelocal.db); // Dapper 会把匿名对象的属性自动映射为参数 var users conn.QueryUser( SELECT Id, UserName, DeptId FROM Users WHERE DeptId DeptId, new { DeptId deptId } ).AsList(); return users; }参数说明conn.QueryUser返回IEnumerableUser内部自动完成 DataReader 到实体对象的映射匿名对象new { DeptId deptId }的属性名必须与 SQL 中的参数名大小写不敏感地匹配。相比裸连版本这里省掉了手动创建SqlParameter、设置CommandText、遍历DataReader的一堆样板代码。Dapper 的查询逻辑是跨数据库通用的因为IDbConnection本身就是抽象的。只要传入不同的连接对象同一段查询代码可以在三种数据库上运行。这就是为什么 dataDemo 里把 Dapper 作为「收口」的一层——裸连代码用来理解差异Dapper 代码用来提高效率。实际项目中如果不想引入完整 ORMDapper 是性价比最高的选择。4. 连接串与事务边界参数怎么设、边界在哪代码逻辑再对连接串写错照样白搭。连接串是数据访问层最容易被忽略又最影响排错效率的部分dataDemo 里三种库的连接串各写了一份我把它们放在一起对照顺便把参数化、事务、连接池这些边角一起讲清楚。4.1 四组连接串写法与易错点连接串不是配置项它直接决定了连接的建立方式、超时时间、加密行为和连接池大小。三库的典型写法如下目标数据库典型连接串常见坑SQL ServerServer127.0.0.1,1433;DatabaseDemo;User Idsa;Passwordxxx;TrustServerCertificateTrue;实例名和端口混淆证书校验导致连接失败OracleData Source127.0.0.1:1521/ORCLPDB;User Idapp_user;Passwordxxx;SID 和 Service Name 混用TNS 配置干扰SQLiteData SourceC:\data\demo.db;相对路径解析不一致WAL 文件伴随产生SQLite 内存库Data Sourcefile:demo.db?modememorycacheshared连接关闭即库消失需保持连接打开SQL Server 连接串里最常见的坑是实例名与端口混写。默认实例用Server机器名即可命名实例通常要带实例名而端口写法Server127.0.0.1,1433适用于自定义端口。TrustServerCertificateTrue是 .NET 7 之后的高频坑——目标库的 SSL 证书是自签名的不加这个参数直接报证书链错误。Oracle 连接串的Data Source有两种形态host:port/SERVICE_NAME和host:port:SID。SID 是实例的唯一标识Service Name 是集群对外提供的服务名两者经常不一致。dataDemo 里用的是/ORCLPDB这种 Service Name 写法如果连接报「无法解析连接描述符」先确认目标库是单机 SID 还是 PDB 服务名。SQLite 的坑在路径上。Data Source../data/demo.db是相对路径工作目录不同连接到的文件就不同。建议在 demo 里始终用Path.GetFullPath转成绝对路径再拼连接串否则本地能跑、部署到服务上就「数据库不存在」。4.2 参数化的正确姿势与事务的提交点参数化查询是老生常谈但参数化的细节决定性能。dataDemo 里没有用AddWithValue图省事而是显式指定类型和长度这个细节值得展开。// 事务一定要包在 using 里并控制好提交点 using var tx conn.BeginTransaction(); try { // 扣减库存Condition 里带上 Count n 做乐观锁 using var cmd conn.CreateCommand(); cmd.Transaction tx; // 命令必须绑定到事务否则报“事务已中止” cmd.CommandText UPDATE Inventory SET Count Count - n WHERE GoodsId id AND Count n; cmd.Parameters.Add(new SqlParameter(n, SqlDbType.Int) { Value 10 }); cmd.Parameters.Add(new SqlParameter(id, SqlDbType.Int) { Value 3 }); if (cmd.ExecuteNonQuery() ! 1) throw new InvalidOperationException(库存不足事务回滚); tx.Commit(); // 全部成功才提交 } catch { tx.Rollback(); // 任何异常都回滚不留半截数据 throw; }逻辑说明BeginTransaction之后所有命令都要把cmd.Transaction指向同一个事务对象否则执行时报「操作对事务状态无效」。ExecuteNonQuery返回受影响行数用它判断库存扣减是否成功比先查后改更安全。参数说明SqlParameter(n, SqlDbType.Int)显式声明了参数类型避免AddWithValue在字符串长度、数值精度上的隐式猜测。Count n这个条件让 UPDATE 具备了原子性——并发场景下只有一个请求能成功扣减另一个受影响行数为 0直接回滚。这里的边界值得记牢事务生命周期要短不要在事务里做远程调用或耗时 IO一个事务只做一件事不要把「扣库存 写流水 发通知」全塞进去。dataDemo 里每个事务示例都控制在两条 SQL 以内这是刻意为之。4.3 连接池、超时与批量写入连接池是隐形的性能开关默认开启但参数值得调。SQL Server 和 Oracle 的连接串里可以追加PoolingTrue;Min Pool Size5;Max Pool Size100;SQLite 的连接池行为不同默认情况下每次Open都是新连接频繁开关会有开销。超时参数是排错时的第一道线索。Connect Timeout15控制建立连接的超时Command Timeout30在连接串里用Default Command Timeout控制 SQL 执行超时。dataDemo 里的连接串普遍只写了Connect Timeout15命令超时在代码里设置避免把超时限制写死在配置里。批量写入三库各有各的姿势。SQL Server 用SqlBulkCopy直接把 DataTable 灌进去每秒几万行很常见Oracle 可以用 ODP.NET 的数组绑定功能SQLite 没有专门的批量接口最有效的做法是开启一个事务批量循环执行 INSERT提交一次。dataDemo 里对三类批量写入都有对应的 demo 文件需要做数据迁移时直接参考。5. 避坑记录五条从数据访问 demo 里踩出来的实际教训下面是这部分资源里最值得反复看的章节。每一条都是我以及我周围同事在类似项目里真实遇到过的。讲现象、说原因、给解决按这个顺序写方便你对照排查。5.1 连接与驱动相关的坑坑一Oracle 连接串把 SID 和 Service Name 混用导致连接超时。现象本地用 SQL Developer 连得上程序里就报「ORA-12514: TNS: listener does not currently know of service requested in connect descriptor」。原因SQL Developer 里填的是 Service Name程序连接串里写的是 SID或者反过来。两者指向的可能是同一个实例但监听器解析方式完全不同。也有一部分情况是目标库经历了从单机到 RAC 的迁移SID 已不再对外提供服务。解决先确认目标库当前用的是哪种连接标识。可以在数据库机器上执行lsnrctl services查看监听器注册的服务名列表也可以用grep查看tnsnames.ora里的SERVICE_NAME配置。确认后在连接串里统一改成Data Sourcehost:port/服务名的格式。从那以后我每接一个 Oracle 项目第一件事就是问 DBA 要一份当前生效的连接串样例不再自己猜。坑二参数占位符混用Oracle 报 ORA-00933SQL Server 报“必须声明标量变量”。现象同一段带参数的 SQL在 SQL Server 上跑得好好的换到 Oracle 直接报 ORA-00933 或 ORA-01036反过来在 SQL Server 上遇到「必须声明标量变量 p」。原因SQL Server 的参数占位符是参数名Oracle 的参数占位符是:参数名。Dapper 会自动根据驱动类型选择正确的占位符但裸连代码里写死的是换数据库就翻车。解决裸连代码里写哪个数据库就用哪个占位符不要试图“统一”。如果坚持要写跨库兼容的参数化 SQL可以借助 Dapper 或 SqlKata 这类查询构造器让框架帮你翻译占位符。dataDemo 里 01 和 02 两份代码的差异恰好演示了这个问题——裸连版三个文件的占位符各不相同Dapper 版却完全一致。5.2 查询与写入相关的坑坑三同一句分页 SQL 在三个数据库上只有 SQLite 能跑通。现象把SELECT * FROM Users LIMIT 10 OFFSET 20直接挪到 SQL Server 和 Oracle 上前者报「关键字 LIMIT 附近有语法错误」后者报「ORA-00933」。原因三种数据库的分页语法天生不同。SQLite 的LIMIT/OFFSET最直观SQL Server 必须用ORDER BY ... OFFSET ... FETCHOracle 12c 之前还要套两层子查询配合ROWNUM。把一种数据库的方言硬套到另一种上报错只是第一步更危险的是某些方言能跑通但结果错乱。解决把分页封装成一个公共方法按数据库类型分发 SQL 模板。最简单的方式是写一个BuildPagingSql(string sql, int pageIndex, int pageSize)内部用switch (dbType)返回不同的分页语句。dataDemo 里 03_Common 目录下如果有分页辅助类就是这个套路。不要试图让一句 SQL 三库通用这不现实。坑四SQLite 多线程写入时报 database is locked程序随机崩。现象开了三个线程同时写同一个 SQLite 数据库文件偶尔抛出SQLite Error 5: database is locked复用连接池后报得更频繁。原因SQLite 的锁粒度是数据库级别的写事务期间其他写连接会被阻塞。默认的 journal mode 是 rollback journal锁等待时间很短一旦超时就报错。连接池的复用又让多个线程可能共享一个连接对象进一步加剧竞争。解决连接串里或打开连接后执行两条 PRAGMA。PRAGMA journal_modeWAL;把日志模式切换为 Write-Ahead Logging写读可以并发PRAGMA busy_timeout5000;把锁等待时间设为 5 秒超过才报错。同时业务上强制同一个 SQLite 数据库文件的写操作走单一连接串行化写入。这条在 dataDemo 的 SQLite 示例文件里有注释说明我第一次跑 demo 时忽略了后来在真实项目里被线上日志教了一课。坑五AddWithValue 导致索引失效慢查询从 20ms 涨到 800ms。现象用cmd.Parameters.AddWithValue(name, userName)查询一个大表的nvarchar(50)索引列执行计划显示走了全表扫描。原因AddWithValue会把字符串推断为nvarchar(MAX)在部分 SQL Server 版本和驱动组合下类型不匹配导致索引列上发生了隐式转换查询优化器放弃索引。解决显式声明参数类型和长度cmd.Parameters.Add(new SqlParameter(name, SqlDbType.NVarChar, 50) { Value userName });参数说明SqlDbType.NVarChar指定类型50指定长度两处都要和表结构一致。这个写法和 dataDemo 里 01_PlainAdo 的代码风格一致。如果你接手的老代码里全是AddWithValue排查慢查询时优先怀疑这些参数。6. 进阶技巧把临时 Demo 重构成三库通用的数据访问层demo 能跑和能进生产是两回事。最后分享把 dataDemo 里的代码升级成通用数据访问层的两个关键动作统一连接工厂、用内存数据库做冒烟测试。连接工厂的作用是让业务代码不再关心「今天连的是哪个数据库」。所有连接对象的创建集中在一个方法里配置驱动由配置文件决定而不是编译期写死// 03_Common/DbConnectionFactory.cs using Microsoft.Data.SqlClient; using Microsoft.Data.Sqlite; using Oracle.ManagedDataAccess.Client; public static IDbConnection Create(string providerName, string connStr) { return providerName switch { sqlserver new SqlConnection(connStr), oracle new OracleConnection(connStr), sqlite new SqliteConnection(connStr), _ throw new NotSupportedException($不支持的驱动: {providerName}) }; }使用时的效果是业务层只依赖IDbConnection新增数据库时改一行工厂代码和配置文件即可。dataDemo 里的三层结构如果你只抄一个文件我建议抄这个工厂类它是整个数据访问层的门面。第二个动作是用 SQLite 内存库做冒烟测试。SQLite 支持Data Source:memory:连接存续期间数据库就在内存里断开即销毁。这让单元测试不需要外部数据库服务跑得又快又干净// 冒烟测试同一个 CRUD 用例切换 provider 验证 using var conn DbConnectionFactory.Create(sqlite, Data Source:memory:); conn.Open(); // 建表 using var cmd conn.CreateCommand(); cmd.CommandText CREATE TABLE Users (Id INTEGER PRIMARY KEY, UserName TEXT);; cmd.ExecuteNonQuery(); // 写入和查询的代码与 SQL Server 环境共用同一套注意事项:memory:数据库在连接关闭后消失如果使用连接池且连接被回收数据可能不达预期。测试场景里务必保持连接打开到断言结束。另一个容易忽略的点是SQLite 内存库对 SQL 方言的宽容度比 SQL Server 和 Oracle 高很多所以冒烟测试只能验证逻辑不能验证方言兼容性。我每次往老系统里塞新的数据访问代码都强制走一遍这个流程先跑通 dataDemo 里对应数据库的裸连示例确认连接串和参数占位符无误再把业务 SQL 放到真实目标库里执行一遍确认分页、类型映射、自增键没踩坑最后才集成到业务层。这套习惯帮我省掉了大量「代码本地能跑、上了测试环境就废」的尴尬。希望这份拆解也能让你少走一段弯路。本文还有配套的精品资源点击获取