C# 封装 MySQL 自动写入与查询工具类(基于 MySqlConnector)

做数据采集类项目时,常见需求是:定时把采集到的数据写入 MySQL,之后还能按表、列、行查询回来。这篇文章介绍一个封装好的工具类 MySqlWriter,基于 MySqlConnector,覆盖建表、写入、事务、定时、导出、查询的完整流程。

功能说明

1. 动态建表与预编译写入

调用 Initialize 时传入表数量、每个表的列标题数组和表名数组,类内部自动生成 CREATE TABLE 语句建表。列类型提供两种重载:指定 1~255 长度的 VARCHAR,或不限长度的 TEXT。每张表会自动追加 record_time VARCHAR(20) 时间列,存放格式化后的写入时间。

建表完成后,类会为每张表预编译一条 INSERT 命令并调用 Prepare()。预编译的作用是:SQL 只解析一次,后续写入只传参数,执行更快,也避免了重复拼接 SQL 的开销。

2. 事务批量写入

Write(string[][] data) 一次写入所有表各一行数据。时间戳由方法内部统一生成,保证同一批数据时间一致。useTransaction 为 true 时,所有 INSERT 包在一个事务里提交:任何一张表写入失败,事务整体回滚,不会出现部分表写入成功的中间状态。

3. 定时采集循环

StartTimedLoop(Func<string[][]> writeFunc, CancellationToken) 每秒执行一次:取数、写入、补偿睡眠。补偿逻辑是记录本轮开始时间,写入后只睡到 1000ms,保证周期稳定,不会累积漂移。传 CancellationToken 可随时停止。

4. 表管理与 CSV 导出

DropAllTables 删除当前库全部表,删除前先关闭外键检查,结束后恢复。ExportAllTablesToCsv 把每张表导出为同名 CSV,包含列名行;字段含逗号、引号或换行时按 CSV 规范转义,NULL 导出为空字符串。

5. 三个查询方法

写入之后配套了查询能力:

  • GetAllTableNames():返回当前数据库全部表名;

  • GetTableHeaders(tableName, out columnTitles, out rowTitles):返回指定表的列标题(含 record_time)和行标题(record_time 去重升序);

  • GetValue(tableName, columnTitle, rowTitle):按表名、列标题、行标题定位,返回对应值。

两个关键处理

标识符安全。 表名和列名不能参数绑定,只能拼接进 SQL,有注入风险。类里用 EscapeIdentifier 按 MySQL 规则转义反引号,并在拼接前先通过 information_schema 校验表确实存在,不存在直接抛 ArgumentException

连接并发。 MySqlConnector 的同一个 MySqlConnection 不能并发使用。定时写入和查询共用一条连接,所以三个查询方法都用 _queryLock 加锁串行执行。另外 record_time 可能重复,取值 SQL 固定 ORDER BY record_time DESC LIMIT 1,明确返回最新一条。

使用示例

cs 复制代码
using var writer = new MySqlWriter("密码", "192.168.1.10", 3306, "testdb", "root");
writer.Initialize(1, new[] { new[] { "温度", "湿度" } }, new[] { "env_data" }, 50);
writer.Write(new[] { new[] { "25.3", "60" } });
writer.StartTimedLoop(CollectData, cts.Token);

完整代码约 400 行:

cs 复制代码
using MySqlConnector;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;

namespace MyWpfTemplate.Tools
{
    // MySQL 数据库写入工具类:支持动态建表、批量写入、预编译语句、事务提交、精确计时和 CSV 导出。
    public class MySqlWriter : IDisposable
    {
        #region Basic

        // 数据库连接字符串,构造时生成,只读。
        private readonly string _connectionString;

        // 数据库连接对象,初始化后可用,释放后置空。
        private MySqlConnection? _connection;

        // 每个表对应一个预编译 INSERT 命令。
        private MySqlCommand[]? _commands;

        // 标记连接、表结构和预编译命令是否已就绪。
        private bool _isInitialized = false;

        // 创建写入工具。
        // password:数据库密码;ip:服务器 IP,空值按 localhost 处理;port:端口;database:数据库名;userName:用户名;tablePrefix:表名前缀。
        public MySqlWriter(string password, string? ip, int port, string database, string userName)
        {
            // IP 参数可选:不传就是 localhost,传了就使用传入的 IP。
            string serverIp = string.IsNullOrWhiteSpace(ip) ? "localhost" : ip;
            _connectionString = $"Server={serverIp};Port={port};Database={database};User ID={userName};Password={password};";
        }

        #endregion

        #region 初始化方法

