简介:这份 dataDemo.rar 面向具备一定 C# 基础、希望系统掌握多数据库访问的开发者,聚焦 Oracle、SQL Server、SQLite 等数据库在 .NET 环境下的连接与操作实践。压缩包共 38 个文件,约 623KB,以 cs 源码为主,辅以 sln 解决方案、csproj 工程文件、config 配置、resx 资源及 exe、dll 编译产物,并附带一个 db 数据库文件,结构完整可直接运行调试。资源围绕连接字符串配置、Command 执行、DataReader 读取、事务管理与结果集处理等核心环节展开,示例代码覆盖多种数据库的差异化写法,便于横向对比与迁移复用。目前已有 754 人学习下载,适合作为数据库访问层的入门参考或项目脚手架,帮助读者快速理解 ADO.NET 通用模式与各数据库驱动的使用要点,减少重复摸索成本。
1. 从一份 C# 多数据库 Demo 说起:Oracle、SQL Server、SQLite 到底怎么选
如果你手头正维护着一套老系统,客户现场一会儿是 Oracle,一会儿是 SQL Server,测试环境又图省事用 SQLite,那你大概率经历过这种崩溃:同一段查询逻辑,换个数据库就报错,参数写法不一样,连自增主键的取值方式都得改。dataDemo.rar这个资源包,解决的就是这类“一套代码适配多种数据库”的落地问题。它用 C# 写了一套演示程序,把 Oracle、SQL Server、SQLite 三种数据库的连接、建表、增删改查、事务处理都跑通了,适合正在做数据访问层抽象、或者需要给不同客户交付不同数据库版本的开发者。你不需要从零去踩每种数据库的驱动差异,直接看它怎么组织代码、怎么抽接口、怎么处理分页和参数化查询就行。下面我按“先搞懂它为什么这么设计,再动手跑起来,最后说清楚哪些地方容易翻车”的顺序拆一遍。
2. 三种数据库的 C# 接入方式:驱动、连接串与参数化差异
2.1 为什么不能只写一套 SqlConnection
很多新手会想:反正都是 ADO.NET,我直接new SqlConnection改个连接串不就行了?实际不行。Oracle 和 SQL Server 虽然都实现了DbConnection抽象,但底层驱动完全独立。Oracle 要用Oracle.ManagedDataAccess.Client,SQL Server 用System.Data.SqlClient或Microsoft.Data.SqlClient,SQLite 则是System.Data.SQLite或Microsoft.Data.Sqlite。每个驱动对参数占位符的写法都不一样:SQL Server 用@p1,Oracle 用:p1,SQLite 两种都认但推荐@p1。如果你在 SQL 字符串里写死了@,换到 Oracle 直接抛异常。这个 Demo 的做法是定义一个IDbProvider接口,把CreateConnection、CreateCommand、GetLastInsertId这些方法抽出来,每种数据库写一个实现类。这样上层业务代码只依赖接口,切换数据库时改配置就行。
2.2 连接字符串的典型写法与坑点
先看三种数据库在 Demo 里用的连接串格式,我整理成表格方便对照:
| 数据库 | 连接字符串示例 | 关键参数说明 |
|---|---|---|
| SQL Server | Server=localhost;Database=TestDb;User Id=sa;Password=123456;TrustServerCertificate=True; | 本地开发常用Integrated Security=True走 Windows 认证;远程连接务必加TrustServerCertificate=True避免证书报错 |
| Oracle | Data Source=127.0.0.1:1521/ORCL;User Id=system;Password=oracle; | Data Source里1521是端口,ORCL是服务名,不是 SID;用 SID 要写成(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))(CONNECT_DATA=(SID=ORCL))) |
| SQLite | Data Source=./test.db;Version=3; | 文件路径用相对路径时注意工作目录;Version=3是必须的,否则可能按旧版解析 |
Demo 里把连接串放在App.config或appsettings.json里,通过一个DbType枚举来切换。我一般会再加一个ConnectionString的校验方法,在启动时尝试Open()一次,失败就给出明确提示,而不是等到第一次查询才报错。
2.3 参数化查询的代码实现
下面这段代码是 Demo 里SqlHelper的核心片段,我加了注释说明每个参数的作用:
public int ExecuteNonQuery(string sql, params DbParameter[] parameters) { using (var conn = _provider.CreateConnection(_connectionString)) { conn.Open(); using (var cmd = _provider.CreateCommand(sql, conn)) { // 关键:参数占位符由各 Provider 自己转换 // 比如传入的 sql 里写 @p1,OracleProvider 会替换成 :p1 foreach (var p in parameters) { cmd.Parameters.Add(p); } return cmd.ExecuteNonQuery(); } } }逻辑说明:_provider是IDbProvider的具体实现,CreateCommand内部会做占位符替换。参数说明:parameters用DbParameter[]而不是object[],是为了让调用方明确指定参数类型和长度,避免 Oracle 因为隐式转换导致索引失效。注意 Oracle 的DbParameter必须设置Size,否则字符串参数可能被截断。
2.4 自增主键的获取方式差异
这是最容易翻车的地方。SQL Server 用SCOPE_IDENTITY(),Oracle 用SEQUENCE.CURRVAL或者RETURNING INTO,SQLite 用last_insert_rowid()。Demo 里在IDbProvider定义了GetLastInsertId(DbConnection conn)方法,每种数据库各自实现。如果你在 Oracle 里忘了先取SEQUENCE.NEXTVAL就直接CURRVAL,会报“序列尚未在此会话中定义”。我一般会在插入前先执行一次SELECT seq.NEXTVAL FROM DUAL,把值存下来,插入时显式指定主键,这样最稳。
3. 把 Demo 跑起来:环境准备、建库建表与增删改查验证
3.1 开发环境与依赖包安装
Demo 基于 .NET Framework 或 .NET Core 都可以跑,我建议用 .NET 6+ 配合 Visual Studio 2022。需要安装的 NuGet 包如下:
Oracle.ManagedDataAccess.Core(Oracle 官方托管驱动,不需要装 Oracle 客户端)Microsoft.Data.SqlClient(SQL Server 驱动,比System.Data.SqlClient更新更活跃)Microsoft.Data.Sqlite(SQLite 驱动,轻量,跨平台)
用命令行安装:
dotnet add package Oracle.ManagedDataAccess.Core dotnet add package Microsoft.Data.SqlClient dotnet add package Microsoft.Data.Sqlite注意:Oracle 托管驱动虽然免客户端,但连接串里的Data Source格式必须写对,否则会报ORA-12154: TNS:could not resolve the connect identifier specified。我一般先用 SQL Developer 或 DBeaver 测试连接串,确认能连上再写进代码。
3.2 建表脚本与初始化数据
Demo 里为三种数据库分别准备了建表脚本。SQLite 最简单,直接CREATE TABLE IF NOT EXISTS;SQL Server 用IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Users');Oracle 用BEGIN EXECUTE IMMEDIATE 'CREATE TABLE ...'; EXCEPTION WHEN OTHERS THEN NULL; END;。下面以 SQLite 为例:
CREATE TABLE IF NOT EXISTS Users ( Id INTEGER PRIMARY KEY AUTOINCREMENT, UserName TEXT NOT NULL, Age INTEGER, CreatedAt TEXT DEFAULT (datetime('now', 'localtime')) );逻辑说明:AUTOINCREMENT保证 Id 单调递增,datetime('now', 'localtime')存本地时间。参数说明:TEXT对应 C# 的string,INTEGER对应int或long。注意 SQLite 没有真正的DateTime类型,存字符串比较通用,但排序时要用datetime()函数转换。
3.3 增删改查的完整调用示例
Demo 的Program.cs里有一段演示代码,我简化后贴出来:
var factory = new DbProviderFactory(DbType.SQLite, "Data Source=./test.db"); var helper = new SqlHelper(factory); // 插入 var insertSql = "INSERT INTO Users (UserName, Age) VALUES (@name, @age)"; var p1 = factory.CreateParameter("@name", "张三"); var p2 = factory.CreateParameter("@age", 28); helper.ExecuteNonQuery(insertSql, p1, p2); // 查询 var querySql = "SELECT Id, UserName, Age FROM Users WHERE Age > @minAge"; var readers = helper.ExecuteReader(querySql, factory.CreateParameter("@minAge", 18)); while (readers.Read()) { Console.WriteLine($"{readers["Id"]} - {readers["UserName"]}"); }逻辑说明:DbProviderFactory是 Demo 自己封装的工厂类,根据DbType返回对应的IDbProvider。参数说明:CreateParameter方法内部会处理不同数据库的参数前缀,调用方统一用@即可。注意ExecuteReader返回的DbDataReader用完必须Dispose,Demo 里用using包住了。
3.4 事务处理的统一写法
跨数据库的事务写法也有差异。SQL Server 用SqlTransaction,Oracle 用OracleTransaction,SQLite 用SqliteTransaction。Demo 在IDbProvider里定义了BeginTransaction(DbConnection conn)返回DbTransaction,上层代码只操作DbTransaction的Commit和Rollback。我一般会在SqlHelper里加一个ExecuteTransaction(Action<DbTransaction> action)方法,把try-catch-rollback的逻辑包起来,避免每次写重复代码。
4. 避坑与排查:驱动版本、字符集、连接池的五个血泪教训
4.1 现象:Oracle 报“ORA-00933: SQL 命令未正确结束”
原因:SQL 语句末尾带了分号。Oracle 的 ODP.NET 驱动不允许在单条命令里加分号,而 SQL Server 和 SQLite 都允许。
解决:在OracleProvider的CreateCommand里统一去掉末尾分号,或者写 SQL 时就不加。
4.2 现象:SQLite 插入中文变成乱码
原因:连接串没指定编码,或者数据库文件本身不是 UTF-8。
解决:连接串加UTF8Encoding=True,建库时确认PRAGMA encoding='UTF-8';。Demo 里默认就是 UTF-8,但如果你用旧版System.Data.SQLite驱动,需要显式设置。
4.3 现象:SQL Server 连接池耗尽,报“超时时间已到”
原因:DbConnection没有及时Dispose,连接池默认最大 100,泄漏几次就满了。
解决:所有DbConnection、DbCommand、DbDataReader都用using包住。Demo 里SqlHelper的每个方法都用了using,这是硬性要求。
4.4 现象:Oracle 参数化查询查不到数据,但直接拼 SQL 可以
原因:Oracle 的DbParameter默认按VARCHAR2处理,如果数据库字段是NVARCHAR2或CHAR,类型不匹配会导致隐式转换失败。
解决:创建参数时显式指定DbType,比如new OracleParameter("@name", OracleDbType.NVarchar2) { Value = "张三" }。Demo 的CreateParameter方法里根据值类型自动推断,但复杂场景建议手动指定。
4.5 现象:SQLite 并发写入报“database is locked”
原因:SQLite 默认是文件锁,多个线程同时写会冲突。
解决:连接串加Pooling=True;Max Pool Size=1;强制串行写入,或者用BEGIN IMMEDIATE事务提前占锁。Demo 里演示了单线程写入,生产环境如果有多线程需求,建议换 SQL Server 或 Oracle。
5. 进阶技巧:用依赖注入把多数据库切换做成配置项
5.1 在 .NET Core 里注册 IDbProvider
Demo 原本是控制台程序,我把它改造成 ASP.NET Core 的依赖注入模式,这样切换数据库只需要改appsettings.json:
// Program.cs builder.Services.AddSingleton<IDbProvider>(sp => { var config = sp.GetRequiredService<IConfiguration>(); var dbType = config.GetValue<DbType>("Database:Type"); return dbType switch { DbType.SqlServer => new SqlServerProvider(), DbType.Oracle => new OracleProvider(), DbType.SQLite => new SqliteProvider(), _ => throw new NotSupportedException($"不支持的数据库类型: {dbType}") }; }); builder.Services.AddScoped<SqlHelper>();逻辑说明:DbType枚举从配置读取,switch表达式返回对应的 Provider 实例。参数说明:AddSingleton保证 Provider 只创建一次,AddScoped让SqlHelper跟随请求生命周期。注意 Oracle 的OracleConnection不是线程安全的,SqlHelper里每次操作都新建连接,不要复用。
5.2 用配置文件驱动建表脚本
我把三种数据库的建表脚本放在Scripts/目录下,命名规则是{DbType}_Init.sql。启动时根据当前DbType读取对应文件并执行:
var scriptFile = $"Scripts/{dbType}_Init.sql"; var script = File.ReadAllText(scriptFile); // 按分号拆分,逐条执行 foreach (var sql in script.Split(';', StringSplitOptions.RemoveEmptyEntries)) { if (!string.IsNullOrWhiteSpace(sql)) helper.ExecuteNonQuery(sql.Trim()); }注意:Oracle 的 PL/SQL 块里也有分号,简单拆分不行。我一般用OracleCommand的CommandText直接执行整个脚本,或者用SqlPlus的@命令。Demo 里为了简化,建表脚本都是单条 SQL,没有 PL/SQL 块。
5.3 验证切换是否成功的检查清单
每次切换数据库后,我强制走一遍这个清单:
| 检查项 | 预期结果 | 失败时看什么 |
|---|---|---|
| 连接打开 | conn.State == ConnectionState.Open | 连接串、防火墙、服务是否启动 |
| 建表 | 表存在且字段类型正确 | 脚本是否执行、权限是否足够 |
| 插入并取回 Id | Id 大于 0 | 自增/序列配置、GetLastInsertId实现 |
| 参数化查询 | 返回预期行数 | 参数类型、占位符前缀 |
| 事务回滚 | 数据未写入 | Rollback是否调用、连接是否复用 |
从那以后我每次给客户部署前,都会把这份清单跑一遍,确认三种数据库都通过再交付。希望帮到你。
本文还有配套的精品资源,点击获取