C#教程

关注公众号 jb51net

关闭
首页 > 软件编程 > C#教程 > C#连接MySQL

C#连接MySQL数据库的完整教程与常见问题排查方法

作者:菩提风

数据库连接是各类应用开发中的基础环节,其稳定性与性能直接影响业务系统的响应效率,本文会用最直白的语言讲透C#连接字符串、连接池、参数化查询和事务处理,附赠多年踩坑实录和排查技巧,让你告别连接超时、SQL注入和乱码问题,增删改查一次到位

做C#开发这些年,被问得最多的问题之一就是:“怎么连MySQL啊?为什么我连不上?为什么我这套增删改查写出来这么卡?”说实话,C#连MySQL本身不难,难的是把连接管理、参数化查询、事务处理这些基本功一次性做对。尤其是做上位机、桌面工具、中小型系统的朋友,很多人折腾半天连上了,结果读写逻辑写得稀碎,要么SQL注入漏洞满天飞,要么连接没释放把数据库拖垮。

这篇内容我按自己实际项目的习惯来写,覆盖从驱动选型、连接字符串、连接池原理,到读、增、删、改、查的完整实现,最后再附上我这几年踩过的坑和排查思路。不管你是刚接触C#的新手,还是写了好几年业务代码但没系统整理过数据库操作的老手,这篇应该都能让你少走一些弯路。

1. 准备工作与驱动选型:为什么我最终选了MySql.Data

1.1 三种常见驱动方案对比

C#要连MySQL,首先得解决“用什么连”的问题。市面上主流方案有三条路:官方提供的MySql.Data(也叫MySqlConnector的前身,但官方维护的还是MySql.Data)、开源社区性能更好的MySqlConnector、以及万金油式的ODBC方式。

我最早用的是ODBC,因为当时觉得“一套代码通吃所有数据库”很酷。后来发现这玩意配置麻烦,依赖系统ODBC驱动版本,换一台机器部署就各种报错,调试成本极高。ODBC适合做ETL工具、通用查询器这类产品,业务项目里用它是给自己找不痛快。

MySqlConnector这几年口碑很好,异步性能、管道协议支持、内存占用都优于官方驱动,尤其在.NET Core 3.0+的高并发场景下优势明显。但对于绝大多数桌面程序、中小型Web系统来说,官方MySql.Data完全够用,坑也少,网上的资料和解答最全,出了问题搜一下基本都是现成答案。

1.2 环境安装与驱动引入细节

假设你已经装好了MySQL服务端(版本建议5.7或8.0+,8.0的caching_sha2_password认证在后面会有坑,我会专门讲),本地也准备好了Visual Studio 2022或者VS Code加.NET SDK。

驱动引入有两种常见方式:

# 方式一:NuGet命令行
Install-Package MySql.Data
# 方式二:dotnet CLI
dotnet add package MySql.Data

我自己的习惯是直接在NuGet包管理器里搜索MySql.Data,选稳定版下载。这里有个经验:看清楚项目目标框架。如果你还是.NET Framework 4.x的老项目,选8.0.x之前的版本更稳妥;如果用的是.NET 6/8,直接上最新的MySql.Data 8.x就行。我的一个老项目就是Framework 4.7.2强行引用了MySql.Data 8.0,结果启动直接报“Method not found”的错,后来老老实实降级到6.9.12才消停。

注意:NuGet安装成功后,check一下引用里有没有出现MySql.Data.dll。有时候安装显示成功,但项目没刷新,引用列表里还是空的,重新生成一下解决方案就好。

引入完成后,代码里先验证一下基础命名空间:

using MySql.Data.MySqlClient;

只要这行不飘红,环境就到位了。接下来就看连接字符串怎么写了。

2. 连接字符串与连接管理:这才是“连数据库”的核心

2.1 连接字符串关键参数逐项拆解

连接字符串写不对,后面全是白干。我见过太多人把这串配置当成“抄就完事”的东西,其实里面每一个参数都有讲究。一个标准的连接串长这样:

Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;Allow User Variables=true;Connection Timeout=30;Pooling=true;

拆开来看:

2.2 连接池的工作原理:从“TCP长连接与短连接”联想到的

很多新手写数据库代码有个习惯:每次操作都new一个连接,用完就Close。这在连接池开启时其实是“假关闭”——物理连接并没有真正断掉,而是回到了连接池里复用,这就是一种典型的“长连接”思想。