        // 初始化并指定字符串列长度;varcharLength 需在 1~255,否则抛异常。
        // tableCount:表数量;titles:每个表的数据列标题,不含自动追加的 record_time;tableNames:每个表实际表名;varcharLength:VARCHAR 长度;dropExisting:是否先删表重建。
        public void Initialize(int tableCount, string[][] titles, string[] tableNames, int varcharLength, bool dropExisting = true)
        {
            if (varcharLength <= 0 || varcharLength > 255)
                throw new ArgumentException("varcharLength 必须在 1~255 之间", nameof(varcharLength));

            InitializeInternal(tableCount, titles, tableNames, varcharLength, dropExisting);
        }

        // 初始化并使用 TEXT 列类型;参数含义同带 varcharLength 的重载。
        public void Initialize(int tableCount, string[][] titles, string[] tableNames, bool dropExisting = true)
        {
            InitializeInternal(tableCount, titles, tableNames, 0, dropExisting);
        }

        // 仅建立连接,不创建表,也不预编译命令。
        public void Initialize()
        {
            if (string.IsNullOrWhiteSpace(_connectionString))
                throw new InvalidOperationException("连接字符串未正确构造");

            var connection = new MySqlConnection(_connectionString);
            connection.Open();
            _connection = connection;
        }

        #endregion

        #region Public

        // 写入一批数据;时间戳由本方法生成并放在 data 之前。
        // data:data[i] 为第 i 个表的一行数据,data[i][j] 对应 titles[i][j];useTransaction:true 时用事务包裹全部 INSERT。
        public void Write(string[][] data, bool useTransaction = true)
        {
            if (!_isInitialized)
                throw new InvalidOperationException("请先调用 Initialize 方法初始化");

            // 用局部变量固定可空引用,避免后续反复解引用产生空警告。
            var connection = _connection ?? throw new InvalidOperationException("数据库连接未正确初始化");
            var commands = _commands ?? throw new InvalidOperationException("数据库命令未正确初始化");

            if (data.Length != commands.Length)
                throw new ArgumentException($"data 长度({data.Length})与表数量({commands.Length})不匹配");

            // 生成写入时间戳,时间列不来自 data 参数。
            string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

            if (useTransaction)
            {
                using (var transaction = connection.BeginTransaction())
                {
                    try
                    {
                        ExecuteInserts(connection, commands, data, timestamp, transaction);
                        transaction.Commit();
                    }
                    catch
                    {
                        transaction.Rollback();
                        throw;
                    }
                }
            }
            else
            {
                ExecuteInserts(connection, commands, data, timestamp, null);
            }
        }

        // 启动每秒一次的定时采集写入循环。
        // writeFunc:每次循环返回待写数据,结构与 Write 的 data 相同;cancellationToken:请求取消后循环退出。
        public void StartTimedLoop(Func<string[][]> writeFunc, CancellationToken cancellationToken)
        {
            if (!_isInitialized)
                throw new InvalidOperationException("请先调用 Initialize 方法初始化");

            while (!cancellationToken.IsCancellationRequested)
            {
                DateTime loopStart = DateTime.Now;

                // 取数并写入。
                string[][] data = writeFunc();
                Write(data, useTransaction: true);

                // 按上一秒耗时补偿 sleep,保证周期接近 1 秒。
                TimeSpan elapsed = DateTime.Now - loopStart;
                if (elapsed.TotalMilliseconds < 1000)
                {
                    Thread.Sleep(1000 - (int)elapsed.TotalMilliseconds);
                }
            }
        }

        // 释放预编译命令和数据库连接。
        public void Dispose()
        {
            if (_commands != null)
            {
                foreach (var cmd in _commands)
                {
                    cmd?.Dispose();
                }
                _commands = null;
            }

            _connection?.Dispose();
            _connection = null;
            _isInitialized = false;
        }


