☰
C#操作Access数据库:封装可复用源码工程与避坑指南
2026/10/3 8:56:15 网站建设 项目流程

简介:这是一套面向C#开发者、数据库初学者及需要快速集成Access能力的工程师的源码工程,围绕Access数据库的创建与操作封装了独立类库,可直接引入任意项目复用,省去从零开发的时间。包内共97个文件,以41个dll、13个cs源码、8个json配置、2个csproj工程文件及若干pdb、exe、md说明为主,压缩包约2.53MB,结构清晰便于按模块查阅。工程覆盖三种常用连接字符串获取、代码创建数据库与数据表、通用SQL执行帮助类(含单条与批量插入、事务处理、指定内容查询、分页查询、更新、删除),以及获取所有表名与字段名等能力,并附带完整的增删查改使用示例。目前已有1996人学习下载,适合希望把精力集中在业务逻辑、快速掌握Access操作套路的开发者参考借鉴。

1. 用 C# 把 Access 数据库操作封装成可复用的源码工程:一次讲清能不能做、值不值得做

很多人第一次接到「用 C# 操作 Access 数据库」的需求时,第一反应是这玩意儿太老了,是不是该直接上 SQLite 或者 SQL Server。但现实是,大量中小型制造企业、上位机项目、工控现场的历史数据仍然躺在.mdb或.accdb文件里,客户不会为了你重构数据层,他们只会说一句:能不能直接读。于是你绕不开 Access 数据库,也绕不开 C# 连接 Access 数据库这条链路。这个源码项目工程要解决的核心问题就三件事:怎么用 C# 稳定地创建 Access 数据库文件、怎么对表做增删改查、怎么把这套逻辑封装成别人能直接拿去用的工程结构。适合谁看?适合做 C# 上位机、做 MES 数据采集、做小型管理系统,手里有一堆 Access 文件要处理的工程师。不适合想找 ORM 银弹的人,Access 这块没有银弹,只有踩坑。

2. Access 数据库在 C# 里的连接原理与驱动选型:为什么你的程序在 64 位机器上翻车

2.1 Access 数据库不是服务,它是一个文件加一套驱动

Access 数据库和 SQL Server、MySQL 最大的区别在于:它没有独立的数据库服务进程。.mdb和.accdb本质上就是一个文件,C# 程序通过 OLE DB 或者 ODBC 驱动去读写这个文件。这意味着两件事:第一,你的程序不需要连网络,本地文件路径对了就能跑;第二,驱动装没装、装的是 32 位还是 64 位,直接决定你的程序能不能启动。

C# 连接 Access 最常见的方式是System.Data.OleDb命名空间下的OleDbConnection,连接字符串长这样:

// 连接 Access 2007+ 的 .accdb 文件 string connStr = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=D:\data\demo.accdb;Persist Security Info=False;";

如果是老的.mdb文件,Provider 要换成Microsoft.Jet.OLEDB.4.0。这两个 Provider 的区别不是版本号高低,而是 Jet 引擎只支持 32 位,ACE 引擎有 32 位和 64 位两个版本。这就是为什么很多人在 64 位系统上跑程序,报「未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0 提供程序」,因为你的程序编译成了 AnyCPU 或者 x64,但机器上只装了 32 位的 ACE 驱动。

2.2 驱动选型的三个决策点:位数、版本、部署方式

第一个决策点是位数匹配。你的 C# 程序目标平台如果是 x86,就必须装 32 位 ACE 驱动;如果是 x64,就必须装 64 位 ACE 驱动。AnyCPU 在 64 位系统上默认按 64 位跑,所以也会要求 64 位驱动。最稳妥的做法是在项目属性里把目标平台固定成 x86,然后统一装 32 位驱动,因为 32 位驱动兼容性更好,很多老工控机上的其他组件也是 32 位的。

