
简介整套基于C#开发的Access数据库创建与操作源码工程面向需要快速集成桌面或Web数据库能力的.NET开发者旨在解决从Access数据库底层的建库、建表到日常增删改查、分页查询、批量插入、事务提交等全链路重复开发问题。工程已将核心操作独立封装为多个帮助类支持自动获取常用连接字符串或传入自定义连接可直接编译生成DLL后嵌入业务项目复用节省大量基础代码编写时间。压缩包共97个文件大小约2.53MB主要包含41个dll类库、13个cs源码文件、8个json配置以及项目工程文件、调试符号等目录结构清晰便于按需提取。资源附带了完整示例程序从创建Access数据库与数据表开始依次演示了单条与批量插入、更新删除、普通与分页查询以及读取数据库所有表和字段名等操作每个环节均有对应帮助类调用示例代码注释详细适合C#初学者理解数据访问层的封装思路也适合开发者在中小型管理系统、桌面工具中直接套用或二次扩展目前已有1995人学习下载。1. 用 C# 创建 Access 数据库比你想的更简单也更讲究很多人写 C# 操作 Access 数据库第一反应是「装 Office用 Access 建好库然后拷个 .accdb 文件到程序目录」。但真正做上位机、桌面工具和中小型管理系统时你会发现客户机器上既没有 Office也没有 Access 运行时连 OLEDB 提供程序都得手动装。更麻烦的是程序在 64 位 Windows 上跑报错「未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0」是家常便饭。这个用 C# 开发的 Access 数据库创建、操作的源码项目工程核心价值其实不在「建库」那一个动作而在于把「创建数据库文件 → 建表 → CRUD → 处理 64 位驱动兼容 → 并发访问」整条链路打通。适合写上位机历史数据存储、车间报表采集、单机版工具软件的人内容按「驱动环境 → 创建库 → 增删改查 → 并发与性能 → 验证与进阶」推进直接可抄。2. 环境准备Access 数据库驱动和 C# 项目的位数匹配2.1 为什么 64 位系统上连接 Access 总是报错连接 Access 数据库的 OLEDB 提供程序有两条线老一代的Microsoft.Jet.OLEDB.4.0和微软后来主推的Microsoft.ACE.OLEDB.12.0。Jet 4.0 只带在 32 位 Windows 里64 位系统上必须靠 ACE。而 ACE 引擎的安装包分 x86 和 x64 两套一个系统里不能同时装两个版本装了 x64 版之后32 位程序用ACE.OLEDB.12.0依旧报错注册失败。这就是网上所谓「Access 数据库 64 位系统驱动程序」问题的大本营。我一般建议的路线是x64 系统 安装 64 位 Access Database Engine 项目平台目标设为 x64 或 AnyCPU取消「首选 32 位」如果包体里有旧版 32 位依赖才反过来用 x86。这个事没有通解只能按部署环境锁死。2.2 连接串的参数怎么设在App.config里写连接串时注意 Provider 和文件路径不宜写死相对路径connectionStrings add nameAccessDb connectionStringProviderMicrosoft.ACE.OLEDB.12.0;Data SourceD:\data\history.accdb;Persist Security InfoFalse; providerNameSystem.Data.OleDb / /connectionStrings这里Data Source指向的是数据库文件路径如果文件不存在部分场景下会被创建取决于你用什么方式触发Persist Security InfoFalse表示不在连接后保留密码信息。使用System.Data.OleDb命名空间时类型统一走OleDbConnection、OleDbCommand、OleDbDataAdapter这套东西和 SQL Server 版的 SqlClient 用法几乎一一对应只是参数符号从变成?。2.3 项目平台目标检查清单配置项推荐值说明目标平台x64有条件地选 x86与所装 ACE 引擎位数严格一致首选 32 位取消勾选默认勾选会导致 AnyCPU 编译后以 32 位运行引用System.Data.OleDb.NET Framework 4.x 自带.NET Core/.NET 5 需附加包ACE 引擎安装2016 Redistributable x64官方下载中心搜「Access Database Engine 2016 Redistributable」提示用 Windows Forms 上位机时注意c# 上位机项目里经常混用 32 位第三方相机 SDK、串口库这种场景下我一般把整个解决方案锁成 x86ACE 引擎也装 32 位版否则一个进程里混合位数会直接 BadImageFormatException。3. 创建 Access 数据库文件和初始化表结构3.1 用 OleDbConnection 直接创建 .accdb 文件Access 的 OLEDB 提供程序有一个隐藏行为当连接串里的Data Source指定的文件不存在时Open()会自动创建一个空库文件。这是最常见的建库方式代码很短using System.Data.OleDb; var connString ProviderMicrosoft.ACE.OLEDB.12.0;Data SourceD:\\data\\product.accdb;; using (var conn new OleDbConnection(connString)) { conn.Open(); }Open()执行成功后D:\data 目录下会生成一个 0 字节左右的 .accdb 文件。此时库里没有任何表但文件本身是有效的新版 Access 数据库格式。这个技巧对「程序第一次启动自动初始化数据库」特别有用不需要安装 Office也不需要预置模板文件。不过要特别注意Open()建的文件是 Access 2007-2016 格式ACCDB不是老式 MDB。如果客户侧工具只能读 MDB就得换 ADOX 方式建 2000 格式的库或者用 Access 的另存功能做转换。3.2 用 ADOX 创建老式 MDB 数据库当目标环境需要使用Microsoft.Jet.OLEDB.4.0或者必须生成 MDB 格式时常见的做法是引用 ADOX。在 C# 中添加 COM 引用Microsoft ADO Ext. 6.0 for DDL and Security再通过ADOX.Catalog创建var catalog new ADOX.Catalog(); catalog.Create(ProviderMicrosoft.Jet.OLEDB.4.0;Data SourceD:\\data\\legacy.mdb); catalog.ActiveConnection null;Catalog.Create的入参是一个完整的连接串而非纯路径它调用的是 Jet 引擎的建库接口生成的库格式是 Access 2000 兼容格式把 Provider 换成 ACE 也能建 ACCDB。根据我的经验这个方式适合在上位机程序中做「按日期滚动历史数据库」的定时任务。3.3 创建表的 DDL 和字段类型映射数据库文件创建后第一步是建表。Access 的 DDL 走CREATE TABLE类型关键字与 SQL Server 有明显差异——没有int、varchar而是INTEGER、TEXT、MEMO、DATETIME之类。string[] ddlStatements new string[] { CREATE TABLE History( Id INTEGER PRIMARY KEY, RecordTime DATETIME, StationName TEXT(20), Temperature REAL, IsPassed BIT, Memo MEMO) }; using (var conn new OleDbConnection(connString)) { conn.Open(); foreach (var sql in ddlStatements) { using (var cmd new OleDbCommand(sql, conn)) { cmd.ExecuteNonQuery(); } } }各类型对应场景TEXT(n)存短字符串最大 255 字符MEMO存长文本REAL存单精度浮点记录温度、压力这类传感器数值时够用BIT对应布尔值DATETIME存上位机采集时间。在给表设计字段时要注意 Access 的INTEGER是 32 位整型超过 21 亿的计数建议用LONG注意 Access 的LONG其实是 64 位整型。提示PRIMARY KEY直接定义在 CREATE TABLE 里Access 会对该列自动建立索引。常见的坑是主键用INTEGER且手工插入记录时不赋值会导致主键冲突因为 Access 不会像 SQL Server 那样自动生成自增值——除非你声明AUTOINCREMENT。3.4 自增列和默认值在 Access 里的写法自增主键的完整建表语句和 SQL Server 不同ACCDB 中要显式使用AUTOINCREMENT关键字CREATE TABLE AlarmRecord( Id AUTOINCREMENT PRIMARY KEY, AlarmTime DATETIME DEFAULT NOW(), [Level] INTEGER, Message TEXT(200) )这里AUTOINCREMENT让 Access 自动从 1 开始递增DEFAULT NOW()让该字段在未显式赋值时自动写入当前时间。日常开发里常见的错误是照搬 SQL Server 语法写IDENTITY(1,1)这会直接报语法错误。给字段起名时如果用了 Level、Text 这类 Access 的保留字需要加[]括起来。4. C# 操作 Access 的增删改查与单条记录显示4.1 封装一个紧凑的 AccessHelper既然是「源码项目工程」应当有一个集中管理的帮助类把连接、执行、查询都收拢。以下是我在多数项目中使用的精简封装规避了反复解析连接串的问题using System.Data; using System.Data.OleDb; public class AccessHelper { private readonly string _connString; public AccessHelper(string connString) { _connString connString; } public int ExecuteNonQuery(string sql, params OleDbParameter[] parameters) { using (var conn new OleDbConnection(_connString)) using (var cmd new OleDbCommand(sql, conn)) { if (parameters ! null parameters.Length 0) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } public DataTable ExecuteQuery(string sql, params OleDbParameter[] parameters) { using (var conn new OleDbConnection(_connString)) using (var cmd new OleDbCommand(sql, conn)) using (var adapter new OleDbDataAdapter(cmd)) { if (parameters ! null parameters.Length 0) cmd.Parameters.AddRange(parameters); var dt new DataTable(); adapter.Fill(dt); return dt; } } }ExecuteNonQuery返回受影响行数适用于 INSERT、UPDATE、DELETEExecuteQuery内部用 DataAdapter 把结果集一次性填充到DataTable适用于查询。每次操作都开一个新连接用完即释放在单机程序中是最稳的策略避免长连接被 Access 锁文件机制打断。4.2 显示一条记录的全部字段数据「c#显示查找一条记录字段数据」是高频搜索需求在 WinForms 里最常见的坑是把整行数据塞进 ListBox 时只显示成了System.Data.DataRowView或者拼字符串时遗漏空值列。下面这段代码把指定编号的记录按「字段名: 值」输出到文本框var db new AccessHelper(connString); DataTable dt db.ExecuteQuery( SELECT * FROM History WHERE Id ?, new OleDbParameter(p1, 1001)); if (dt.Rows.Count 0) { DataRow row dt.Rows[0]; var sb new StringBuilder(); foreach (DataColumn col in dt.Columns) { string val row[col] DBNull.Value ? (NULL) : row[col].ToString(); sb.AppendLine(${col.ColumnName}: {val}); } txtRecordDetail.Text sb.ToString(); }注意这里参数写的是?OleDbParameter 的名称和个数要按?的顺序对应即使参数命名为p1也不会生效这只是为了可读性。DBNull.Value的判断必不可少Access 的 TEXT 字段空值在 DataRow 里取出来是DBNull直接ToString()不会报错但会丢信息。4.3 分页查询怎么在 Access 上落地Access 不支持 SQL Server 那种OFFSET ... FETCH常见做法是用子查询嵌套TOP实现SELECT * FROM History WHERE Id IN ( SELECT TOP 20 Id FROM ( SELECT TOP 40 Id FROM History ORDER BY RecordTime DESC ) ORDER BY RecordTime ASC ) ORDER BY RecordTime DESC外层TOP 20取当前页条数内层TOP 40等于页码乘以页大小。把40换成(pageIndex * pageSize)参数即可实现通用分页。这种做法在百万行以内性能可接受数据量更大时建议迁 SQLite 或 SQL Server LocalDB毕竟 Access 的定位就不是高并发数据库。4.4 更新和删除语句的易错点Access 的 UPDATE 和 DELETE 语法与 SQL Server 基本相同但有两个大坑。第一个更新 TEXT 字段时如果写入超过 255 字符会比较难定位因为 DDL 里定义了TEXT(20)会导致多余字符静默截断建议直接建MEMO类型。第二个DELETE 所有行后再插入数据时Access 的自增计数器不会回退简单 DELETE FROM 表名 不会重置AUTOINCREMENT起点需要再执行一条ALTER TABLE 表名 ALTER COLUMN Id COUNTER(1,1)来重置。string updateSql UPDATE History SET StationName ?, Temperature ? WHERE Id ?; int affected db.ExecuteNonQuery(updateSql, new OleDbParameter(p1, A线), new OleDbParameter(p2, 25.6), new OleDbParameter(p3, 1001));参数化更新的核心价值是避免字符串拼接带来的 SQL 注入风险同时省去手工处理单引号转义。Access 下参数顺序必须与语句中的?出现顺序一致参数名仅供参考。ExecuteNonQuery返回的行数等于实际改动行数如果更新的值与旧值相同Access 会返回 0——这往往会引发业务判断上的误会排查时先确认这一点。5. 并发写入、多线程采集和 UI 刷新卡顿的瓶颈处理5.1 Access 锁机制与连接串的超时参数Access 是文件型数据库写入时对文件加排他锁并发能力天然弱。多个线程同时写同一张表时报「文件正在使用中」的概率不低。处理这个问题的常见做法是在连接串中增加两个参数ProviderMicrosoft.ACE.OLEDB.12.0;Data SourceD:\data\history.accdb;Persist Security InfoFalse;Jet OLEDB:Database Locking Mode0;Jet OLEDB:Connection Timeout30;Jet OLEDB:Database Locking Mode0表示共享模式允许多个连接读Connection Timeout的单位是秒它控制获取文件锁的等待时间适当调大能减少高频写入时的异常暴露。要说明的是这些参数并不能把 Access 变成并发数据库它们只是让锁冲突延迟暴露、尽可能不中断程序真正的防线应该放在写入策略上。5.2 写入队列把并发写变成串行写上位机采集往往每秒产生几十条记录如果每个传感器线程都直接调用INSERT轮询和锁等待会拖垮 UI。我一般会做一个「队列 单消费线程」的方案private ConcurrentQueueHistoryEntity _queue new ConcurrentQueueHistoryEntity(); private void Producer() { var data new HistoryEntity { RecordTime DateTime.Now, Temperature sensorValue }; _queue.Enqueue(data); } private void ConsumerLoop() { while (!_cancelled) { if (_queue.TryDequeue(out var item)) { db.ExecuteNonQuery(INSERT INTO History(RecordTime, Temperature) VALUES(?, ?), new OleDbParameter(p1, item.RecordTime), new OleDbParameter(p2, item.Temperature)); } else { Thread.Sleep(50); } } }ConcurrentQueue处理多生产者单消费者的模型很顺手TryDequeue失败说明队列空Sleep 50ms 防止空转打满 CPU。这套模式不仅解决数据库写入冲突还顺带把采集线程和存储线程解耦——采集线程只入队绝不等待磁盘写完成。5.3 批量插入的常见手段与事务边界大批量回补数据时逐条 INSERT 效率过低Access 也没有 SQL Server 的BulkCopy。常见的方案是「事务 分批提交」using (var conn new OleDbConnection(_connString)) { conn.Open(); using (var tx conn.BeginTransaction()) { var cmd new OleDbCommand(); cmd.Connection conn; cmd.Transaction tx; cmd.CommandText INSERT INTO History(RecordTime, Temperature) VALUES(?, ?); foreach (var item in list) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue(p1, item.RecordTime); cmd.Parameters.AddWithValue(p2, item.Temperature); cmd.ExecuteNonQuery(); } tx.Commit(); } }注意两条重要细节BeginTransaction()是OleDbConnection的实例方法事务提交前表是锁定状态CommandText只设置一次循环中只更换参数值命令对象复用能明显减少 OLEDB 内部的语句分析开销。每条 1000 行左右提交一次是比较稳的节奏事务过大时回滚也会很慢。5.4 UI 刷新卡顿与查询结果集解耦热搜词里有「c# 循环数据采集和ui刷新卡顿」。很多卡顿不是数据库查询慢而是把 UI 操作直接挂在采集线程上。我的做法是查询线程只产出数据UI 通过BeginInvoke接收精简后的字符串或数据行不在查询线程里碰控件Task.Run(() { DataTable dt db.ExecuteQuery(SELECT TOP 200 * FROM History ORDER BY RecordTime DESC); string summary ${dt.Rows.Count} 条记录最后时间 {dt.Rows[0][RecordTime]}; textBox1.BeginInvoke(new Action(() { textBox1.Text summary; })); });BeginInvoke是异步投递调用线程不等待 UI 处理完成Task.Run里的数据库操作、字符串拼接都在后台线程完成UI 线程只做最终赋值。更讲究一点的方案是让ExecuteQuery返回精简后的实体集合而不是整个 DataTable 绑定控件——DataTable 的DefaultView绑定在重复刷新时会有明显的内存抖动。6. 验证数据库完整性压缩修复与日常自检技巧Access 的 ACCDB 文件在程序崩溃或磁盘写满时会产生逻辑碎片典型表现是文件体积膨胀、打开变慢、偶发「不可识别的数据库格式」。日常自检和修复有两条路调用系统自带的MSysCompactEngine类的压缩功能或者直接用OleDbConnection执行ALTER DATABASE类的 Jet SQL 语句前者更可控。下面的方法调用了 ADOX 内部的JetEngine接口public static int CompactDatabase(string srcFile, string destFile) { if (File.Exists(destFile)) File.Delete(destFile); string srcConn ProviderMicrosoft.ACE.OLEDB.12.0;Data Source srcFile; string destConn ProviderMicrosoft.ACE.OLEDB.12.0;Data Source destFile; ADOX.Catalog catalog new ADOX.Catalog(); catalog.Create(ProviderMicrosoft.Jet.OLEDB.4.0;Data Source destFile); catalog.ActiveConnection null; var engine new JetEngineClass(); engine.CompactDatabase(srcConn, destConn); return (int)new FileInfo(destFile).Length; }JetEngineClass的CompactDatabase实际上是把整个库读一遍再写一遍期间源库不能有打开连接否则会报锁冲突。压缩完成后用新文件替换旧文件时最好先做一次File.Copy备份再覆盖。这类操作适合放在程序启动时做一次「是否超过 10 天未压缩」的检查而不是每次退出都执行。另一个常用的验证技巧是执行SELECT Count(*)对比行数以及用以下语句直接查询 Access 系统表判断是否存在碎片化问题SELECT Name, Rows FROM MSysObjects WHERE Type 1 AND Name NOT LIKE Msys% ORDER BY NameMSysObjects是 Access 系统表Type1表示用户表Rows是估算行数而不是精确值但可以快速发现「程序认为有数据但表明显异常」的情况。利用这条语句做启动自检配合上面的压缩修复基本能把「Access 数据库用久了打不开」这类问题挡在生产环境之外。日常运维时建议用FileInfo.LastWriteTime做定期检查只要数据库文件写入时间超过 24 小时没有任何变化就要警觉是不是采集线程已经停了。本文还有配套的精品资源点击获取