连接池本身不复杂:首次Open时创建一个物理TCP连接到MySQL服务器(这一步走完握手和认证),之后再用同一个连接字符串Open时,直接从池里拿已建立的连接,省去了重复TCP三次握手和MySQL握手认证的开销。而当连接池满了且没有空闲连接时,新请求就要等待,等的时间受 Connection Timeout 控制,默认15秒,超时就抛异常。

理解了这个原理你就知道为什么我会在连接串里写 Pooling=true ,也明白了为什么“用完就Close”依然是好习惯——把连接归还给池,而不是占用着不放手。真正坑人的是那种“只Open不Close”的写法,往池子里借了不还,池子满了,别人就只能干等超时。

2.3 推荐的连接管理写法

我的习惯是永远用using或try-catch-finally,保证连接一定会释放:

string connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;";

using (var conn = new MySqlConnection(connStr))
{
    conn.Open();
    // 业务操作...
}
// 这里连接自动关闭(归还池中)

如果你用的是C# 8.0以上, using var conn = new MySqlConnection(connStr); 的写法更简洁,作用域到方法结束自动释放。但我个人在写工具类时还是习惯显式的大括号范围,因为可以精确控制连接生命周期,避免连接占用时间过长。

经验:不要在循环里反复New连接,哪怕有连接池也不行。把连接拿到循环外面,循环里只创建Command,性能会有质的提升。我优化过一个批量导入功能,原来每1000条数据Open一次连接耗时40秒,改成单连接批量执行后直接降到3秒。

3. 查询操作(读):从List到DataTable的全面方案

3.1 基础读操作:ExecuteReader逐行读取

读数据是最常用的操作。核心对象是 MySqlCommand 和 MySqlDataReader 。一个最基础但完整的查询流程是这样:

using (var conn = new MySqlConnection(connStr))
{
    conn.Open();
    string sql = "SELECT id, name, age FROM users WHERE age > @minAge";
    
    using (var cmd = new MySqlCommand(sql, conn))
    {
        cmd.Parameters.AddWithValue("@minAge", 18);
        
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                int id = reader.GetInt32("id");
                string name = reader.GetString("name");
                int age = reader.GetInt32("age");
                Console.WriteLine($"ID: {id}, 姓名: {name}, 年龄: {age}");
            }
        }
    }
}

几个细节值得说透:

3.2 带参数查询:为什么必须参数化而不是拼接SQL

这是我最想强调的一点。很多新手图省事,喜欢写成这样:

string sql = "SELECT * FROM users WHERE name = '" + name + "'";

如果name是普通文本,这没啥感觉。但如果你做个登录框,用户在用户名里输入 ' OR '1'='1 ,你的SQL就变成了:

SELECT * FROM users WHERE name = '' OR '1'='1'

这等于永远返回真,直接绕过密码验证,这叫SQL注入。还有一种情况是输入内容里带单引号,比如“O'Neal”,拼接出来SQL直接语法错误。

正确的做法是参数化查询:

string sql = "SELECT * FROM users WHERE name = @name AND age > @age";
using (var cmd = new MySqlCommand(sql, conn))
{
    cmd.Parameters.AddWithValue("@name", name);
    cmd.Parameters.AddWithValue("@age", 18);
    // 执行...
}

参数化的本质是让驱动把参数值和SQL语句分开传给MySQL服务器,MySQL在解析SQL时把参数当作纯数据,而不是SQL片段,从根上杜绝了注入问题。同时还能利用MySQL的预处理语句缓存机制,同一个SQL模板多次执行时,服务端不用重新解析,效率更高。

这里有一个容易踩的坑: AddWithValue 对字符串类型会有NVarChar、VarChar的隐式推断。在MySQL里问题不大,但如果SQL里有隐式类型转换的地方(比如参数和字段类型不匹配),MySQL索引会失效,全表扫描导致慢查询。我的建议是能明确类型的就用 cmd.Parameters.Add("@name", MySqlDbType.VarChar).Value = name; ,这在大数据量查询时能避免很多性能隐患。

3.3 单条记录查询与DataTable填充

如果只需要取一条记录,用 ExecuteScalar 最省事。比如取用户总数:

string sql = "SELECT COUNT(*) FROM users WHERE status = 1";
using (var cmd = new MySqlCommand(sql, conn))
{
    int total = Convert.ToInt32(cmd.ExecuteScalar());
}