第二个决策点是 ACE 版本。Microsoft.ACE.OLEDB.12.0 对应 Access 2007 及以上,Microsoft.ACE.OLEDB.16.0 对应 Office 2016 及以上。如果你机器上装了 Office,Office 安装过程会自动带上对应版本的 ACE 驱动。但注意,Office 的位数决定了 ACE 驱动的位数,装了 64 位 Office 就只有 64 位 ACE,装了 32 位 Office 就只有 32 位 ACE。这就是为什么有些开发机跑得好好的程序,换一台机器就报驱动未注册。

第三个决策点是部署方式。你不能假设客户机器上装了 Office,所以发布程序时要么让客户单独安装 Access Database Engine 可再发行组件,要么在安装包里静默安装。常见做法是在安装脚本里检测注册表HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Microsoft.ACE.OLEDB.12.0是否存在,不存在就调用驱动安装包。

提示:不要试图在同一个进程里同时加载 32 位和 64 位 ACE 驱动,这是不可能的。进程位数在启动时就确定了,驱动必须匹配。

2.3 用代码检测驱动是否可用,而不是等报错

与其等OleDbConnection.Open()抛异常,不如在程序启动时主动检测。下面这段代码枚举本机已注册的 OLE DB Provider,判断 ACE 或 Jet 是否存在:

using System; using System.Data.OleDb; public static class AccessDriverChecker { /// <summary> /// 检测指定 Provider 是否已注册 /// </summary> /// <param name="providerName">例如 Microsoft.ACE.OLEDB.12.0</param> /// <returns>true 表示可用</returns> public static bool IsProviderRegistered(string providerName) { try { // 不打开连接,只枚举 Provider 列表 using (var conn = new OleDbConnection()) { conn.ConnectionString = $"Provider={providerName};"; // Open 会触发 Provider 加载,失败即未注册 conn.Open(); return true; } } catch (OleDbException) { return false; } catch (InvalidOperationException) { return false; } } }

这段代码的逻辑是:构造一个只带 Provider 的连接字符串,调用Open()。如果 Provider 未注册,会抛OleDbException,错误信息里包含「未注册」关键字。参数providerName传Microsoft.ACE.OLEDB.12.0或Microsoft.Jet.OLEDB.4.0。注意这个方法会短暂打开一个连接,虽然不指向具体文件,但某些环境下可能报「未指定数据源」,所以更稳妥的做法是直接读注册表,不过枚举 Provider 的方式跨版本更通用。

3. 从零创建 Access 数据库文件:用 ADOX 还是用 SQL 命令

3.1 创建 .mdb 和 .accdb 的两种路径

C# 创建 Access 数据库文件有两条路。第一条是用 ADOX(Microsoft ADO Ext. for DDL and Security)组件,通过 COM 互操作调用CatalogClass.Create()方法。第二条是用OleDbConnection连接到一个不存在的文件,某些 Provider 会自动创建,但 ACE 驱动默认不会自动创建.accdb,Jet 驱动也不会自动创建.mdb。所以实际项目中,创建数据库文件基本都用 ADOX。

ADOX 的引用方式是在项目里添加 COM 引用Microsoft ADO Ext. 2.8 for DDL and Security,然后在代码里using ADOX;。注意 ADOX 是 COM 组件,部署时不需要额外注册,但需要目标机器有对应的 ACE 或 Jet 驱动。

3.2 创建数据库并建表的完整代码

下面这段代码演示用 ADOX 创建.accdb文件,然后用 SQL 建一张表:

using System; using ADOX; public static class AccessDbCreator { /// <summary> /// 创建 Access 数据库文件 /// </summary> /// <param name="filePath">例如 D:\data\demo.accdb</param> /// <param name="provider">Microsoft.ACE.OLEDB.12.0 或 Microsoft.Jet.OLEDB.4.0</param> public static void CreateDatabase(string filePath, string provider) { if (System.IO.File.Exists(filePath)) throw new InvalidOperationException("文件已存在,不要覆盖"); // CatalogClass 是 ADOX 的核心类 var catalog = new CatalogClass(); try { // 连接字符串只指定 Provider,不指定文件 string connStr = $"Provider={provider};Data Source={filePath};"; catalog.Create(connStr); } finally { // 释放 COM 对象,避免文件被占用 System.Runtime.InteropServices.Marshal.ReleaseComObject(catalog); } } }

创建完文件后,用OleDbConnection打开并执行CREATE TABLE:

using System.Data.OleDb; public static void CreateTable(string filePath, string provider) { string connStr = $"Provider={provider};Data Source={filePath};"; using (var conn = new OleDbConnection(connStr)) { conn.Open(); string sql = @"CREATE TABLE DeviceLog ( Id AUTOINCREMENT PRIMARY KEY, DeviceName TEXT(50), Value DOUBLE, LogTime DATETIME )"; using (var cmd = new OleDbCommand(sql, conn)) { cmd.ExecuteNonQuery(); } } }

参数说明:AUTOINCREMENT是 Access 的自增主键写法,对应 SQL Server 的IDENTITY。TEXT(50)是 Access 的变长字符串,不写长度默认 255。DOUBLE对应 C# 的double,DATETIME对应DateTime。注意 Access 的 SQL 方言和 T-SQL 有差异,比如不支持NVARCHAR,用TEXT或MEMO。

3.3 创建时的三个硬性约束

第一,文件路径不能包含特殊字符,尤其是#和&,OLE DB 连接字符串里这些字符有特殊含义,会导致解析失败。第二,目标目录必须存在,ADOX 不会自动创建目录。第三,如果文件已存在,catalog.Create()会抛异常,所以要先判断。我一般会在创建前用File.Exists检查,如果存在就抛业务异常,让调用方决定是覆盖还是换名。

注意:ADOX 创建.accdb时,如果机器上只有 32 位 ACE 驱动,而你的程序是 64 位,catalog.Create()会报「未注册提供程序」。这不是 ADOX 的问题,是驱动位数不匹配。

4. 增删改查的封装:参数化查询、事务和批量插入的落地写法

4.1 为什么必须用参数化查询,以及 Access 的参数占位符

Access 的 OLE DB 参数占位符是?,不是@name。这意味着你不能像 SQL Server 那样写WHERE Id = @id,只能写WHERE Id = ?,然后按顺序Add参数。顺序错了,结果就错了,这是 Access 开发里最常见的翻车点之一。

下面是一个封装好的查询方法:

using System.Data; using System.Data.OleDb; public static DataTable Query(string filePath, string provider, string sql, params OleDbParameter[] parameters) { string connStr = $"Provider={provider};Data Source={filePath};"; using (var conn = new OleDbConnection(connStr)) using (var cmd = new OleDbCommand(sql, conn)) { if (parameters != null && parameters.Length > 0) cmd.Parameters.AddRange(parameters); var dt = new DataTable(); using (var adapter = new OleDbDataAdapter(cmd)) { adapter.Fill(dt); } return dt; } }

调用时:

var dt = Query(@"D:\data\demo.accdb", "Microsoft.ACE.OLEDB.12.0", "SELECT * FROM DeviceLog WHERE DeviceName = ? AND LogTime > ?", new OleDbParameter("@p1", "PLC-01"), new OleDbParameter("@p2", DateTime.Today));

注意OleDbParameter的名字其实不重要,Access 只看顺序。但为了代码可读性,我一般还是给个名字。参数类型要匹配,DateTime传DateTime,不要传字符串,否则 Access 可能按文本比较,导致日期范围查询结果不对。

4.2 事务和批量插入:Access 的性能边界在哪

Access 单条插入的性能很差,一千条数据逐条ExecuteNonQuery可能要几十秒。正确做法是用事务包起来,或者用OleDbDataAdapter批量更新。下面是一个事务批量插入的写法:

public static void BatchInsert(string filePath, string provider, DataTable data) { string connStr = $"Provider={provider};Data Source={filePath};"; using (var conn = new OleDbConnection(connStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) using (var cmd = new OleDbCommand( "INSERT INTO DeviceLog (DeviceName, Value, LogTime) VALUES (?, ?, ?)", conn, tran)) { // 预定义参数,循环里只改值 cmd.Parameters.Add(new OleDbParameter("@name", OleDbType.VarWChar)); cmd.Parameters.Add(new OleDbParameter("@value", OleDbType.Double)); cmd.Parameters.Add(new OleDbParameter("@time", OleDbType.Date)); foreach (DataRow row in data.Rows) { cmd.Parameters[0].Value = row["DeviceName"]; cmd.Parameters[1].Value = row["Value"]; cmd.Parameters[2].Value = row["LogTime"]; cmd.ExecuteNonQuery(); } tran.Commit(); } } }

参数说明:OleDbType.VarWChar对应 Access 的TEXT,OleDbType.Double对应DOUBLE,OleDbType.Date对应DATETIME。事务提交前所有插入都在一个事务里,Access 会写日志文件,速度比逐条提交快一个数量级。但注意,Access 单表数据量超过 10 万行后,查询性能会明显下降,这不是 C# 能解决的,是 Jet/ACE 引擎的边界。

4.3 更新和删除的注意事项

更新和删除的 SQL 写法没有特殊之处,但有两个坑。第一,Access 不支持UPDATE ... FROM语法,多表关联更新要写成子查询。第二,删除时如果表有自增主键,删除后自增值不会重置,这是正常行为,不要试图去改系统表。下面是一个更新示例:

public static int Update(string filePath, string provider, string deviceName, double newValue) { string connStr = $"Provider={provider};Data Source={filePath};"; using (var conn = new OleDbConnection(connStr)) using (var cmd = new OleDbCommand("UPDATE DeviceLog SET Value = ? WHERE DeviceName = ?", conn)) { cmd.Parameters.Add(new OleDbParameter("@value", newValue)); cmd.Parameters.Add(new OleDbParameter("@name", deviceName)); conn.Open(); return cmd.ExecuteNonQuery(); // 返回受影响行数 } }

ExecuteNonQuery返回受影响行数,可以用来判断更新是否命中记录。如果返回 0,说明WHERE条件没匹配到任何行,不是报错,是业务上没找到。

5. 避坑与排查:Access 数据库在 C# 项目里的五个血泪教训

5.1 报「未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0 提供程序」

现象:程序在开发机跑得好好的,部署到客户机器上直接崩,异常信息就是这句。

原因:客户机器没装 ACE 驱动,或者装的位数和程序不匹配。开发机装了 Office,Office 带了驱动,客户机可能只装了 WPS 或者什么都没装。

解决:在安装包里附带 Access Database Engine 可再发行组件,安装前检测注册表。如果客户机是 64 位系统但装的是 32 位 Office,你的程序必须编译成 x86。最稳的组合是:程序目标平台 x86,安装 32 位 ACE 驱动。

5.2 插入中文变成乱码或者问号

现象:INSERT进去的中文,用 Access 打开看是乱码,或者直接变成???。

原因:连接字符串没有指定字符集,或者参数类型用了OleDbType.VarChar而不是VarWChar。VarChar是 ANSI 字符集,VarWChar是 Unicode。

解决:参数类型统一用OleDbType.VarWChar,连接字符串里可以加Jet OLEDB:Global Partial Bulk Ops=2之类的优化参数,但字符集问题主要靠参数类型解决。

5.3 多线程同时写同一个 Access 文件导致文件锁死

现象:上位机多个线程同时往 Access 写数据,跑一段时间后报「文件已被占用」或者「无法锁定文件」。

原因:Access 是文件级锁,不是行级锁。多个连接同时写,ACE 引擎会尝试加锁,并发高了就冲突。

解决:所有写操作串行化,用一个全局队列或者lock对象包住。如果并发量确实大,考虑换 SQLite 或者 SQL Server。Access 的定位就是单机、低并发,不要拿它当高并发数据库用。

5.4 查询日期范围结果不对

现象:WHERE LogTime > ?传DateTime.Today,结果查出来的数据少了或者多了。

原因:Access 的日期存储包含时间部分,DateTime.Today是当天 00:00:00,如果数据里有当天更早的时间,会被排除。另外,如果参数类型传成了字符串,Access 会按文本比较,"2024-1-1"和"2024-01-01"排序结果不同。

解决:参数类型用OleDbType.Date,查询范围用>=和<组合,比如LogTime >= ? AND LogTime < ?,传DateTime.Today和DateTime.Today.AddDays(1)。

5.5 程序退出后 Access 文件被锁定,无法删除或重命名

现象:程序关了,但.accdb文件删不掉,提示被占用。

原因:OleDbConnection没有Dispose,或者OleDbDataReader没有关闭。ACE 引擎会保持文件句柄,直到连接池释放。

解决:所有OleDbConnection、OleDbCommand、OleDbDataReader都用using包起来。如果用了连接池,可以在连接字符串里加OLE DB Services=-4禁用连接池,但更推荐的做法是确保using到位。另外,ADOX 的CatalogClass是 COM 对象,用完必须Marshal.ReleaseComObject。

6. 把源码工程做成可复用的 NuGet 包或模板项目的具体技巧

6.1 工程结构怎么分层才不乱

一个可复用的 Access 操作工程,我一般分成三层:Access.Core放连接管理、驱动检测、异常封装;Access.Repository放具体的增删改查和事务;Access.Models放实体类。这样别人引用的时候,如果只想用连接管理,就只引Access.Core。如果标题里的「源码项目工程」是要给别人直接用的,那入口类要足够简单,比如一个AccessDbFactory类,构造函数传文件路径和 Provider,然后暴露Query、Execute、BatchInsert几个方法就够了。

6.2 用接口隔离,方便以后换 SQLite

虽然现在用 Access,但保不齐哪天客户要换 SQLite。所以我会定义一个IDbProvider接口:

public interface IDbProvider { DataTable Query(string sql, params object[] parameters); int Execute(string sql, params object[] parameters); void BeginTransaction(); void Commit(); void Rollback(); }

Access 的实现类叫AccessDbProvider,SQLite 的实现类叫SqliteDbProvider。上层业务代码只依赖IDbProvider,换数据库时只改工厂方法。这个技巧在工控上位机项目里特别有用,因为不同现场的历史数据格式可能不一样。

6.3 验证方法:用单元测试覆盖驱动检测和增删改查

不要靠手动点按钮验证。写一个 xUnit 或者 NUnit 测试项目,每个测试方法创建一个临时.accdb文件,跑完删除。下面是一个测试示例:

[Fact] public void CreateInsertQuery_ShouldReturnSameValue() { string tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".accdb"); string provider = "Microsoft.ACE.OLEDB.12.0"; try { AccessDbCreator.CreateDatabase(tempFile, provider); CreateTable(tempFile, provider); BatchInsert(tempFile, provider, BuildTestData()); var dt = Query(tempFile, provider, "SELECT COUNT(*) FROM DeviceLog"); Assert.Equal(100, Convert.ToInt32(dt.Rows[0][0])); } finally { if (File.Exists(tempFile)) File.Delete(tempFile); } }

这个测试覆盖了创建、建表、批量插入、查询四个环节。参数说明:Guid.NewGuid()保证文件名不冲突,Path.GetTempPath()避免权限问题。跑完在finally里删文件,防止临时文件堆积。如果测试报驱动未注册,说明 CI 环境没装 ACE 驱动,需要在 CI 脚本里加安装步骤。

6.4 一个我踩过的坑:不要用SELECT *做长期维护

早期图省事,查询全用SELECT *,后来表结构加了字段,DataTable 的列顺序变了,下游解析代码直接崩。后来我改成显式列名,虽然写起来麻烦,但表结构变更时影响可控。这个习惯在 Access 项目里尤其重要,因为 Access 改表结构不像 SQL Server 那么方便,很多时候是直接加字段,列顺序不可控。

6.5 值不值得做:Access 方案的适用边界

如果你的项目是单机、数据量在 10 万行以内、并发不超过 5 个写入线程,Access 加 C# 这套方案完全够用,开发速度快,部署简单,客户不需要装数据库服务。但如果数据量上百万、多客户端同时写、需要网络访问,趁早换 SQLite 或者 SQL Server。我一般会在项目初期就跟客户确认数据量和并发量,如果超了边界,直接建议换方案,不要硬上 Access,后期迁移成本更高。希望帮到你。

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

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

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

立即咨询