        // 删除当前数据库中的全部表;会先关闭外键检查,结束后恢复。
        public void DropAllTables()
        {
            var connection = _connection;
            if (connection == null || connection.State != System.Data.ConnectionState.Open)
                throw new InvalidOperationException("数据库连接未打开");

            // 取当前数据库名。
            string dbName;
            using (var cmd = new MySqlCommand("SELECT DATABASE();", connection))
                dbName = (string?)cmd.ExecuteScalar() ?? throw new InvalidOperationException("未选择数据库");

            // 查询 information_schema 获取所有表名。
            var tableNames = new List<string>();
            using (var cmd = new MySqlCommand(
                "SELECT table_name FROM information_schema.tables WHERE table_schema = @db", connection))
            {
                cmd.Parameters.AddWithValue("@db", dbName);
                using var reader = cmd.ExecuteReader();
                while (reader.Read())
                    tableNames.Add(reader.GetString(0));
            }

            // 有表才执行批量删除,避免无意义开关外键检查。
            if (tableNames.Count == 0)
                return;

            using (var cmd = new MySqlCommand("SET FOREIGN_KEY_CHECKS = 0;", connection))
                cmd.ExecuteNonQuery();

            foreach (var name in tableNames)
            {
                using var cmd = new MySqlCommand($"DROP TABLE IF EXISTS `{name}`;", connection);
                cmd.ExecuteNonQuery();
            }

            using (var cmd = new MySqlCommand("SET FOREIGN_KEY_CHECKS = 1;", connection))
                cmd.ExecuteNonQuery();
        }
        // 将当前数据库所有表导出为同名 CSV 文件。
        // outputDir:输出目录,空值使用程序运行目录;目录需已存在。
        public void ExportAllTablesToCsv(string? outputDir = null)
        {
            var connection = _connection;
            if (connection == null || connection.State != System.Data.ConnectionState.Open)
                throw new InvalidOperationException("数据库连接未打开");

            // 取当前数据库名。
            string dbName;
            using (var cmd = new MySqlCommand("SELECT DATABASE();", connection))
                dbName = (string?)cmd.ExecuteScalar() ?? throw new InvalidOperationException("未选择数据库");

            // 查询当前库下所有表。
            var tableNames = new List<string>();
            using (var cmd = new MySqlCommand(
                "SELECT table_name FROM information_schema.tables WHERE table_schema = @db", connection))
            {
                cmd.Parameters.AddWithValue("@db", dbName);
                using var reader = cmd.ExecuteReader();
                while (reader.Read())
                    tableNames.Add(reader.GetString(0));
            }

            outputDir ??= AppDomain.CurrentDomain.BaseDirectory;

            foreach (var tableName in tableNames)
            {
                string filePath = Path.Combine(outputDir, $"{tableName}.csv");

                // writer 覆盖写 CSV;cmd/reader 按表读取;using 保证释放顺序稳定。
                using (var writer = new StreamWriter(filePath, false, Encoding.UTF8))
                using (var cmd = new MySqlCommand($"SELECT * FROM `{tableName}`;", connection))
                using (var reader = cmd.ExecuteReader())
                {
                    // 先写列名行。
                    for (int i = 0; i < reader.FieldCount; i++)
                    {
                        writer.Write(EscapeCsvField(reader.GetName(i)));
                        if (i < reader.FieldCount - 1) writer.Write(',');
                    }
                    writer.WriteLine();

                    // 再写数据行,NULL 统一导出为空字符串。
                    while (reader.Read())
                    {
                        for (int i = 0; i < reader.FieldCount; i++)
                        {
                            var value = reader.IsDBNull(i) ? "" : reader.GetValue(i).ToString() ?? "";
                            writer.Write(EscapeCsvField(value));
                            if (i < reader.FieldCount - 1) writer.Write(',');
                        }
                        writer.WriteLine();
                    }
                }
            }
        }


        // 查询锁:三个查询方法与写入循环共用同一连接,必须串行访问。
        private readonly object _queryLock = new object();

        // 查询所有表名,按字典序返回。
        public List<string> GetAllTableNames()
        {
            var connection = GetOpenConnection();

            lock (_queryLock)
            {
                var tableNames = new List<string>();
                using (var cmd = new MySqlCommand(
                    "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name;", connection))
                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                        tableNames.Add(reader.GetString(0));
                }
                return tableNames;
            }
        }