ExecuteScalar 返回结果集第一行第一列的值,适合聚合查询、判断记录是否存在( SELECT 1 FROM table WHERE ... LIMIT 1 )这类场景。

如果你做的系统需要绑定数据到DataGridView这类控件,或者需要把数据暂存起来做二次处理,用 MySqlDataAdapter 填充 DataTable 更直接:

string sql = "SELECT id, name, age, create_time FROM users";
using (var adapter = new MySqlDataAdapter(sql, conn))
{
    DataTable dt = new DataTable();
    adapter.Fill(dt);
    dataGridView1.DataSource = dt;
}

填完DataTable后连接就可以关了,数据已经全部拉到内存里,后续操作不需要连接保持。这个思路和DataReader有着本质区别:DataReader是“流式读取”,读一行用一行;DataTable是一次性全部载入内存。数据量小无所谓,数据量大就要权衡——我处理过百万级的导出任务,用DataTable直接内存爆掉,后来换成DataReader边读边写文件才解决。

3.4 排序、分页与模糊查询的组合姿势

查询这块再补充两个高频场景:排序和分页。MySQL的分页用 LIMIT @offset, @count ,注意参数化时你不能直接写 LIMIT @offset, @count 就完事,因为MySQL的预处理语句在某些版本下不支持LIMIT参数绑定(8.0以上可以,5.7不行)。稳妥的方案是拼接整数,但要先确保它是int类型再拼:

int pageIndex = 1, pageSize = 20;
int offset = (pageIndex - 1) * pageSize;
string sql = "SELECT id, name, age FROM users ORDER BY create_time DESC LIMIT " + offset + ", " + pageSize;

排序字段如果有用户可控输入,建议搞一个白名单映射,防止通过 ORDER BY 注入。例如:

Dictionary<string, string> sortMap = new Dictionary<string, string>
{
    { "name", "name" },
    { "age", "age" },
    { "create_time", "create_time" }
};
string orderBy = sortMap.ContainsKey(inputSort) ? sortMap[inputSort] : "id";

至于模糊查询,用 LIKE CONCAT('%', @keyword, '%') 。这里别写成 LIKE '%@keyword%' ,那匹配的是字面量“@keyword”。正确写法是:

string sql = "SELECT * FROM users WHERE name LIKE CONCAT('%', @keyword, '%')";
cmd.Parameters.AddWithValue("@keyword", keyword);

用 CONCAT 而不是直接拼 '%"+keyword+"%' ,是因为参数里如果带%或_会被当作通配符处理,而CONCAT是服务端拼接,参数值中的%依然会被当作通配符。如果不想让用户用%和_来扩大范围,可以先用 REPLACE 处理一下,这点后文会提到。

4. 写操作(增、删、改):ExecuteNonQuery与事务处理

4.1 新增数据的正确姿势

插入数据用的是 ExecuteNonQuery() ,返回值是受影响的行数,判断“插入是否成功”通常看它是否大于0:

string sql = "INSERT INTO users (name, age, email) VALUES (@name, @age, @email)";
using (var cmd = new MySqlCommand(sql, conn))
{
    cmd.Parameters.AddWithValue("@name", "张三");
    cmd.Parameters.AddWithValue("@age", 25);
    cmd.Parameters.AddWithValue("@email", "zhangsan@example.com");
    
    int rows = cmd.ExecuteNonQuery();
    if (rows > 0)
    {
        Console.WriteLine("插入成功");
    }
}

这里有一个关键需求:插完数据后经常要拿到自增主键ID。做法是在INSERT语句后面跟上 SELECT LAST_INSERT_ID() ,用 ExecuteScalar() 取:

string sql = "INSERT INTO users (name, age) VALUES (@name, @age); SELECT LAST_INSERT_ID();";
using (var cmd = new MySqlCommand(sql, conn))
{
    cmd.Parameters.AddWithValue("@name", "李四");
    cmd.Parameters.AddWithValue("@age", 30);
    
    int newId = Convert.ToInt32(cmd.ExecuteScalar());
    Console.WriteLine($"新用户ID:{newId}");
}

这个能工作的前提是 Allow User Variables=true 或者命令行使用了批处理语法。注意 LAST_INSERT_ID() 是基于“当前连接”的,不是全局的,所以并发插入时不会串号。但有一个前提:它必须在同一个连接、紧接着INSERT语句执行,如果你中间又执行了别的INSERT,那取到的就是最后那条的ID了。

