做桌面小工具绕不开数据库,单机部署场景里“C# + Access”双击就能跟用户数据打交道,不需要装服务端。C#连接ACCESS做增删改查,是课程设计、上位机配置、企业内部小系统里出现频率特别高的一套组合。这篇文章就从零开始,把引用驱动、连接字符串、增删改查代码、常见报错全部过一遍,每一步都给出可以直接抄走的代码。
如果你正卡在“连接不上数据库”“找不到驱动程序”“更新不进去数据”这些坎上,这篇文章能帮你省掉很多翻文档的时间。
1. 动手之前的准备工作
1.1 搞清楚用哪个驱动
Access 数据库文件有两种后缀,老版本是 .mdb,新版本是 .accdb。C# 访问 Access,底层走的是 OLEDB 提供程序,常见的是Microsoft.ACE.OLEDB.12.0。这个驱动需要安装“Microsoft Access Database Engine”,如果没有 Office Access,也要单独装这个引擎。
要注意,ACE 引擎分 32 位和 64 位两个版本。如果你的程序编译成了 x86,就装 32 位驱动;编译成 x64 就装 64 位驱动。最容易翻车的是:机器上装的是 64 位 Office,驱动也是 64 位的,但 Visual Studio 默认生成的程序是 AnyCPU,调试时加载了 32 位进程,结果报错“未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0 提供程序”。
所以第一件事,把项目的生成目标平台改好。操作如下:
- 在解决方案资源管理器右键项目 → 属性
- 切到“生成”选项卡
- 把“平台目标”改成 x86 或 x64
- 或者用“配置管理器”新建一个 x86 平台
单机小工具推荐 x86,因为很多老机器的 Office 和 Access 引擎都是 32 位的,兼容性最好。如果你的程序一定要跑 64 位,就统一装 64 位驱动,别混用。
1.2 连接字符串怎么写
连接字符串是整段代码的命门。一个标准的 Access 连接字符串长这样:
string connStr = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=D:\Demo\student.accdb;";各部分的含义:
Provider:指定 OLEDB 提供程序,ACE 12.0 是 2007 以后版本的通用驱动。Data Source:Access 数据库文件的完整路径。Persist Security Info:默认为 False 就可以,不需要改成 True。- 如果数据库设置了密码,连接字符串追加
Jet OLEDB:Database Password=你的密码;。
注意Data Source前面加@符号,避免路径里的反斜杠被当成转义符。如果路径里有空格,也直接写在引号里,不用额外处理。
1.3 引用哪些命名空间
在代码文件顶部加这几行:
using System.Data; using System.Data.OleDb;所有 Access 操作都围绕OleDbConnection、OleDbCommand、OleDbDataAdapter、OleDbDataReader这几个类展开。它们都来自System.Data.OleDb,这是 .NET Framework 和 .NET Core/.NET 5+ 都内置支持的。
2. 数据库设计与代码结构思路
2.1 先建库建表,再写代码
很多新手一上来就写代码,数据库还没建,连接字符串里的路径随便填,然后报错“文件找不到”或者“不是有效路径”。靠谱顺序是:
- 用 Access 软件建好 .accdb 文件
- 在里面建表、建字段
- 填几条测试数据
- 再回到 Visual Studio 写连接和增删改查
如果你没有装 Access,也可以让程序在启动时自动建库建表。这样部署的时候不用额外依赖数据库文件,对用户更友好。建库的代码示例:
string dataDir = AppDomain.CurrentDomain.BaseDirectory; string dbPath = Path.Combine(dataDir, "student.accdb"); if (!File.Exists(dbPath)) { var catalog = new ADOX.Catalog(); catalog.Create("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + dbPath + ";"); MessageBox.Show("数据库已创建"); }这段代码里用到了 ADOX,需要添加 COM 引用Microsoft ADO Ext. 6.0 for DDL and Security,或者直接优先建议用 Access 手动建,简单不出错。程序里自动建表也是可以的:
using (OleDbConnection conn = new OleDbConnection(connStr)) { conn.Open(); string sql = @"CREATE TABLE Student ( ID AUTOINCREMENT PRIMARY KEY, Name VARCHAR(50), Age INT, Score DOUBLE, Birthday DATETIME)"; OleDbCommand cmd = new OleDbCommand(sql, conn); cmd.ExecuteNonQuery(); }这里把ID设成AUTOINCREMENT,就是 Access 里的“自动编号”,每次插入新记录会自动生成一个不重复的编号,适合做课程设计里的学号、工号场景。
2.2 字段类型映射别踩坑
Access 的数据类型和 C# 对应关系大致如下:
| Access 类型 | 存储内容 | C# 对应类型 |
|---|---|---|
| 文本 | 短字符串 | string |
| 备注 | 长文本 | string |
| 数字(长整型) | 整数 | int |
| 数字(单精度/双精度) | 小数 | float/double |
| 日期/时间 | 日期 | DateTime |
| 是/否 | 布尔 | bool |
| OLE 对象 | 二进制 | byte[] |
特别注意,Access 的“数字”类型默认是长整型,取值不能超过 int 的范围。插入小数时要提前把字段类型设为“双精度”或“小数”,否则数据会被静默截断。
2.3 为什么用参数化而不是拼接字符串
增删改查接用户输入时,最忌讳直接把值拼进 SQL 字符串。比如:
string sql = "INSERT INTO Student(Name, Age) VALUES('" + nameTextBox.Text + "', " + ageTextBox.Text + ")";这样写,用户输入一个' OR '1'='1就能把你的 SQL 搞乱,甚至删掉整张表的数据,这就是著名的 SQL 注入。Access 数据库同样有这个问题。
正确写法是用参数:
string sql = "INSERT INTO Student(Name, Age) VALUES(@Name, @Age)"; OleDbCommand cmd = new OleDbCommand(sql, conn); cmd.Parameters.AddWithValue("@Name", nameTextBox.Text); cmd.Parameters.AddWithValue("@Age", Convert.ToInt32(ageTextBox.Text));参数名用@开头,和 SQL Server 的写法一致,但底层传递方式不同。用参数化之后,引号、中文、特殊字符都不用担心,这是必须养成的习惯。
3. 增删改查完整代码实现
3.1 封装数据库连接辅助类
写一个小工具类,把公共逻辑抽出来,后面所有窗体都能复用。这是实际项目里最实用的做法。
public class AccessHelper { private string connStr; public AccessHelper(string dbPath) { connStr = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + dbPath + ";"; } public DataTable Query(string sql, params OleDbParameter[] parameters) { using (OleDbConnection conn = new OleDbConnection(connStr)) { conn.Open(); using (OleDbCommand cmd = new OleDbCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); using (OleDbDataAdapter adapter = new OleDbDataAdapter(cmd)) { DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } } } public int Execute(string sql, params OleDbParameter[] parameters) { using (OleDbConnection conn = new OleDbConnection(connStr)) { conn.Open(); using (OleDbCommand cmd = new OleDbCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } } }这里用using包裹连接和命令,可以确保用完自动释放,避免连接没关导致数据库文件被锁。文件锁是 Access 开发里最常见的坑,后面会单独讲。
3.2 添加数据 INSERT
添加数据的思路:连接数据库 → 写 INSERT 语句 → 添加参数 → 执行。
string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "student.accdb"); AccessHelper helper = new AccessHelper(dbPath); string sql = "INSERT INTO Student(Name, Age, Score, Birthday) VALUES(@Name, @Age, @Score, @Birthday)"; OleDbParameter[] ps = { new OleDbParameter("@Name", nameTextBox.Text), new OleDbParameter("@Age", Convert.ToInt32(ageTextBox.Text)), new OleDbParameter("@Score", Convert.ToDouble(scoreTextBox.Text)), new OleDbParameter("@Birthday", Convert.ToDateTime(birthdayPicker.Value)) }; int rows = helper.Execute(sql, ps); if (rows > 0) MessageBox.Show("添加成功");几个容易出问题的细节:
- 文本框里如果是空字符串,直接
Convert.ToInt32会抛异常,提交前先做输入校验。 - 日期值建议用
DateTimePicker,避免用户手动输入各种格式的日期,解析时容易出错。 - 插入语句不要写
ID字段,它是自动编号,交给数据库生成。
3.3 查询数据 SELECT 和显示
查询有两种常见方式。一种是只查一条记录或一个聚合结果,用ExecuteScalar;另一种是查多条记录显示在表格里,用DataTable配合DataGridView。
查单条记录示例:
string sql = "SELECT COUNT(*) FROM Student WHERE Age > @Age"; using (OleDbConnection conn = new OleDbConnection(connStr)) { conn.Open(); using (OleDbCommand cmd = new OleDbCommand(sql, conn)) { cmd.Parameters.AddWithValue("@Age", 18); int count = Convert.ToInt32(cmd.ExecuteScalar()); MessageBox.Show("成年学生数量:" + count); } }查列表并显示到DataGridView:
string sql = "SELECT ID, Name, Age, Score, Birthday FROM Student"; DataTable dt = helper.Query(sql); dataGridView1.DataSource = dt;这里有个细节:Access 支持SELECT *,但实际项目不建议写*。原因有两个,一是如果表结构变了,程序里取字段名会乱;二是字段顺序不可控,跟界面不一定对得上。尽量明确写出需要的字段。
如果要模糊查询,用LIKE:
string sql = "SELECT ID, Name, Age, Score, Birthday FROM Student WHERE Name LIKE @Keyword"; OleDbParameter[] ps = { new OleDbParameter("@Keyword", "%" + keywordTextBox.Text.Trim() + "%") }; DataTable dt = helper.Query(sql, ps);Access 的 LIKE 通配符是百分号,这点和 SQL Server 一样。
3.4 修改数据 UPDATE
修改的关键在于 WHERE 条件,必须准确锁到要更新的那条记录。一般用主键 ID 作为条件,不要用姓名。因为姓名可能重复,一改就是好几条记录。
string sql = "UPDATE Student SET Name=@Name, Age=@Age, Score=@Score, Birthday=@Birthday WHERE ID=@ID"; OleDbParameter[] ps = { new OleDbParameter("@Name", nameTextBox.Text), new OleDbParameter("@Age", Convert.ToInt32(ageTextBox.Text)), new OleDbParameter("@Score", Convert.ToDouble(scoreTextBox.Text)), new OleDbParameter("@Birthday", Convert.ToDateTime(birthdayPicker.Value)), new OleDbParameter("@ID", currentId) }; int rows = helper.Execute(sql, ps); if (rows > 0) MessageBox.Show("修改成功");currentId通常来自用户在DataGridView里选中行的 ID 值。取选中行 ID 的写法:
if (dataGridView1.CurrentRow != null) { int currentId = Convert.ToInt32(dataGridView1.CurrentRow.Cells["ID"].Value); }注意CurrentRow可能为 null,特别是表格刚加载完还没点击任何行的时候。取之前一定要判空。
3.5 删除数据 DELETE
删除按 ID 删,避免误删。
string sql = "DELETE FROM Student WHERE ID=@ID"; OleDbParameter[] ps = { new OleDbParameter("@ID", currentId) }; int rows = helper.Execute(sql, ps); if (rows > 0) MessageBox.Show("删除成功");删除操作最好加确认提示,防止手滑。真删了 Access 里没有回滚的概念,也没有 SQL Server 那种事务日志可以恢复,做好确认弹窗比什么都强。
DialogResult dr = MessageBox.Show("确定删除这条记录吗?", "删除确认", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (dr == DialogResult.Yes) { // 执行删除 }3.6 带事务的多步骤操作
有些业务要求“要么都成功,要么都失败”。比如同时删除一个学生和他的选课记录,删了学生没删选课表,数据就不完整了。这时候用事务:
using (OleDbConnection conn = new OleDbConnection(connStr)) { conn.Open(); using (OleDbTransaction tx = conn.BeginTransaction()) { try { string sql1 = "DELETE FROM Score WHERE StudentID=@StudentID"; OleDbCommand cmd1 = new OleDbCommand(sql1, conn, tx); cmd1.Parameters.AddWithValue("@StudentID", studentId); cmd1.ExecuteNonQuery(); string sql2 = "DELETE FROM Student WHERE ID=@ID"; OleDbCommand cmd2 = new OleDbCommand(sql2, conn, tx); cmd2.Parameters.AddWithValue("@ID", studentId); cmd2.ExecuteNonQuery(); tx.Commit(); } catch { tx.Rollback(); throw; } } }OleDbCommand构造函数里可以传事务对象,代表这条命令在事务中执行。中途任何一个步骤异常,回滚到事务开始前。
4. 学生信息管理小工具:完整流程跑一遍
4.1 界面设计
为了让上面的代码串起来,我做一个最简单的学生信息管理窗体。界面放这些控件:
- 三个文本框:姓名、年龄、分数
- 一个日期选择器:生日
- 四个按钮:添加、修改、删除、查询
- 一个关键字文本框 + 一个查找按钮
- 一个 DataGridView 用于显示数据
这个结构是入门增删改查最典型的排布。不用做多复杂,逻辑走通以后再往后加功能。
4.2 窗体加载时刷新表格
private void Form1_Load(object sender, EventArgs e) { LoadData(); } private void LoadData() { string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "student.accdb"); AccessHelper helper = new AccessHelper(dbPath); string sql = "SELECT ID, Name, Age, Score, Birthday FROM Student"; DataTable dt = helper.Query(sql); dataGridView1.DataSource = dt; // 隐藏自动编号列,避免用户修改主键 if (dataGridView1.Columns.Contains("ID")) dataGridView1.Columns["ID"].Visible = false; }这里把 ID 列隐藏了,因为它是自动编号,不应该让用户看到或修改。但代码里取值仍然需要它。
4.3 新增记录
private void btnAdd_Click(object sender, EventArgs e) { if (!ValidateInput()) return; string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "student.accdb"); AccessHelper helper = new AccessHelper(dbPath); string sql = "INSERT INTO Student(Name, Age, Score, Birthday) VALUES(@Name, @Age, @Score, @Birthday)"; OleDbParameter[] ps = { new OleDbParameter("@Name", nameTextBox.Text.Trim()), new OleDbParameter("@Age", Convert.ToInt32(ageTextBox.Text.Trim())), new OleDbParameter("@Score", Convert.ToDouble(scoreTextBox.Text.Trim())), new OleDbParameter("@Birthday", birthdayPicker.Value) }; int rows = helper.Execute(sql, ps); if (rows > 0) { MessageBox.Show("添加成功"); LoadData(); ClearInput(); } }ValidateInput可以这样写:
private bool ValidateInput() { if (string.IsNullOrWhiteSpace(nameTextBox.Text)) { MessageBox.Show("姓名不能为空"); return false; } if (!int.TryParse(ageTextBox.Text, out _)) { MessageBox.Show("年龄必须是整数"); return false; } if (!double.TryParse(scoreTextBox.Text, out _)) { MessageBox.Show("分数必须是数字"); return false; } return true; }先校验再入库,比提交数据库抛异常再提示要友好得多。
4.4 双击表格填充修改框
用户点击表格某一行,把该行的数据显示到上方的文本框里,这样修改时可以直接看到当前值。
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e) { if (dataGridView1.CurrentRow == null) return; nameTextBox.Text = dataGridView1.CurrentRow.Cells["Name"].Value.ToString(); ageTextBox.Text = dataGridView1.CurrentRow.Cells["Age"].Value.ToString(); scoreTextBox.Text = dataGridView1.CurrentRow.Cells["Score"].Value.ToString(); birthdayPicker.Value = Convert.ToDateTime(dataGridView1.CurrentRow.Cells["Birthday"].Value); }这里有个坑:如果数据库里的生日字段是空的,Convert.ToDateTime就会炸。所以建表时给生日字段设置默认值,或者读取时判空:
if (dataGridView1.CurrentRow.Cells["Birthday"].Value != DBNull.Value) birthdayPicker.Value = Convert.ToDateTime(dataGridView1.CurrentRow.Cells["Birthday"].Value);4.5 按条件模糊查询
private void btnSearch_Click(object sender, EventArgs e) { string keyword = keywordTextBox.Text.Trim(); string dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "student.accdb"); AccessHelper helper = new AccessHelper(dbPath); string sql = "SELECT ID, Name, Age, Score, Birthday FROM Student WHERE Name LIKE @Keyword"; OleDbParameter[] ps = { new OleDbParameter("@Keyword", "%" + keyword + "%") }; DataTable dt = helper.Query(sql, ps); dataGridView1.DataSource = dt; }如果关键字为空,就当作查全部。这个逻辑可以优化一下:
string sql = "SELECT ID, Name, Age, Score, Birthday FROM Student"; if (!string.IsNullOrWhiteSpace(keyword)) { sql += " WHERE Name LIKE @Keyword"; // 添加参数并执行 }4.6 修改与删除联动
修改、删除按钮的代码前面已经给了,核心点是记住当前选中的行 ID。可以定义一个窗体级变量:
private int currentId = -1;在CellClick事件里赋值:
currentId = Convert.ToInt32(dataGridView1.CurrentRow.Cells["ID"].Value);修改按钮里直接使用这个变量。如果currentId == -1,说明用户没有选中任何记录,直接提示。
这个完整流程跑通之后,增删改查就不只是会写四个方法,而是一个连得起来的小系统。
5. 高频报错与排查方案
5.1 未在本地计算机上注册 ACE.OLEDB.12.0 提供程序
这个是最常见的。原因只有一个:程序和驱动位数对不上。
排查步骤:
- 在代码里第一行加
Environment.Is64BitProcess,看程序跑的是 32 位还是 64 位 - 检查安装的 ACE 驱动位数
- 项目平台目标改成一致
如果项目是 AnyCPU,在 x64 系统上默认以 64 位进程加载,如果是 32 位驱动那就报错。最省事的办法是固定 x86。
5.2 操作必须使用可更新查询
这个报错通常发生在执行UPDATE或DELETE时。常见原因有两个:
- 查询语句里做了 JOIN,连接了多张表,Access 无法确定要更新哪张表
- 数据库文件被设置为只读
排查方式:
- 更新语句只操作一张表,JOIN 的需求改成先查一张表,再用结果更新另一张
- 右键 .accdb 文件,检查“只读”属性是否被勾选
- 把文件复制到本地再试,有些权限策略会导致 Access 无法写文件
5.3 数据库文件被锁,出现“文件正由另一进程使用”
Access 是基于文件的数据库,没有独立的数据库服务。程序打开连接后,如果连接没有关闭,文件会一直处于锁定状态,其他程序或本程序第二次打开就会冲突。
排查方式:
- 检查是否正确释放连接,统一用
using。 - 检查窗体是否留下未结束的
OleDbConnection对象。 - 如果程序调试时崩溃,旧进程没退,打开任务管理器结束残留进程。
- 不要把 Access 数据库文件放在某些云同步目录里,同步工具会频繁访问文件,容易造成无法写入。
这类文件锁问题在 SQL Server 里几乎不存在,这是 Access 自带的天生限制。如果你发现你的工具经常出现文件锁,说明数据量或并发量已经超出 Access 合适范围,需要考虑换数据库了。
5.4 找不到可安装的 ISAM
这个报错一般是连接字符串里的Provider写错,或者数据库扩展名和内容不匹配。比如 .mdb 文件用了 ACE 驱动,老版本 .mdb 应该用Microsoft.Jet.OLEDB.4.0;如果用了 ACE 驱动访问,记得检查扩展名。
如果是 .mdb 文件,连接字符串写成:
string connStr = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\Demo\student.mdb;";Jet 驱动在 64 位系统上支持比较差,老项目尽量迁移到 .accdb 格式。
5.5 错误 1045 Access denied
注意这个错误码是 MySQL 的,不是 Access 的。如果你在跑连接字符串时看到它,说明连接字符串或者代码里实际连的是 MySQL,而不是 Access 文件。把Provider和Data Source再检查一遍。
5.6 添加数据后 ID 不连续
Access 自动编号默认是按顺序递增的,但你删除记录再插入,它不会回填之前删掉的编号。比如删掉 ID=3 的记录,下次插入可能变成 ID=4。
这不是 bug,是设计如此。课程设计里如果要求学号连续,就不要用自动编号,改为手动生成编号,程序里查询最大值再加一:
string sql = "SELECT MAX(ID) FROM Student"; object result = cmd.ExecuteScalar(); int newId = 1; if (result != DBNull.Value && result != null) newId = Convert.ToInt32(result) + 1;这个方案在单机使用没问题,并发插入时会有问题,但 Access 本身就不适合高并发,个人工具和课程设计够用了。
5.7 调试时进程退出异常 0xc0000005
这个不是 Access 特有问题,而是内存访问冲突。C# 程序出现这类异常,通常和调用本机 DLL 有关。OLEDB 就是 COM 组件,接近本机层面,最容易踩。
常见的触发场景是:连接没释放、驱动版本混乱、同一进程里多次创建和销毁连接。建议先统一驱动位数,再整理代码里所有连接的使用,确保没有跨线程共用一个连接对象。
5.8 高频问题速查表
| 报错信息 | 可能原因 | 解决方案 |
|---|---|---|
| 未在本地计算机上注册提供程序 | 驱动位数不一致 | 先确认 x86/x64,装对应驱动 |
| 操作必须使用可更新查询 | 多表 JOIN 或文件只读 | 改为单表更新,检查文件属性 |
| 找不到可安装的 ISAM | Provider 错误或文件格式不对 | 检查连接字符串和数据库扩展名 |
| 文件正由另一进程使用 | 连接未释放 | 用 using,清理残留进程 |
| 无法更新数据库,数据库或对象为只读 | 权限不足 | 把文件复制到本地,关闭只读 |
| 不能锁定文件 | 网络路径或同步目录 | 本地磁盘目录,排除云同步目录 |
6. 读取和显示单条记录的字段数据
有时候不想用 DataGridView,只查一条记录并填入文本框,这也是上位机和工具软件里常见的需求。比如用扫码枪扫一下学号,自动带出学生信息。
string sql = "SELECT Name, Age, Score, Birthday FROM Student WHERE ID=@ID"; using (OleDbConnection conn = new OleDbConnection(connStr)) { conn.Open(); using (OleDbCommand cmd = new OleDbCommand(sql, conn)) { cmd.Parameters.AddWithValue("@ID", idTextBox.Text.Trim()); using (OleDbDataReader reader = cmd.ExecuteReader()) { if (reader.Read()) { nameTextBox.Text = reader["Name"].ToString(); ageTextBox.Text = reader["Age"].ToString(); scoreTextBox.Text = reader["Score"].ToString(); birthdayPicker.Value = Convert.ToDateTime(reader["Birthday"]); } else { MessageBox.Show("没有找到这条记录"); } } } }ExecuteReader返回一个只读的数据流,适合读少量记录或者单条记录。和DataTable相比,DataReader更轻量,但只能向前读,不能用下标随机访问多行。两者使用场景别搞混。
7. 扩展:用 Dapper 封装 Access 操作
如果项目稍微大一点,手写OleDbCommand太啰嗦,可以考虑 Dapper。Dapper 是轻量级 ORM,支持 MySQL、SQL Server、SQLite,也支持 Access。
需要先通过 NuGet 安装 Dapper。连接对象可以保持原来的OleDbConnection,Dapper 的扩展方法会自动处理命令和参数:
using Dapper; public class Student { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } public double Score { get; set; } public DateTime Birthday { get; set; } } public List<Student> GetStudents(string keyword) { using (OleDbConnection conn = new OleDbConnection(connStr)) { string sql = "SELECT ID, Name, Age, Score, Birthday FROM Student WHERE Name LIKE @Keyword"; return conn.Query<Student>(sql, new { Keyword = "%" + keyword + "%" }).ToList(); } }Dapper 会根据查询结果的字段名自动映射到Student类的属性。增删改查用Execute方法:
string sql = "INSERT INTO Student(Name, Age, Score, Birthday) VALUES(@Name, @Age, @Score, @Birthday)"; conn.Execute(sql, new { student.Name, student.Age, student.Score, student.Birthday });注意 Dapper 在 Access 下有兼容性问题,比如 Access 的AUTOINCREMENT类型插入时不能包含 ID 字段,Dapper 的参数化写法在 Name 边上带@是可以的,但个别 Access 驱动版本对命名参数支持不好。建议先在测试机上跑通,再进入开发。
说到底,Dapper 是减少样板代码,连接和驱动底层还是 OLEDB,之前的坑一样会踩。
8. 一些经验之谈
单机版的 Access 增删改查,代码量不大,但环境坑特别多。驱动程序位数、数据库文件存放路径、连接释放、参数化查询,这四个地方做好了,整个项目就稳定了。
我自己调试这类程序时有个习惯:把connStr放到配置文件里,而不是硬编码在代码里。这样换一台电脑,数据库路径变了,只需要改一下App.config里的连接字符串,不用重新编译程序。
<connectionStrings> <add name="AccessDb" connectionString="Provider=Microsoft.ACE.OLEDB.12.0;Data Source=|DataDirectory|student.accdb;"/> </connectionStrings>代码里读取:
string connStr = ConfigurationManager.ConnectionStrings["AccessDb"].ConnectionString;|DataDirectory|是一个占位符,默认指向程序运行目录,部署时把数据库文件放到 exe 同目录就能跑。
还有一个细节:发布程序时记得把 Access 数据库文件包含进去。项目里添加数据库文件后,右键属性,把“复制到输出目录”改成“如果较新则复制”。这样每次编译,新的数据库文件会自动复制到bin\Debug或bin\Release目录,程序就能直接用相对路径找到文件。
最后说一句,Access 适合数据量不大、并发不高的桌面工具,一旦遇到多用户同时写文件,它的文件锁机制会很痛苦。如果你预感到项目以后会往服务器方向走,一开始就换 SQLite 或 SQL Server Express 可能更省事。但作为学习数据库开发和增删改查底层逻辑的入门项目,C# 连接 Access 这套流程仍然是一个绕不过去的经典组合。把这里面的连接管理、参数化、事务处理搞明白,以后换任何数据库都一通百通。