        // 输入表名,查询所有列标题和行标题。
        // tableName:表名;columnTitles:输出列标题(含 record_time,按定义顺序);rowTitles:输出行标题(record_time 去重升序)。
        public void GetTableHeaders(string tableName, out List<string> columnTitles, out List<string> rowTitles)
        {
            var connection = GetOpenConnection();
            string table = EscapeIdentifier(tableName, nameof(tableName));
            columnTitles = new List<string>();
            rowTitles = new List<string>();

            lock (_queryLock)
            {
                // 先校验表存在,避免不存在或恶意的表名被拼进 SQL。
                using (var cmd = new MySqlCommand(
                    "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = @name;", connection))
                {
                    cmd.Parameters.AddWithValue("@name", tableName.Trim());
                    if (Convert.ToInt64(cmd.ExecuteScalar()) == 0)
                        throw new ArgumentException($"表不存在:{tableName}", nameof(tableName));
                }

                // LIMIT 0 只取列名,不读取数据。
                using (var cmd = new MySqlCommand($"SELECT * FROM `{table}` LIMIT 0;", connection))
                using (var reader = cmd.ExecuteReader())
                {
                    for (int i = 0; i < reader.FieldCount; i++)
                        columnTitles.Add(reader.GetName(i));
                }

                // record_time 是固定的行标题列。
                using (var cmd = new MySqlCommand(
                    $"SELECT DISTINCT record_time FROM `{table}` ORDER BY record_time;", connection))
                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                        rowTitles.Add(reader.GetString(0));
                }
            }
        }

        // 输入表名、列标题、行标题,返回对应值。
        // tableName:表名;columnTitle:列标题;rowTitle:行标题(record_time);返回最新一条匹配值,无匹配或 NULL 返回 null。
        public string? GetValue(string tableName, string columnTitle, string rowTitle)
        {
            var connection = GetOpenConnection();
            string table = EscapeIdentifier(tableName, nameof(tableName));
            string column = EscapeIdentifier(columnTitle, nameof(columnTitle));

            lock (_queryLock)
            {
                // record_time 可能重复,ORDER BY 明确取最新一条。
                using var cmd = new MySqlCommand(
                    $"SELECT `{column}` FROM `{table}` WHERE record_time = @row ORDER BY record_time DESC LIMIT 1;", connection);
                cmd.Parameters.AddWithValue("@row", rowTitle);

                var result = cmd.ExecuteScalar();
                return result == null || result == DBNull.Value ? null : result.ToString();
            }
        }
        #endregion

        #region Methods

        // 校验并转义 SQL 标识符(表名/列名):空值抛异常,内部反引号按 MySQL 规则双写。
        private static string EscapeIdentifier(string identifier, string paramName)
        {
            if (string.IsNullOrWhiteSpace(identifier))
                throw new ArgumentException("标识符不能为空", paramName);
            return identifier.Replace("`", "``");
        }

        // 检查连接已打开并返回,供查询类公共方法复用。
        private MySqlConnection GetOpenConnection()
        {
            var connection = _connection;
            if (connection == null || connection.State != System.Data.ConnectionState.Open)
                throw new InvalidOperationException("数据库连接未打开");
            return connection;
        }

        // 实际执行 INSERT;transaction 为空表示不使用事务。
        // connection:已打开连接;commands:与表一一对应的预编译命令;data:待写数据;timestamp:本条写入时间。
        private void ExecuteInserts(MySqlConnection connection, MySqlCommand[] commands, string[][] data, string timestamp, MySqlTransaction? transaction)
        {
            for (int i = 0; i < commands.Length; i++)
            {
                if (transaction != null)
                    commands[i].Transaction = transaction;

                // 第 0 个参数是自动追加的 record_time。
                commands[i].Parameters[0].Value = timestamp;

                // 后续参数来自 data[i],顺序与 titles[i] 一致。
                for (int j = 0; j < data[i].Length; j++)
                {
                    commands[i].Parameters[j + 1].Value = data[i][j];
                }

                commands[i].ExecuteNonQuery();
            }
        }



        // 内部初始化:打开连接、按需删表、校验标题、建表并预编译 INSERT 命令。
        // tableCount:表数量;titles:每个表的数据列标题,不含时间戳;tableNames:每个表实际表名;varcharLength:VARCHAR 长度,0 表示 TEXT;dropExisting:是否先删表重建。
        private void InitializeInternal(int tableCount, string[][] titles, string[] tableNames, int varcharLength, bool dropExisting)
        {
            if (string.IsNullOrWhiteSpace(_connectionString))
                throw new InvalidOperationException("连接字符串未正确构造");
            if (titles == null || titles.Length < tableCount)
                throw new ArgumentException("titles 数组长度必须大于等于 tableCount");
            if (tableNames == null || tableNames.Length < tableCount)
                throw new ArgumentException("tableNames 数组长度必须大于等于 tableCount");

            var connection = new MySqlConnection(_connectionString);
            connection.Open();
            _connection = connection;

            if (dropExisting && tableCount > 0)
            {
                // 拼一次 DROP TABLE IF EXISTS,减少往返。
                var dropSql = new StringBuilder("DROP TABLE IF EXISTS ");
                for (int i = 1; i <= tableCount; i++)
                {
                    dropSql.Append($"{tableNames[i - 1]}");
                    if (i < tableCount) dropSql.Append(", ");
                }
                dropSql.Append(";");
                using (var cmd = new MySqlCommand(dropSql.ToString(), connection))
                    cmd.ExecuteNonQuery();
            }

            // 建表前先校验每个标题非空,避免生成非法 SQL。
            for (int i = 0; i < tableCount; i++)
            {
                foreach (string title in titles[i])
                {
                    if (string.IsNullOrWhiteSpace(title))
                        throw new ArgumentException($"第 {i + 1} 个表的标题不能为空");
                }
            }

            // varcharLength 为 0 时使用 TEXT,否则使用指定长度 VARCHAR。
            string columnType = varcharLength == 0 ? "TEXT" : $"VARCHAR({varcharLength})";

            for (int i = 1; i <= tableCount; i++)
            {
                string[] thisTableTitles = titles[i - 1];
                var colDefs = new StringBuilder();
                for (int j = 0; j < thisTableTitles.Length; j++)
                {
                    colDefs.Append($"{thisTableTitles[j]} {columnType}");
                    if (j < thisTableTitles.Length - 1) colDefs.Append(", ");
                }

                // record_time 为固定时间列,使用 VARCHAR(20) 存放格式化字符串。
                string sql = $"CREATE TABLE IF NOT EXISTS {tableNames[i - 1]} (record_time VARCHAR(20) NOT NULL, {colDefs}) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;";
                using (var cmd = new MySqlCommand(sql, connection))
                    cmd.ExecuteNonQuery();
            }

            _commands = new MySqlCommand[tableCount];
            var paramType = varcharLength == 0 ? MySqlDbType.Text : MySqlDbType.VarChar;

            for (int i = 0; i < tableCount; i++)
            {
                string[] thisTableTitles = titles[i];
                var insertSql = new StringBuilder();
                insertSql.Append($"INSERT INTO {tableNames[i]} (record_time, ");
                for (int j = 0; j < thisTableTitles.Length; j++)
                {
                    insertSql.Append(thisTableTitles[j]);
                    if (j < thisTableTitles.Length - 1) insertSql.Append(", ");
                }
                insertSql.Append(") VALUES (@t, ");
                for (int j = 0; j < thisTableTitles.Length; j++)
                {
                    insertSql.Append($"@d{j}");
                    if (j < thisTableTitles.Length - 1) insertSql.Append(", ");
                }
                insertSql.Append(")");

                _commands[i] = new MySqlCommand(insertSql.ToString(), connection);
                _commands[i].Parameters.Add("@t", MySqlDbType.VarChar);
                for (int j = 0; j < thisTableTitles.Length; j++)
                {
                    _commands[i].Parameters.Add($"@d{j}", paramType);
                }
                _commands[i].Prepare();
            }

            _isInitialized = true;
        }


        // CSV 字段转义:含逗号、引号、换行或回车时加双引号包裹,并把内部引号替换为两个引号。
        // field:待转义字段;返回可直接写入 CSV 的字段文本。
        private static string EscapeCsvField(string field)
        {
            if (field.Contains(',') || field.Contains('"') || field.Contains('\n') || field.Contains('\r'))
            {
                return "\"" + field.Replace("\"", "\"\"") + "\"";
            }
            return field;
        }

        #endregion
    }
}