4.2 更新与删除:不要忽略影响行数和WHERE条件

更新和删除在SQL语法上简单,但实际使用中有两个细节必须养成习惯。

第一个细节: 务必注意WHERE条件 。总有人在生产环境写 UPDATE users SET age = 18 没带WHERE,结果全表都被改了。这种事情一旦发生,跑都跑不掉。我自己的习惯是写UPDATE和DELETE之前,先在SELECT里用同样的WHERE确认一下“锁定的范围”,然后再执行写操作。这个习惯帮我挡过至少三次事故。

第二个细节: 检查返回的行数 。更新操作返回的“受影响行数”不一定是匹配的行数。比如:

UPDATE users SET age = 18 WHERE id = 100;

如果id=100的记录原来的age已经是18,那么MySQL默认返回的受影响行数是0(值没变化,不会产生实际的更新)。所以拿返回值判断“记录是否存在”会误判。需要区分这两个场景的话,改连接串加上 UseAffectedRows=true 或者用 ROW_COUNT() 单独统计。

重新设置MySQL的affected rows行为:默认是“找到多少行返回多少行”,改成“实际改变了多少行”可以通过连接串参数控制。场景不同需求不同,这个细节在数据库运维层面最深有体会。

更新子查询的问题我在实际项目中遇到过:MySQL不允许你在UPDATE一个表的同时,子查询里直接SELECT同一个表。比如:

UPDATE users SET score = (SELECT MAX(score) FROM users) WHERE id = 1;

这语法MySQL直接报错“You can't specify target table 'users' for update in FROM clause”。解决方案是用临时表包一层:

UPDATE users 
SET score = (SELECT maxScore FROM (SELECT MAX(score) AS maxScore FROM users) AS tmp) 
WHERE id = 1;

这是个冷门但典型的问题,网上每年都有人问,顺手记录在这里。

4.3 事务处理:多条写操作如何保证一致性

增删改里,最核心也最容易写错的就是事务。什么叫事务?简单说就是把多条写操作打包成一个“原子操作”,要么全成功,要么全回滚,不存在“成功一半”的中间状态。最经典的就是转账场景:A扣钱、B加钱,如果A扣了钱但B加钱失败,钱就凭空消失了。

在C#里用MySQL事务的标准写法:

using (var conn = new MySqlConnection(connStr))
{
    conn.Open();
    using (var transaction = conn.BeginTransaction())
    {
        try
        {
            string sql1 = "UPDATE account SET balance = balance - @amount WHERE id = @fromId";
            using (var cmd1 = new MySqlCommand(sql1, conn, transaction))
            {
                cmd1.Parameters.AddWithValue("@amount", 100);
                cmd1.Parameters.AddWithValue("@fromId", 1);
                cmd1.ExecuteNonQuery();
            }

            string sql2 = "UPDATE account SET balance = balance + @amount WHERE id = @toId";
            using (var cmd2 = new MySqlCommand(sql2, conn, transaction))
            {
                cmd2.Parameters.AddWithValue("@amount", 100);
                cmd2.Parameters.AddWithValue("@toId", 2);
                cmd2.ExecuteNonQuery();
            }

            transaction.Commit();
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }
}

关键点有两个:一是 MySqlCommand 创建时必须传入transaction对象,这样命令才会在事务上下文中执行;二是 Commit() 在try的最后, Rollback() 在catch里。别把Commit写在using外面,也别在Commit之后又去执行其他SQL而不新开事务。

我踩过的坑是:在 try 块里先 Commit() 然后才执行一些非SQL的后续逻辑,比如日志写入或者发通知。这些后续逻辑一旦抛异常,事务已经提交了,逻辑上出现了“数据库已改但通知没发”的断档。正确的做法是让事务范围只包含数据库操作,其他动作放到事务提交之后再做,或者反过来把所有可能失败的逻辑都放在Commit之前,但承诺大家养成“Commit即终点”的习惯最稳妥。

4.4 批量写入的两种优化方案

如果你要提高批量写入效率,常见两种方案。

第一种是单条SQL拼接 VALUES 多行插入。比如一次性插入100条用户记录:

INSERT INTO users (name, age) VALUES ('A', 20), ('B', 21), ('C', 22)

用C#拼这个SQL时注意参数的数量,MySQL默认有 max_allowed_packet 限制,一般8MB。每条记录3个字段,100条就是300个参数,完全没问题,但别一次拼几千条,容易超包大小限制,分批次每批500条左右比较稳。

第二种是使用 MySqlBulkLoader 或者 MySqlBulkCopy 。老版本的MySql.Data没有内置的BulkCopy,需要借助MySqlBulkLoader。在WinForm、WPF处理Excel导入数据到MySQL时,我实测过:逐条INSERT 10万条记录要几分钟,用BulkLoader不到3秒,差距非常大。

5. 封装一个顺手的数据访问帮助类

5.1 通用增删改查的实现思路

每次写业务都手动创建连接、Command、参数,重复代码太多。我的做法是封装一个轻量级DbHelper,把“拿连接、执行、释放”的逻辑留在内部,对外暴露简单方法:

public static class MySqlHelper
{
    private static readonly string _connStr = "Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;Pooling=true;";

    /// <summary>执行查询,返回DataTable</summary>
    public static DataTable ExecuteDataTable(string sql, params MySqlParameter[] parameters)
    {
        using (var conn = new MySqlConnection(_connStr))
        {
            conn.Open();
            using (var cmd = new MySqlCommand(sql, conn))
            {
                if (parameters != null && parameters.Length > 0)
                    cmd.Parameters.AddRange(parameters);
                    
                using (var adapter = new MySqlDataAdapter(cmd))
                {
                    DataTable dt = new DataTable();
                    adapter.Fill(dt);
                    return dt;
                }
            }
        }
    }

    /// <summary>执行增删改,返回受影响行数</summary>
    public static int ExecuteNonQuery(string sql, params MySqlParameter[] parameters)
    {
        using (var conn = new MySqlConnection(_connStr))
        {
            conn.Open();
            using (var cmd = new MySqlCommand(sql, conn))
            {
                if (parameters != null && parameters.Length > 0)
                    cmd.Parameters.AddRange(parameters);
                return cmd.ExecuteNonQuery();
            }
        }
    }

    /// <summary>执行查询,返回首行首列</summary>
    public static object ExecuteScalar(string sql, params MySqlParameter[] parameters)
    {
        using (var conn = new MySqlConnection(_connStr))
        {
            conn.Open();
            using (var cmd = new MySqlCommand(sql, conn))
            {
                if (parameters != null && parameters.Length > 0)
                    cmd.Parameters.AddRange(parameters);
                return cmd.ExecuteScalar();
            }
        }
    }
}

这套封装在几十个项目里用下来都挺稳。注意几个设计细节:每个方法内部都自己Open/Close,所以调用方不需要关心连接释放问题;参数类型用 MySqlParameter[] 传入,天然引导调用方使用参数化查询;返回DataTable意味着查询结果和连接已经解耦,调用方随便用。

5.2 上位机/桌面程序里的实际用法

我常年做C#上位机,需要一个高频场景:设备数据定时写入数据库。用上面的帮助类,代码可以写得非常干净:

// 设备温度数据上报
string sql = "INSERT INTO device_data (device_id, temperature, humidity, collect_time) VALUES (@deviceId, @temp, @humi, @time) ON DUPLICATE KEY UPDATE temperature = @temp, humidity = @humi";

MySqlParameter[] ps = {
    new MySqlParameter("@deviceId", deviceId),
    new MySqlParameter("@temp", tempValue),
    new MySqlParameter("@humi", humiValue),
    new MySqlParameter("@time", DateTime.Now)
};

int rows = MySqlHelper.ExecuteNonQuery(sql, ps);

这里用到了 ON DUPLICATE KEY UPDATE ,它的意思是:如果主键或唯一索引冲突,就执行更新而不是插入。这个语法在设备数据上报场景里极其好用,既保证“同样的设备ID只会有一条最新记录”,又不用先查一次再决定插入还是更新。

上位机程序还有一个常见问题:UI线程卡死。如果在按钮点击事件里直接执行数据库操作,数据量大时界面会无响应。解决方案是异步执行,比如:

private async void btnSave_Click(object sender, EventArgs e)
{
    string sql = "INSERT INTO records (data) VALUES (@data)";
    var param = new MySqlParameter("@data", txtData.Text);
    
    int rows = await Task.Run(() => MySqlHelper.ExecuteNonQuery(sql, param));
    MessageBox.Show(rows > 0 ? "保存成功" : "保存失败");
}

用 Task.Run 把数据库操作丢到线程池,界面就不会卡了。如果你的驱动版本支持异步方法,也可以用 ExecuteNonQueryAsync ,效果更优雅。

5.3 连接字符串应该放哪里

连接字符串写死在代码里当然能跑,但建议放在配置文件中。WinForm/WPF项目放在 App.config 里,Web项目放在 appsettings.json 里:

<connectionStrings>
    <add name="MySqlConn" 
         connectionString="Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;Charset=utf8mb4;SslMode=None;Pooling=true;" />
</connectionStrings>

代码里这样读:

string connStr = ConfigurationManager.ConnectionStrings["MySqlConn"].ConnectionString;

为什么放配置文件?因为开发环境、测试环境、生产环境的数据库地址和密码很可能不一样。程序部署到现场,运维只需要改配置文件而不用重新编译代码,这个便利性在交付项目时是刚需。另外从安全角度讲,明文密码放在代码里比放在配置文件里更隐蔽,但两者都不是最佳方案,有条件可以做加密,但那是另一个话题了。

6. 常见问题与排查技巧实录

6.1 “目标计算机积极拒绝连接”到底是谁的问题

这个报错原文通常是:

MySql.Data.MySqlClient.MySqlException: 无法连接到任何指定的 MySQL 主机。 
System.Net.Sockets.SocketException: 由于目标计算机积极拒绝,无法连接。127.0.0.1:3306

第一次遇到的人都会慌,但其实排查思路很清晰。这个错误的核心是“TCP层都没连上”,根本没到MySQL认证那一步。按顺序检查:

MySQL服务是否启动了。在Windows服务管理器里找MySQL服务(名字可能是MySQL80、MySQL57等),看状态是不是“正在运行”。没启动就右键启动。

端口对不对。如果MySQL没走默认3306,而是自定义了3307、3308,你在连接串里写3306肯定连不上。用命令行确认一下:

netstat -ano | findstr 3306

能看到 LISTENING 状态说明端口正常监听,看不到就是MySQL没起来或者配置了别的端口。 3. MySQL是否允许当前IP连接。如果服务器在另一台机器上,默认配置可能只允许localhost登录,需要修改MySQL的 bind-address 配置,或者在用户授权里加上对应host权限。比如在MySQL里执行:

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '密码' WITH GRANT OPTION;
FLUSH PRIVILEGES;

防火墙是否拦截了3306端口。Windows防火墙、云服务器安全组都要放行。本地开发连不上,先把防火墙临时关掉测试,能连上就是防火墙规则的问题,再精确添加允许规则即可。

这个报错的排查难度其实很低,但很多人一看到“积极拒绝”就慌,开始怀疑驱动问题、代码问题,绕一大圈回来结果发现MySQL服务没启动。

6.2 认证方式不兼容:caching_sha2_password是个大坑

如果你用的是MySQL 8.0以上版本,连接时很可能遇到这个报错:

Authentication method 'caching_sha2_password' not supported by any of the available plugins.

原因很直接:MySQL 8.0默认认证插件改成了caching_sha2_password,而部分旧版MySql.Data驱动(8.0.11版本之前)不支持这个新插件。解决办法有两个方向:

一是升级驱动。这是最推荐、最彻底的方式。NuGet里把MySql.Data升级到8.0.13以上版本,新版驱动已经支持caching_sha2_password了。

二是把MySQL用户改回旧版认证方式。如果因为某些原因驱动没法升级,可以执行:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;

两种方法我都用过。个人建议优先升级驱动,因为改回旧版认证会降低账户安全性,而且治标不治本,下次换个环境连接可能又踩坑。

6.3 中文乱码与时间日期时区问题速查

中文乱码在C#连MySQL里出现过两种情况。第一种是读取时正常,写入后变问号,多半是表和连接字符集的问题。建库建表时确保字符集是utf8mb4,连接串里也加上 Charset=utf8mb4 。两个都对了,中文基本不会乱。第二种是读取直接显示乱码,检查连接串charset是否写对了,比如误写成utf-8(带横杠),MySQL不识别就会用默认的latin1,中文必乱。

时间时区问题常见报错:

The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone.

这是MySQL 8.0的时区配置问题。解决方案是在连接串里加 ServerTimezone=Asia/Shanghai 或 Local timezone :

Server=127.0.0.1;Port=3306;Database=testdb;Uid=root;Pwd=123456;SslMode=None;ServerTimezone=Asia/Shanghai;

我遇到一个和这个相关的坑:应用服务器在本地(北京时间),数据库服务器在海外(UTC时区),写入时间字段时没注意时区转换,导致所有时间记录都差了几个小时。后来统一约定:数据库里所有时间字段存UTC时间,显示时再转换成本地时间,问题才算根治。

6.4 高频问题速查表

整理一份我这几年见过的高频问题对照表,方便遇到问题时快速定位:

症状大概率原因解决方法
无法连接,目标计算机积极拒绝MySQL服务未启动、端口不对、防火墙拦截检查服务状态、端口监听、防火墙规则
Authentication method not supportedMySQL 8.0新认证方式与旧驱动不兼容升级MySql.Data驱动,或改用户认证插件
Incorrect string value: '\xE5...'表或连接字符集不是utf8mb4改库表字符集为utf8mb4,连接串加Charset=utf8mb4
中文乱码连接串charset没配对或写错确保连接串Charset=utf8mb4
Time zone不识别数据库时区未设置连接串加ServerTimezone=Asia/Shanghai
连接池超时连接没有释放,池被耗尽检查代码里是否有Connection未Dispose
You can't specify target table for update in FROM clauseUPDATE子查询直接引用同一张表用临时表包一层
插入后取不到自增IDLAST_INSERT_ID()依赖当前连接确保在同一连接、同一命令文本中紧接INSERT执行

这里特别说一下连接池超时问题,它是最隐蔽的。故障表象是“运行一段时间后,数据库操作突然全部报错”,重启程序又好一阵。这种往往就是某个分支里new了MySqlConnection但没释放,连接被借走不还,池子耗尽,后续请求全部排队超时。排查方法:打开MySQL的 SHOW PROCESSLIST; ,如果看到大量 Sleep 状态的连接堆积,基本就是连接泄漏了。修复方式就是保证所有 MySqlConnection 都套上using。

6.5 调试MySQL命令的推荐姿势

最后分享一个调试习惯:在写C#代码前,先用MySQL命令行或者图形工具(Navicat、MySQL Workbench都行)把SQL语句验证一遍。确认SQL本身没问题,再写到代码里。这样可以隔离问题——是先排查SQL错误还是C#代码错误,不用两头猜。

至于C#里的SQL语句到底长什么样、参数值是否拼进去了,可以在代码里临时打印一下:

cmd.CommandText = sql;
Console.WriteLine($"Executing SQL: {cmd.CommandText}");

但注意别打印参数里的敏感字段(比如密码、令牌),否则日志会泄漏重要信息。

7. 个人项目中的几条实战体会

前前后后做了十几个涉及C#和MySQL的项目,从几百条数据的小工具到百万级数据量的上位机系统,要说最深刻的体会,倒不是某个语法写法,而是下面这几条很难在官方文档里看到的东西:

第一,写数据库代码要把“异常安全”刻在脑子里。不只是try-catch,而是所有资源都保证释放,所有操作都考虑“执行到一半崩了怎么办”。以前带过几个新人,问他们“事务和连接都using了吗”,回答“应该吧”,这种不确定性本身就是隐患。

第二,能用连接池的地方绝对不要“裸连”。连接池不是只为了性能,它本质上是在帮你做TCP长连接的管理。裸连每次新建TCP会话、做MySQL握手认证,在高频操作时开销极大,拖慢的不仅是数据库速度,还有整个应用程序的响应。

第三,SQL语句能参数化就一定参数化。这不是“避免SQL注入”这一个理由,还包括代码可读性、维护性、性能一致性这些综合好处。参数化之后,SQL模板不会因为输入值变化而改变,MySQL的查询缓存和预处理语句都能发挥作用。

第四,对于增删改查这种基本功,值得花时间做一套自己的帮助类。只要配置好连接字符串,后面所有业务的增删改查都通过它走,代码风格统一了,排查问题时的心智负担也小很多。我现在的帮助类越来越精简,但它覆盖了我90%以上的数据库操作场景。

最后再给新手一个建议:不要死记硬背代码,而是把连接字符串每个参数的含义、连接池的原理、参数化的必要性、事务的本质想明白。这些东西一旦通了,不管你是用MySql.Data还是MySqlConnector,不管是做WinForm、WPF还是ASP.NET Core,碰到数据库操作就不会再心虚了。

以上就是C#连接MySQL数据库的完整教程与常见问题排查方法的详细内容,更多关于C#连接MySQL的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文