1. 从零到一:为什么选择MySql.Data.dll这条路
先聊点实际的。刚接触C#连接MySQL的人,第一反应基本都是去网上搜“C# MySQL连接”,搜出来一堆答案,有说用EF Core的,有说用Dapper的,有说直接用ADO.NET的,还有让你装一堆插件配一堆依赖的。新手一看就懵了,老手一看就知道这些方案各有各的适用场景,但如果你只是想快速把一个基于C#的桌面程序、上位机或者小工具对接上MySQL数据库,MySql.Data.dll这套方案是最直接的——它就是一个纯托管的ADO.NET驱动,引用dll,写连接串,调方法,完事。
这套方案适合谁?适合三类人:一是刚开始学C#和MySQL怎么配合的新手,二是做WinForm/WPF桌面应用或上位机开发、需要本地数据落地的工程师,三是写内部工具不想引入太重型ORM的人。它解决的痛点很明确:不折腾、不绕路、几行代码就能把数据库读写跑通。
我个人的一个体会是,很多教程一上来就教EF Core,虽然EF Core确实是企业级开发的主流,但如果你只是做一个需要读写MySQL的小工具,引入EF Core那套DbContext、Migration、导航属性的概念反而把简单问题复杂化了。MySql.Data.dll就是最小可用方案,先把这条链路走通,后面如果项目复杂度上来了再换ORM,也完全来得及。
提示:MySql.Data.dll是MySQL官方提供的.NET连接器(Connector/NET)里的核心程序集,不是第三方野路子库,可以放心用。它支持.NET Framework 4.0以上和.NET Core/.NET 5+,覆盖面非常广。
2. 环境准备:MySQL安装、dll获取与项目配置
2.1 MySQL服务端的准备
既然要用C#连MySQL,那前提是你得有一个能连的MySQL实例。如果你还没装MySQL,先去官网下载安装包。注意一点,官网有个MySQL Installer,也有各个版本独立的ZIP包,我建议下载MySQL Installer,因为它会自动帮你处理依赖组件,比如Visual C++运行库。安装的时候选择Server only就够了,不用装那些Workbench、Excel插件之类的附带品,省得以后看着一堆没用的组件心烦。
装完之后有一个很容易踩的坑:root用户的认证方式。MySQL 8.0默认的认证插件是caching_sha2_password,而Connector/NET的新版本是支持这个认证的,所以正常情况下没什么问题。但如果你用的是老版本的MySql.Data.dll(比如6.x、8.0.9之前的版本),去连MySQL 8.0就会报“Authentication method 'caching_sha2_password' not supported”。解决办法有两个:一个是把MySql.Data.dll升级到8.0.11以上,另一个是把MySQL的root用户改回mysql_native_password插件。我建议直接升级dll,因为改认证方式总感觉是在给老代码打工。
安装完成后,建议顺手把MySQL服务设置成Windows服务自启动,因为上位机程序可能跑在工控机上,如果机器重启了MySQL没起来,程序就废了。这个在Windows服务管理器里找到“MySQL80”服务,把启动类型改成“自动”就行。
2.2 获取MySql.Data.dll的两种方式
获取dll的方式有两种,一种是通过NuGet包管理器,一种是直接官网下载ZIP包。
NuGet方式最省事。在Visual Studio里右键项目,选择“管理NuGet程序包”,搜索“MySql.Data”,找到MySQL官方那个包,直接安装。或者用包管理控制台:
Install-Package MySql.Data这种方式会自动把dll引用到项目里,省去手动找dll的麻烦。唯一需要注意的是,NuGet装的是NuGet包缓存路径下的dll,程序发布的时候要确保这个dll跟着一起输出到输出目录。
官网下载方式,去MySQL官网的Connector/NET下载页面,选择.NET & Mono平台,下载ZIP压缩包。解压之后在里面的lib文件夹里能找到不同框架版本的MySql.Data.dll,比如net452、netstandard2.0、net6.0等。选哪个版本,取决于你项目的目标框架。
这里有个非常关键的点:如果你用的是.NET Framework 4.7.2或者4.8,选net452或者netstandard2.0的dll都行;如果你的项目是.NET 6、.NET 8这种现代框架,选net6.0或更高版本的dll。选错版本会导致程序运行时报“无法加载程序集”或者类型不兼容的错误。
手动添加引用:在解决方案资源管理器里右键“引用”,选“添加引用”,然后浏览到dll所在路径,选中MySql.Data.dll,确定即可。
注意:如果是.NET Core/.NET 5+项目,不能直接往项目里塞dll就完事。更推荐通过NuGet安装,因为SDK风格的项目对非SDK风格的旧式引用处理得不是很好,容易出现依赖缺失问题。
2.3 项目目标框架与dll版本匹配
这个坑我见得太多了。你的项目目标框架是net48,结果引用了net6.0的MySql.Data.dll,后果就是代码编译的时候一切正常,运行的时候直接抛“System.IO.FileNotFoundException”或者“BadImageFormatException”。原因很简单——net6.0的dll依赖更高版本的.NET运行时,而net48项目里根本没有那个运行时。
所以选dll版本前,先确认自己项目的目标框架。在项目属性 -> 应用程序 -> 目标框架里能看。大致对应关系如下:
| 项目目标框架 | 应选择dll版本 | 说明 |
|---|---|---|
| .NET Framework 4.0 | net40版本 | 较老,建议升级项目 |
| .NET Framework 4.5 - 4.8 | net452 | 兼容性不错 |
| .NET Core 2.0+ | netstandard2.0 | 通用性强 |
| .NET 5 / 6 / 7 | net6.0 | 性能更好 |
| .NET 8+ | net8.0 | 新特性支持 |
如果你的项目是.NET 6以上的,我强烈建议用NuGet方式安装。因为MySql.Data从8.0.28版本之后,对.NET 6+的优化做得越来越深,NuGet会帮你自动处理各种依赖,比手动放dll省心多了。
3. 核心原理与连接串:搞懂这几行就能连上
3.1 MySQL连接的本质——连接串到连接池
C#连接MySQL的本质,就是通过一个连接字符串,让MySql.Data.dll里的连接管理器去跟MySQL服务端建立一个TCP连接,然后再通过这个连接执行SQL命令。
连接字符串长这样:
Server=127.0.0.1;Port=3306;Database=mydb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;这里面的每一项都很关键:
- Server:MySQL服务器地址。本地开发用127.0.0.1或localhost,远程服务器填IP或域名。
- Port:MySQL默认端口3306,如果你改过MySQL的端口配置,这里要对应改。
- Database:要连接的数据库名。
- Uid/Pwd:用户名和密码。
- CharSet:字符集。强烈建议用utf8mb4,因为utf8mb4是MySQL 8.0的默认字符集,能存emoji和四字节字符。用utf8的话,遇到特殊字符会报“Incorrect string value”错误。
- SslMode:SSL模式。本地开发或内网环境建议设成None,否则MySQL 8.0默认要求SSL连接,可能因为证书问题连不上。
还有两个高级参数,新手阶段可以不碰,但你应该知道它们存在。一个是Connection Timeout,默认15秒,设置连接超时时间,比如写Connection Timeout=3就是3秒超时。另一个是Pooling,默认true,即连接池。连接池的意思是你关闭一个连接时,连接并没有真正断开,而是被缓存起来,下次再要连接时直接复用。这在频繁操作数据库的场景下能显著提升性能。
3.2 连接数据库的最小可用代码
一个最基础的连接测试代码长这样:
using MySql.Data.MySqlClient; string connStr = "Server=127.0.0.1;Port=3306;Database=mydb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;"; using (MySqlConnection conn = new MySqlConnection(connStr)) { conn.Open(); Console.WriteLine("连接成功!"); }这段代码有几件事值得说明:
using语句会自动释放连接资源,这是C#里最标准的写法。即使后面代码抛异常,连接也会被正确释放,不会泄露。
conn.Open()是真正建立连接的动作,如果连接失败,异常会在这里被抛出。
这里还有一个非常重要的认知——连接不是长时间占着的。很多新手以为打开连接之后应该一直保持,直到程序退出才关闭,这是个严重的误区。正确的做法是:每次操作数据库时,短连接、快打开、快关闭。连接池会帮你兜底,频繁开关连接性能并不差,反而能避免连接长时间闲置导致MySQL服务端断开连接的问题。
3.3 连接串的常见坑
连接串写错的概率其实不高,但有几个小问题特别容易忽略。
第一,密码里有特殊字符的情况。如果你的MySQL密码里包含了分号、井号这类特殊符号,直接写在连接串里会导致解析错误。解决方法是URL编码,比如密码里的#要写成%23,分号要写成%3B。注意是连接串层面的URL编码,不是普通字符串转义。
第二,Database指定了一个不存在的库。新手会想当然认为不存在的库也能连上,实际会报“Unknown database 'xxx'”。如果你只是想测试连接通不通,可以先把Database参数去掉,连上MySQL后看有哪些库,再决定用哪个。
第三,端口写错。很多人装MySQL的时候在安装向导里改了端口,结果自己忘了,连接时还在用3306,就会一直报超时。如果连不上,先用命令行工具试一下mysql -u root -p -P 端口号,确认端口能不能通。
第四,MySQL 8.0默认的caching_sha2_password认证在低版本的MySql.Data.dll下连不上。这个问题上面说过,建议升级dll版本,而不是去改MySQL认证。
4. 核心CRUD实操:增删改查全流程
4.1 查询:读取单条记录与多条记录
先写最基础的查询功能。假设数据库里有一张用户表users,字段有id、username、age、email。
读取单条记录,核心就是ExecuteScalar或者ExecuteReader:
using MySql.Data.MySqlClient; static string GetUserName(int userId) { string result = string.Empty; string connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;"; using (MySqlConnection conn = new MySqlConnection(connStr)) { conn.Open(); string sql = "SELECT username FROM users WHERE id = @id"; using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@id", userId); object obj = cmd.ExecuteScalar(); if (obj != null) { result = obj.ToString(); } } } return result; }我用了参数化查询,而不是把userId直接拼进SQL字符串。这不是可选项,而是必须项。原因有两个:一是防止SQL注入,这是安全底线;二是性能更好,MySQL会缓存参数化查询的执行计划,重复查询不需要重新解析SQL。如果你是从其他语言转过来的,也要把“拼接SQL字符串”这个习惯彻底改掉。
如果查询结果是多条记录,用ExecuteReader:
static List<UserInfo> GetUsersByAge(int minAge) { List<UserInfo> list = new List<UserInfo>(); string connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;"; using (MySqlConnection conn = new MySqlConnection(connStr)) { conn.Open(); string sql = "SELECT id, username, age, email FROM users WHERE age >= @age"; using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@age", minAge); using (MySqlDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) { UserInfo info = new UserInfo(); info.Id = reader.GetInt32("id"); info.Username = reader.GetString("username"); info.Age = reader.GetInt32("age"); info.Email = reader.IsDBNull(reader.GetOrdinal("email")) ? "" : reader.GetString("email"); list.Add(info); } } } } return list; }reader.Read()每调用一次,就前进一行。reader.GetInt32("id")是按列名取值,也可以按索引取值reader.GetInt32(0)。列名校验更安全,列索引性能稍微好一点点,但可读性差。
特别注意email字段可能为NULL的情况,直接GetString会抛异常,所以要先判断IsDBNull。这个细节在真实项目中太常见了,我见过太多人栽在这里。
4.2 插入:拿自增ID的正确姿势
插入数据用ExecuteNonQuery,它返回受影响的行数。如果表有自增主键,需要在执行插入后获取新生成的ID,这时候用SELECT LAST_INSERT_ID():
static int InsertUser(string username, int age, string email) { string connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;"; using (MySqlConnection conn = new MySqlConnection(connStr)) { conn.Open(); string sql = @" INSERT INTO users (username, age, email) VALUES (@username, @age, @email); SELECT LAST_INSERT_ID();"; using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@username", username); cmd.Parameters.AddWithValue("@age", age); cmd.Parameters.AddWithValue("@email", email); object result = cmd.ExecuteScalar(); if (result != null && int.TryParse(result.ToString(), out int newId)) { return newId; } } } return -1; }这里有个SQL语句的技巧:两条SQL用分号隔开,连在一个Command里执行。第二条SELECT LAST_INSERT_ID()能拿到上一条INSERT语句生成的ID。
再补充一个日常高频的操作——批量插入。如果一次要插入几千条数据,一条条INSERT执行会很慢。那种情况下应该用MySqlBulkLoader或者拼一个巨大的Multi-Row INSERT。Multi-Row INSERT的值部分用VALUES (..),(..),(..)这种形式,一条SQL插入多条数据,性能能提升几倍。不过要注意SQL长度限制,一般单条SQL建议不要超过几百KB
4.3 更新与删除:别忘了事务保护
更新和删除本质上是同一种操作,都是非查询语句:
static bool UpdateUserEmail(int userId, string email) { string connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;"; using (MySqlConnection conn = new MySqlConnection(connStr)) { conn.Open(); string sql = "UPDATE users SET email = @email WHERE id = @id"; using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@email", email); cmd.Parameters.AddWithValue("@id", userId); int affected = cmd.ExecuteNonQuery(); return affected > 0; } } }删除类似,把SQL换成DELETE FROM users WHERE id = @id即可。ExecuteNonQuery返回受影响行数,如果返回0说明没有匹配的条件,通常意味着数据不存在。
但是,删除和更新有一个更大的话题值得展开——事务。比如你要做“用户转账”这种操作,A扣钱、B加钱,两步必须同时成功或者同时失败,不能只成功一步。这时候就需要事务:
using (MySqlConnection conn = new MySqlConnection(connStr)) { conn.Open(); using (MySqlTransaction transaction = conn.BeginTransaction()) { try { string sql1 = "UPDATE accounts SET balance = balance - @amount WHERE id = @fromId"; string sql2 = "UPDATE accounts SET balance = balance + @amount WHERE id = @toId"; using (MySqlCommand cmd = new MySqlCommand(sql1, conn, transaction)) { cmd.Parameters.AddWithValue("@amount", amount); cmd.Parameters.AddWithValue("@fromId", fromId); cmd.ExecuteNonQuery(); } using (MySqlCommand cmd = new MySqlCommand(sql2, conn, transaction)) { cmd.Parameters.AddWithValue("@amount", amount); cmd.Parameters.AddWithValue("@toId", toId); cmd.ExecuteNonQuery(); } transaction.Commit(); } catch { transaction.Rollback(); throw; } } }BeginTransaction()之后,Command必须绑定这个事务对象,即构造时传入conn, transaction。全部成功就Commit,任何一条失败就Rollback回滚到事务开始之前的状态。这是保证数据一致性的底线,在涉及金额、库存、订单状态的场景下不用不行。
4.4 显示一条记录字段数据的完整示例
基于前面的内容,结合最近网上很多人搜“C#显示查找一条记录字段数据”,我把这个常见场景写完整。比如你在WinForm里输入一个用户ID,点按钮,页面显示这个用户的全部字段:
private void btnQuery_Click(object sender, EventArgs e) { if (!int.TryParse(txtUserId.Text.Trim(), out int userId)) { MessageBox.Show("请输入合法的用户ID"); return; } string connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;"; using (MySqlConnection conn = new MySqlConnection(connStr)) { try { conn.Open(); string sql = "SELECT id, username, age, email FROM users WHERE id = @id"; using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@id", userId); using (MySqlDataReader reader = cmd.ExecuteReader()) { if (reader.Read()) { lblId.Text = reader.GetInt32("id").ToString(); lblUserName.Text = reader.GetString("username"); lblAge.Text = reader.GetInt32("age").ToString(); lblEmail.Text = reader.IsDBNull(reader.GetOrdinal("email")) ? "无" : reader.GetString("email"); } else { MessageBox.Show("未找到该用户"); } } } } catch (Exception ex) { MessageBox.Show("查询失败:" + ex.Message); } } }这里的关键点是把查询操作放进了try-catch里。连接数据库这种涉及外部服务的操作,必须做异常处理。网络断开、MySQL服务没启动、SQL写错、权限不足,哪个环节都可能炸,不接异常就直接把错误抛给用户界面,体验非常糟糕。
5. 常见问题与排查技巧实录
5.1 连不上MySQL:超时、拒绝访问、认证失败三连问
连不上MySQL是出现频率最高的问题,通常细分为三种情况:
情况一:连接超时
报错信息大概是“Unable to connect to any of the specified MySQL hosts”或者“Connection Timeout”。
排查顺序是:第一步,确认MySQL服务有没有启动,Windows服务里找MySQL80,没启动就启动它;第二步,确认端口通不通,命令行里telnet 127.0.0.1 3306,如果不通,检查MySQL配置文件my.ini里的端口和绑定地址;第三步,确认服务器防火墙有没有放行3306端口。
如果是远程服务器,还有一个特别隐蔽的坑:MySQL默认绑定在127.0.0.1上,外部IP根本连不进来。需要在my.ini里把bind-address改成0.0.0.0,并重启MySQL服务。
情况二:Access denied
报错“Access denied for user 'root'@'localhost' (using password: YES)”。这就是密码不对或者用户名不对。注意,MySQL的用户是“用户名+主机名”联合定义的,root@localhost和root@%是两个不同的用户。如果你创建了root@'192.168.1.100'这个用户,那只有那台机器能连接,其他机器即使密码正确也会被拒绝。
情况三:认证插件问题
报错“Authentication method 'caching_sha2_password' not supported”。前面已经说过,升级MySql.Data.dll到8.0.11+版本即可。
5.2 中文乱码问题
乱码的核心是字符集不匹配。MySQL服务端、连接串、数据表三个地方的字符集必须保持一致,推荐统一用utf8mb4。
排查方法:先查MySQL服务端默认字符集,执行SHOW VARIABLES LIKE 'character_set_server';,如果不是utf8mb4,修改my.ini里的character-set-server=utf8mb4。连接串里的CharSet=utf8mb4必须加上。建表的时候也指定字符集:
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, age INT, email VARCHAR(100) ) DEFAULT CHARSET=utf8mb4;如果修改了服务端字符集,已经存在的表还需要单独修改表字符集:
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4;5.3 连接未关闭导致资源泄露
连接池虽然会自动管理连接,但如果你使用连接后没有正确释放,连接池里的连接会越积越多,最终占满连接池导致“Too many connections”错误。
要避免这种情况,核心就是养成用using包裹连接对象的习惯。using语句在代码块结束时自动调用Dispose()方法,连接会被释放回连接池。
一个我见过很多次的错误写法是:
MySqlConnection conn = new MySqlConnection(connStr); conn.Open(); // 各种操作 // 忘记关闭连接这种写法在连接池模式下不一定马上报错,但长时间运行后必然出问题。所有连接对象的创建,全部放进using里,没有例外。
5.4 SQL注入风险与参数化查询
直接拼接字符串写SQL是新手最容易犯、危害最大的错误。比如:
string sql = "SELECT * FROM users WHERE username = '" + username + "'";如果username输入的是' OR '1'='1,那SQL就变成了SELECT * FROM users WHERE username = '' OR '1'='1',查询所有用户,数据全泄露了。更严重的还能删库。
杜绝这个问题的唯一正确方式就是参数化查询,前面所有示例代码都用了@参数名。MySqlCommand的Parameters集合会自动做转义处理,传入的参数值永不参与SQL解析,从根源上杜绝注入。
5.5 MySql.Data.dll版本冲突
有时候程序里引用了多个版本的MySql.Data.dll,会报“未能加载文件或程序集”的错。这种情况多发生在项目引用了第三方库,而第三方库内部也依赖了MySql.Data的情况下。
解决方法是打开NuGet包管理器,在“已安装”里看是否有重复引用,删除不需要的版本,或者通过boundRedirect强制统一版本。新式项目通常在.csproj文件里能看到类似:
<PackageReference Include="MySql.Data" Version="8.0.33" />保证所有引用都在一个版本上即可。
6. 性能优化与进阶玩法
6.1 连接池参数调优
连接池默认开启,但有些参数值得调整。核心参数有三个:
- Pooling:是否启用连接池,默认true,不建议关。
- Minimum Pool Size:最小连接数,默认0。如果你的程序会频繁访问数据库,建议设为5~10,这样程序启动时就会预创建一批连接,减少首次访问的等待时间。
- Maximum Pool Size:最大连接数,默认100。如果你的程序并发访问量很大,这个值不够用的话,要调大。但要先确认MySQL服务端的
max_connections是多少,连接池上限不能超过服务端上限,否则连接建立了也会被服务端拒绝。
连接串写法:
Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50;从.NET 6.0开始,使用MySqlDataSource可以避免在每次创建连接时配置连接串。这种方式推荐现代.NET项目采用:
var builder = new MySqlDataSourceBuilder("Server=127.0.0.1;..."); builder.UseMinimumPoolSize(5); builder.UseMaximumPoolSize(50); await using var dataSource = builder.Build(); await using var conn = await dataSource.OpenConnectionAsync();这比每次new MySqlConnection更高效,因为它把连接池的配置集中管理了。
6.2 批量操作的性能优化
逐条执行SQL是性能杀手。假设你要插入1万条数据,用循环一条条INSERT,可能需要几十秒甚至更久。但换成批量方式,能降到1秒以内。
批量插入的推荐做法是使用MySqlBulkLoader:
using MySql.Data.MySqlClient; var loader = new MySqlBulkLoader(conn) { TableName = "users", FieldTerminator = ",", LineTerminator = "\n", FileName = "data.csv" }; int count = loader.Load();这个类本质上是调用MySQL的LOAD DATA LOCAL INFILE语句,速度和普通的INSERT完全不在一个级别。它适合从文件批量导入数据的场景。
另一个做法是拼Multi-Row INSERT,但要注意SQL单条语句的字节限制,以及参数个数限制。MySQL的参数个数上限默认是65535个。如果你想一次插入1万条数据,每条数据3个字段,那参数就是3万个,在限制以内,但再大就要拆成多批了。
6.3 异步操作:别把UI线程堵死
在WinForm或者WPF程序里,如果在UI按钮事件里直接执行数据库操作,界面会卡死。因为数据库操作是耗时的IO操作,而UI线程被阻塞了。
解决办法是用异步方法。MySql.Data从8.0版本开始支持完整的异步API:
private async void btnQuery_Click(object sender, EventArgs e) { string connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;"; await using (MySqlConnection conn = new MySqlConnection(connStr)) { await conn.OpenAsync(); string sql = "SELECT * FROM users WHERE id = @id"; await using (MySqlCommand cmd = new MySqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@id", txtUserId.Text); await using (MySqlDataReader reader = await cmd.ExecuteReaderAsync()) { if (await reader.ReadAsync()) { // 更新UI控件 } } } } }注意await using和async void这两个关键字。UI事件处理器用async void是C#允许的特殊场景,其他普通方法不建议用async void,因为异常处理方式不同。还有一点,从.NET 6开始,MySqlConnection实现了IAsyncDisposable,所以用await using。
异步操作最大的好处是UI不卡顿,界面上可以加一个“加载中”的转圈动画,用户体验好很多。
6.4 从DataTable到ORM的平滑演进
前面所有的示例用的都是底层ADO.NET API。如果你的项目规模变大,需要更高效地开发,可以考虑引入ORM。
Dapper是一个轻量级ORM,它本质上还是用ADO.NET,只是帮你封装了从DataReader到实体对象的映射:
using Dapper; public class UserInfo { public int Id { get; set; } public string Username { get; set; } public int Age { get; set; } public string Email { get; set; } } var user = conn.QueryFirstOrDefault<UserInfo>("SELECT * FROM users WHERE id = @id", new { id = userId });一行代码完成查询和映射,性能和图灵级的手写ADO.NET几乎一致,因为Dapper在底层做了大量缓存优化。
如果你的项目非常复杂,实体关系多,事务应用广泛,那可以考虑EF Core。但我要说的是,EF Core不是银弹。EF Core的抽象层次高,生成的SQL不是完全可控的,有时候为了一个复杂查询反而要写原始SQL。而且EF Core的启动成本和学习成本都比Dapper高。
我的建议是:简单的工具、上位机、内部项目,用MySql.Data+参数化查询就够了;中大型企业项目,用Dapper或EF Core;追求极致性能且需要完全掌控SQL的场景,继续用原生ADO.NET。
提示:不管用不用ORM,MySql.Data.dll都是底层基础。ORM本质上也是基于这套驱动工作的,只是帮你封装了调用细节。所以理解本篇文章的内容,是所有高级方案的基础。
7. 实务注意:发布部署时的三个小细节
程序开发完成后还有几个容易忽略的细节。
第一,发布时dll必须跟着走。MySql.Data.dll是个托管dll,但发布时如果忘了把它包含进去,部署到其他机器上就会报错“找不到MySql.Data.dll”。用NuGet安装的话,发布时会自动复制到输出目录。手动引用的dll,要在dll的属性里把“复制到输出目录”设为“如果较新则复制”。
第二,发布目标机器的环境。如果目标是Windows的普通电脑,需要安装对应版本的.NET运行时。如果是.NET Framework项目,Windows 10/11自带.NET Framework 4.8运行库,基本不用装。如果是.NET 6/8项目,需要安装.NET Desktop Runtime。桌面程序一定要装Desktop Runtime,不是普通的Runtime,这个问题我见过很多次。
第三,区别于Web应用,上位机和桌面程序的数据库连接串通常写在配置文件里。WinForm项目用App.config:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="MySqlConn" connectionString="Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;CharSet=utf8mb4;SslMode=None;" /> </connectionStrings> </configuration>代码里读取:
string connStr = ConfigurationManager.ConnectionStrings["MySqlConn"].ConnectionString;注意要添加System.Configuration的引用。把连接串放到配置文件里,好处是改数据库地址不用重新编译程序,直接改配置文件就行。对于工控机、现场部署的软件来说,这个习惯能救命。你不想为了改一个IP地址,专门带电脑跑一趟现场重新编译发布吧?
8. 写在最后的一些经验之谈
做C#连MySQL这件事,我从最开始踩过的坑到现在基本上闭着眼睛都能写,最大的感悟是什么?不是某个API怎么用,而是数据库访问代码的容错意识。
真实环境中,MySQL服务可能会挂、网络可能会断、SQL可能会写错、数据可能会不符合预期,这些情况在你的开发环境中永远不会发生,但一旦上了生产环境,它们就像约好了似的排队出现。所以无论你的代码业务逻辑多简单,都必须加异常处理,都必须用using保证资源释放,都必须用参数化查询防止注入,这些不是“高级技巧”,而是基本功。
另外一点经验是关于调试效率的。遇到数据库连不上的问题,我建议大家先用官方客户端工具(如MySQL Workbench或命令行)验证一下连接串里的信息能不能用。客户端能连上,说明服务器、账号、密码都没问题,问题出在程序这边的连接串或dll版本上。如果客户端也连不上,那就别急着看代码了,先排查MySQL服务本身的状态、端口、防火墙这些基础环节。先把问题定位到某一端,再集中精力解决,这是最省时间的方法。
最后,我再分享一个小技巧。在开发阶段,建议临时开启MySql.Data的详细日志或者用try-catch把完整异常信息输出到控制台或日志文件。很多时候,报错信息里已经包含了问题根源,比如认证方式不支持、连接被拒绝、表不存在,精确到具体的问题描述。不要只报一个“连接失败”就完事,要把ex.Message和ex.StackTrace都记录下来,解决问题会快很多。
MySql.Data.dll这条路走通了,后面无论是玩Dapper还是EF Core,本质上都是在同一套底层驱动上做更上层的封装。把这篇内容吃透,C#操作MySQL的核心能力你就真正掌握了。