单文件实现,适合采集、监控等场景直接使用或按需裁剪。

相关推荐
zxy284722530111 小时前
C# 音频倍速播放
c#·音频·naudio·soundtouch·倍速
呆萌很14 小时前
MySQL 数据库和表的管理操作(命令行)
数据库·mysql
毕业设计70314 小时前
(免费领源码)基于Python的博物馆研学活动管理系统的设计与实现22724- java、PHP、python、C#、小程序、大数据、单片机、网络工程等)
python·mysql·随机森林·pycharm·django·flask·推荐算法
Water_Sunzhipeng15 小时前
HandyControl Demo 设置特定框架记录
c#
czhc114007566315 小时前
插补、另存为、标记过滤、采集超时——今天聊的四件事
c#
小羊没烦恼!16 小时前
Memory 记忆设计讨论:Agent Memory 的数据模型可以怎么设计
java·开发语言·前端·c++·c#
程序员阿鹏19 小时前
如何实现MySQL分库分表?
数据结构·数据库·sql·mysql·缓存
风哥2号19 小时前
数据库教程FGMT33‑MySQL主从复制项目实施与维护06(MySQL8.4/9.7 MGR组复制)
数据库·mysql
小羊没烦恼!20 小时前
Memory 记忆设计讨论:为什么 Agent Memory 不能只靠向量数据库?
java·开发语言·windows·算法·c#