作为一名C#开发者,我经常遇到这样的场景:通过VPN访问内网的SQL Server,然后需要将数据同步到外网数据库。手动操作?太低效!写脚本?容易出错!
今天我要分享一个完整的 C# WinForms数据库同步工具 ,不仅支持表结构同步,还能进行批量数据同步,关键是代码完全开源,拿来就能用!
🎯 我们要解决什么问题? 在企业级应用中,数据库同步是个老大难问题:
手动导入导出
效率低下,容易遗漏
结构不一致
主键、约束、默认值经常丢失
数据量大
传统方式容易超时
回滚困难
出错后难以恢复
💡 完美解决方案 我们的C#同步工具具备以下核心功能:
✅ 智能表结构同步 :自动识别列类型、主键、默认值
✅ 高效批量数据传输 :使用SqlBulkCopy提升性能
✅ 事务保护 :确保数据一致性
✅ 进度监控 :实时显示同步状态
✅ 配置管理 :支持保存和加载同步配置
🔥 核心代码实战 1️⃣ 表结构同步核心方法 private async Task SynchronizeTableStructureAsync ( string tableName) { try { // 🎯 获取源表的完整结构信息 DataTable columnsSchema = await GetTableColumnsAsync(tableName); List< string > primaryKeys = await GetPrimaryKeysAsync(tableName); Dictionary< string , string > defaultConstraints = await GetDefaultConstraintsAsync(tableName); using (SqlConnection targetConnection = new SqlConnection(_targetConnectionString)) { await targetConnection.OpenAsync(); using (SqlTransaction transaction = targetConnection.BeginTransaction()) { try { // 🚀 重建目标表 if (await CheckTableExistsAsync(tableName)) { await ExecuteNonQueryAsync ($ "DROP TABLE [{tableName}]" , targetConnection, transaction) ; } // 🔧 动态生成CREATE TABLE语句 string createTableSql = BuildCreateTableSql(tableName, columnsSchema, primaryKeys, defaultConstraints); await ExecuteNonQueryAsync (createTableSql, targetConnection, transaction) ; transaction.Commit(); LogMessage($ "✅ 表 {tableName} 结构同步成功" ); } catch (Exception ex) { transaction.Rollback(); throw new Exception($ "表结构同步失败: {ex.Message}" ); } } } } catch (Exception ex) { LogMessage($ "❌ 处理表 {tableName} 时发生错误: {ex.Message}" ); throw ; } }
2️⃣ 高性能数据同步 private async Task SynchronizeTableDataFullAsync ( string tableName) { try { LogMessage($ "🔄 开始全量同步表 {tableName} 的数据..." ); // 📊 从源数据库读取数据 DataTable sourceData = new DataTable(); using (SqlConnection sourceConnection = new SqlConnection(_sourceConnectionString)) { await sourceConnection.OpenAsync(); string selectQuery = $ "SELECT * FROM [{tableName}]" ; using (SqlCommand command = new SqlCommand(selectQuery, sourceConnection)) { command.CommandTimeout = 300 ; // 5分钟超时 using (SqlDataAdapter adapter = new SqlDataAdapter(command)) { adapter.Fill(sourceData); } } } LogMessage($ "📥 从源数据库读取到 {sourceData.Rows.Count} 行数据" ); // 🚀 使用SqlBulkCopy高效写入 using (SqlConnection targetConnection = new SqlConnection(_targetConnectionString)) { await targetConnection.OpenAsync(); using (SqlTransaction transaction = targetConnection.BeginTransaction()) { try { // 🗑️ 清空目标表 await ExecuteNonQueryAsync ($ "DELETE FROM [{tableName}]" , targetConnection, transaction) ; if (sourceData.Rows.Count > 0 ) { // ⚡ 批量插入数据 using (SqlBulkCopy bulkCopy = new SqlBulkCopy(targetConnection, SqlBulkCopyOptions.Default, transaction)) { bulkCopy.DestinationTableName = tableName; bulkCopy.BatchSize = ( int )numericBatchSize.Value; // 可配置批次大小 bulkCopy.BulkCopyTimeout = 600 ; // 10分钟超时 bulkCopy.EnableStreaming = true ; // 启用流式传输 // 🎯 映射列名 foreach (DataColumn column in sourceData.Columns) { bulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName); } await bulkCopy.WriteToServerAsync(sourceData); } } transaction.Commit(); LogMessage($ "✅ 表 {tableName} 全量数据同步完成" ); } catch (Exception ex) { transaction.Rollback(); throw new Exception($ "数据同步失败,已回滚: {ex.Message}" ); } } } } catch (Exception ex) { LogMessage($ "❌ 同步表 {tableName} 数据时出错: {ex.Message}" ); throw ; } }
3️⃣ 智能获取表结构信息 private async Task<DataTable> GetTableColumnsAsync( string tableName) { DataTable schema = new DataTable(); using (SqlConnection connection = new SqlConnection(_sourceConnectionString)) { await connection.OpenAsync(); // 🔍 查询表的详细列信息 string query = @ " SELECT c.name AS COLUMN_NAME, t.name AS DATA_TYPE, CASE WHEN t.name IN ('nvarchar','nchar','varchar','char') THEN c.max_length ELSE NULL END AS CHARACTER_MAXIMUM_LENGTH, CASE WHEN c.is_nullable = 1 THEN 'YES' ELSE 'NO' END AS IS_NULLABLE, c.precision AS NUMERIC_PRECISION, c.scale AS NUMERIC_SCALE FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id INNER JOIN sys.tables tbl ON c.object_id = tbl.object_id WHERE tbl.name = @TableName ORDER BY c.column_id" ; using (SqlCommand command = new SqlCommand(query, connection)) { command.Parameters.AddWithValue( "@TableName" , tableName); using (SqlDataAdapter adapter = new SqlDataAdapter(command)) { adapter.Fill(schema); } } } return schema; }
🎨 界面设计亮点 异步操作与进度监控 private async Task StartSyncProcess (List< string > tables) { _isSyncing = true ; _cancellationTokenSource = new CancellationTokenSource(); try { SetSyncMode( true ); // 🔒 锁定界面 progressBarMain.Maximum = tables.Count; for ( int i = 0 ; i < tables.Count; i++) { if (_cancellationTokenSource.Token.IsCancellationRequested) break ; string tableName = tables[i]; LogMessage($ "🔄 正在同步表:{tableName} ({i + 1}/{tables.Count})" ); // 🎯 执行同步操作 await SynchronizeTableStructureAsync (tableName) ; await SynchronizeTableDataFullAsync (tableName) ; // 📊 更新进度 UpdateProgress(i + 1 , tables.Count); } LogMessage( "🎉 同步操作完成!" ); } catch (OperationCanceledException) { LogMessage( "⏹️ 同步操作已取消" ); } finally { _isSyncing = false ; SetSyncMode( false ); // 🔓 解锁界面 } }
🛡️ 安全性保障 连接字符串验证 private async Task TestConnection ( string connectionString, string dbName) { if ( string .IsNullOrWhiteSpace(connectionString)) { MessageBox.Show($ "请输入{dbName}连接字符串!" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Warning); return ; } try { using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); MessageBox.Show($ "✅ {dbName}连接测试成功!" , "成功" , MessageBoxButtons.OK, MessageBoxIcon.Information); } } catch (Exception ex) { MessageBox.Show($ "❌ {dbName}连接测试失败:{ex.Message}" , "错误" , MessageBoxButtons.OK, MessageBoxIcon.Error); } }
🔧 配置管理功能 [Serializable] public class SyncConfig { public string SourceConnectionString { get; set ; } public string TargetConnectionString { get; set ; } public bool SyncStructure { get; set ; } public bool SyncData { get; set ; } public int BatchSize { get; set ; } public List< string > SelectedTables { get; set ; } } // 💾 保存配置 private void SaveConfig ( string filePath, SyncConfig config) { var serializer = new XmlSerializer(typeof(SyncConfig)); using (var writer = new StreamWriter(filePath)) { serializer.Serialize(writer, config); } }
完整代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.SqlClient; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml.Serialization; using Microsoft.Data.SqlClient; namespace AppSqlServerSynTool { public partial class FrmMain : Form { private CancellationTokenSource _cancellationTokenSource; private bool _isSyncing = false ; private string _sourceConnectionString = "" ; private string _targetConnectionString = "" ; public FrmMain () { InitializeComponent(); InitializeEvents(); InitializeForm(); this .Load += FrmMain_Load; } #region 事件绑定 private void InitializeEvents () { // 连接测试按钮事件 btnTestSourceConnection.Click += BtnTestSourceConnection_Click; btnTestTargetConnection.Click += BtnTestTargetConnection_Click; // 表操作按钮事件 btnRefreshTables.Click += BtnRefreshTables_Click; btnSelectAll.Click += BtnSelectAll_Click; btnSelectNone.Click += BtnSelectNone_Click; // 同步操作按钮事件 btnStartSync.Click += BtnStartSync_Click; btnCancel.Click += BtnCancel_Click; // 配置操作按钮事件 btnSaveConfig.Click += BtnSaveConfig_Click; btnLoadConfig.Click += BtnLoadConfig_Click; // 退出按钮事件 btnExit.Click += BtnExit_Click; // 菜单事件 saveConfigToolStripMenuItem.Click += BtnSaveConfig_Click; loadConfigToolStripMenuItem.Click += BtnLoadConfig_Click; exitToolStripMenuItem.Click += BtnExit_Click; aboutToolStripMenuItem.Click += AboutToolStripMenuItem_Click; // 窗体关闭事件 this .FormClosing += FrmMain_FormClosing; } #endregion #region 初始化 private void InitializeForm () { // 设置默认连接字符串示例 txtSourceConnection.Text = "Server=.;Database=SourceDB;Integrated Security=true;" ; txtTargetConnection.Text = "Server=.;Database=TargetDB;Integrated Security=true;" ; // 设置默认值 numericBatchSize.Value = 1000 ; } private void FrmMain_Load (object sender, EventArgs e) { LogMessage( "程序启动完成,请配置数据库连接信息。" ); } #endregion #region 连接测试 private async void BtnTestSourceConnection_Click (object sender, EventArgs e) { await TestConnection (txtSourceConnection.Text, "源数据库" ) ; } private async void BtnTestTargetConnection_Click (object sender, EventArgs e) { await TestConnection (txtTargetConnection.Text, "目标数据库" ) ; } private async Task TestConnection ( string connectionString, string dbName) { if ( string .IsNullOrWhiteSpace(connectionString)) { MessageBox.Show($ "请输入{dbName}连接字符串!" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Warning); return ; } try { SetControlsEnabled( false ); toolStripStatusLabel.Text = $ "正在测试{dbName}连接..." ; using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); MessageBox.Show($ "{dbName}连接测试成功!" , "成功" , MessageBoxButtons.OK, MessageBoxIcon.Information); LogMessage($ "{dbName}连接测试成功。" ); } } catch (Exception ex) { MessageBox.Show($ "{dbName}连接测试失败:{ex.Message}" , "错误" , MessageBoxButtons.OK, MessageBoxIcon.Error); LogMessage($ "{dbName}连接测试失败:{ex.Message}" ); } finally { SetControlsEnabled( true ); toolStripStatusLabel.Text = "就绪" ; } } #endregion #region 表操作 private async void BtnRefreshTables_Click (object sender, EventArgs e) { if ( string .IsNullOrWhiteSpace(txtSourceConnection.Text)) { MessageBox.Show( "请先配置源数据库连接字符串!" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Warning); return ; } try { SetControlsEnabled( false ); toolStripStatusLabel.Text = "正在获取表列表..." ; checkedListBoxTables.Items.Clear(); _sourceConnectionString = txtSourceConnection.Text; var tables = await GetTableListAsync(_sourceConnectionString); foreach (var table in tables) { checkedListBoxTables.Items.Add(table); } LogMessage($ "成功获取 {tables.Count} 个表。" ); } catch (Exception ex) { MessageBox.Show($ "获取表列表失败:{ex.Message}" , "错误" , MessageBoxButtons.OK, MessageBoxIcon.Error); LogMessage($ "获取表列表失败:{ex.Message}" ); } finally { SetControlsEnabled( true ); toolStripStatusLabel.Text = "就绪" ; } } private void BtnSelectAll_Click (object sender, EventArgs e) { for ( int i = 0 ; i < checkedListBoxTables.Items.Count; i++) { checkedListBoxTables.SetItemChecked(i, true ); } LogMessage($ "已选择所有 {checkedListBoxTables.Items.Count} 个表。" ); } private void BtnSelectNone_Click (object sender, EventArgs e) { for ( int i = 0 ; i < checkedListBoxTables.Items.Count; i++) { checkedListBoxTables.SetItemChecked(i, false ); } LogMessage( "已取消选择所有表。" ); } private async Task<List< string >> GetTableListAsync( string connectionString) { var tables = new List< string >(); const string sql = @ " SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME" ; using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); using (var command = new SqlCommand(sql, connection)) using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { tables.Add(reader.GetString( 0 )); } } } return tables; } #endregion #region 同步操作 private async void BtnStartSync_Click (object sender, EventArgs e) { if (!ValidateSettings()) return ; var selectedTables = GetSelectedTables(); if (selectedTables.Count == 0 ) { MessageBox.Show( "请至少选择一个要同步的表!" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Warning); return ; } var result = MessageBox.Show( $ "确定要同步 {selectedTables.Count} 个表吗?\n\n" + $ "同步选项:\n" + $ "- 同步结构:{(chkSyncStructure.Checked ? " 是 " : " 否 ")}\n" + $ "- 同步数据:{(chkSyncData.Checked ? " 是 " : " 否 ")}\n" + $ "- 创建目标表:{(chkCreateTargetTables.Checked ? " 是 " : " 否 ")}\n" + $ "- 清空目标表:{(chkTruncateTarget.Checked ? " 是 " : " 否 ")}\n" + $ "- 批次大小:{numericBatchSize.Value}" , "确认同步" , MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result != DialogResult.Yes) return ; await StartSyncProcess (selectedTables) ; } private void BtnCancel_Click (object sender, EventArgs e) { if (_cancellationTokenSource != null && !_cancellationTokenSource.Token.IsCancellationRequested) { _cancellationTokenSource.Cancel(); LogMessage( "用户取消了同步操作。" ); } } private bool ValidateSettings () { if ( string .IsNullOrWhiteSpace(txtSourceConnection.Text)) { MessageBox.Show( "请配置源数据库连接字符串!" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Warning); return false ; } if ( string .IsNullOrWhiteSpace(txtTargetConnection.Text)) { MessageBox.Show( "请配置目标数据库连接字符串!" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Warning); return false ; } if (!chkSyncStructure.Checked && !chkSyncData.Checked) { MessageBox.Show( "请至少选择一种同步选项(同步结构或同步数据)!" , "提示" , MessageBoxButtons.OK, MessageBoxIcon.Warning); return false ; } return true ; } private List< string > GetSelectedTables() { var selectedTables = new List< string >(); for ( int i = 0 ; i < checkedListBoxTables.Items.Count; i++) { if (checkedListBoxTables.GetItemChecked(i)) { selectedTables.Add(checkedListBoxTables.Items[i].ToString()); } } return selectedTables; } private async Task StartSyncProcess (List< string > tables) { _isSyncing = true ; _cancellationTokenSource = new CancellationTokenSource(); try { SetSyncMode( true ); progressBarMain.Maximum = tables.Count; progressBarMain.Value = 0 ; labelProgress.Text = $ "0/{tables.Count}" ; _sourceConnectionString = txtSourceConnection.Text; _targetConnectionString = txtTargetConnection.Text; LogMessage( "开始同步操作..." ); LogMessage($ "共需同步 {tables.Count} 个表。" ); var syncOptions = new SyncOptions { SyncStructure = chkSyncStructure.Checked, SyncData = chkSyncData.Checked, CreateTargetTables = chkCreateTargetTables.Checked, TruncateTargetTables = chkTruncateTarget.Checked, BatchSize = ( int )numericBatchSize.Value }; await SyncTablesAsync (tables, syncOptions, _cancellationTokenSource.Token) ; if (!_cancellationTokenSource.Token.IsCancellationRequested) { LogMessage( "同步操作完成!" ); MessageBox.Show( "同步操作完成!" , "成功" , MessageBoxButtons.OK, MessageBoxIcon.Information); } } catch (OperationCanceledException) { LogMessage( "同步操作已取消。" ); } catch (Exception ex) { LogMessage($ "同步操作发生错误:{ex.Message}" ); MessageBox.Show($ "同步操作发生错误:{ex.Message}" , "错误" , MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _isSyncing = false ; SetSyncMode( false ); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } } private async Task SyncTablesAsync (List< string > tables, SyncOptions options, CancellationToken cancellationToken) { for ( int i = 0 ; i < tables.Count; i++) { if (cancellationToken.IsCancellationRequested) break ; var tableName = tables[i]; LogMessage($ "正在同步表:{tableName} ({i + 1}/{tables.Count})" ); try { // 1. 同步表结构 if (options.SyncStructure && options.CreateTargetTables) { LogMessage($ "正在同步表 {tableName} 的结构..." ); await SynchronizeTableStructureAsync (tableName) ; } // 2. 同步数据 if (options.SyncData) { LogMessage($ "正在同步表 {tableName} 的数据..." ); await SynchronizeTableDataFullAsync (tableName) ; // 3. 验证同步结果 LogMessage($ "正在验证表 {tableName} 的同步结果..." ); bool isValid = await ValidateSyncResultAsync(tableName); LogMessage($ "表 {tableName} 同步验证结果:{(isValid ? " 成功 " : " 失败 ")}" ); } LogMessage($ "表 {tableName} 同步完成。" ); } catch (Exception ex) { LogMessage($ "表 {tableName} 同步失败:{ex.Message}" ); // 继续处理下一个表,不中断整个流程 } // 更新进度 UpdateProgress(i + 1 , tables.Count); } } // 您的同步方法 - 集成到类中 private async Task SynchronizeTableStructureAsync ( string tableName) { try { LogMessage($ "获取表 {tableName} 的列信息..." ); DataTable columnsSchema = await GetTableColumnsAsync(tableName); if (columnsSchema == null || columnsSchema.Rows.Count == 0 ) { LogMessage($ "警告: 表 {tableName} 没有列信息或不存在" ); return ; } LogMessage($ "获取到表 {tableName} 的 {columnsSchema.Rows.Count} 列信息" ); List< string > primaryKeys = await GetPrimaryKeysAsync(tableName); LogMessage($ "表 {tableName} 的主键列: {string.Join(" , ", primaryKeys)}" ); Dictionary< string , string > defaultConstraints = await GetDefaultConstraintsAsync(tableName); LogMessage($ "表 {tableName} 的默认值约束: {defaultConstraints.Count} 个" ); bool tableExists = await CheckTableExistsAsync(tableName); LogMessage($ "目标数据库中表 {tableName} {(tableExists ? " 已存在 " : " 不存在 ")}" ); using (SqlConnection targetConnection = new SqlConnection(_targetConnectionString)) { await targetConnection.OpenAsync(); using (SqlTransaction transaction = targetConnection.BeginTransaction()) { try { if (tableExists) { LogMessage($ "正在删除目标表 {tableName}" ); string dropTableSql = $ "DROP TABLE [{tableName}]" ; using (SqlCommand command = new SqlCommand(dropTableSql, targetConnection, transaction)) { await command.ExecuteNonQueryAsync(); } } StringBuilder createTableSql = new StringBuilder(); createTableSql.AppendLine($ "CREATE TABLE [{tableName}] (" ); List< string > columnDefinitions = new List< string >(); foreach (DataRow row in columnsSchema.Rows) { string columnName = row[ "COLUMN_NAME" ].ToString(); string dataType = row[ "DATA_TYPE" ].ToString(); string maxLength = row[ "CHARACTER_MAXIMUM_LENGTH" ] != DBNull.Value ? row[ "CHARACTER_MAXIMUM_LENGTH" ].ToString() : "" ; string isNullable = row[ "IS_NULLABLE" ] != DBNull.Value ? row[ "IS_NULLABLE" ].ToString() : "YES" ; string numericPrecision = row[ "NUMERIC_PRECISION" ] != DBNull.Value ? row[ "NUMERIC_PRECISION" ].ToString() : "" ; string numericScale = row[ "NUMERIC_SCALE" ] != DBNull.Value ? row[ "NUMERIC_SCALE" ].ToString() : "" ; StringBuilder columnDefinition = new StringBuilder(); columnDefinition.Append($ "[{columnName}] {dataType}" ); if (dataType.ToLower() == "varchar" || dataType.ToLower() == "nvarchar" || dataType.ToLower() == "char" || dataType.ToLower() == "nchar" ) { if (! string .IsNullOrEmpty(maxLength)) { if (maxLength == "-1" ) columnDefinition.Append( "(MAX)" ); else if (maxLength == "8000" ) { columnDefinition.Append($ "(4000)" ); } else { columnDefinition.Append($ "({maxLength})" ); } } } else if (dataType.ToLower() == "decimal" || dataType.ToLower() == "numeric" ) { if (! string .IsNullOrEmpty(numericPrecision) && ! string .IsNullOrEmpty(numericScale)) { columnDefinition.Append($ "({numericPrecision}, {numericScale})" ); } } columnDefinition.Append(isNullable == "YES" ? " NULL" : " NOT NULL" ); if (defaultConstraints.ContainsKey(columnName)) { columnDefinition.Append($ " DEFAULT {defaultConstraints[columnName]}" ); } columnDefinitions.Add(columnDefinition.ToString()); } if (columnDefinitions.Count == 0 ) { throw new Exception($ "表 {tableName} 没有有效的列定义" ); } createTableSql.AppendLine( string .Join( "," + Environment.NewLine, columnDefinitions)); if (primaryKeys.Count > 0 ) { createTableSql.AppendLine($ ", CONSTRAINT [PK_{tableName}] PRIMARY KEY (" ); createTableSql.AppendLine($ " [{string.Join(" ], [ ", primaryKeys)}]" ); createTableSql.AppendLine( ")" ); } createTableSql.AppendLine( ")" ); string finalSql = createTableSql.ToString(); using (SqlCommand command = new SqlCommand(finalSql, targetConnection, transaction)) { await command.ExecuteNonQueryAsync(); } transaction.Commit(); LogMessage($ "表 {tableName} 结构同步成功" ); } catch (Exception ex) { transaction.Rollback(); LogMessage($ "表 {tableName} 结构同步失败: {ex.Message}" ); throw ; } } } } catch (Exception ex) { LogMessage($ "处理表 {tableName} 时发生错误: {ex.Message}" ); throw ; } } private async Task<DataTable> GetTableColumnsAsync( string tableName) { DataTable schema = new DataTable(); try { using (SqlConnection connection = new SqlConnection(_sourceConnectionString)) { await connection.OpenAsync(); string query = @ " SELECT c.name AS COLUMN_NAME, t.name AS DATA_TYPE, CASE WHEN t.name IN ('nvarchar','nchar','varchar','char') THEN c.max_length ELSE NULL END AS CHARACTER_MAXIMUM_LENGTH, CASE WHEN c.is_nullable = 1 THEN 'YES' ELSE 'NO' END AS IS_NULLABLE, c.precision AS NUMERIC_PRECISION, c.scale AS NUMERIC_SCALE FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id INNER JOIN sys.tables tbl ON c.object_id = tbl.object_id WHERE tbl.name = @TableName ORDER BY c.column_id" ; SqlCommand command = new SqlCommand(query, connection); command.Parameters.AddWithValue( "@TableName" , tableName); SqlDataAdapter adapter = new SqlDataAdapter(command); adapter.Fill(schema); } } catch (Exception ex) { LogMessage($ "获取表 {tableName} 的列信息时出错: {ex.Message}" ); throw ; } return schema; } private async Task<List< string >> GetPrimaryKeysAsync( string tableName) { List< string > primaryKeys = new List< string >(); using (SqlConnection connection = new SqlConnection(_sourceConnectionString)) { await connection.OpenAsync(); string query = @ " SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + CONSTRAINT_NAME), 'IsPrimaryKey') = 1 AND TABLE_NAME = @TableName ORDER BY ORDINAL_POSITION" ; using (SqlCommand command = new SqlCommand(query, connection)) { command.Parameters.AddWithValue( "@TableName" , tableName); using (SqlDataReader reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { primaryKeys.Add(reader.GetString( 0 )); } } } } return primaryKeys; } private async Task<Dictionary< string , string >> GetDefaultConstraintsAsync( string tableName) { Dictionary< string , string > defaultConstraints = new Dictionary< string , string >(); using (SqlConnection connection = new SqlConnection(_sourceConnectionString)) { await connection.OpenAsync(); string query = @ " SELECT c.name AS ColumnName, dc.definition AS DefaultValue FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id WHERE t.name = @TableName" ; using (SqlCommand command = new SqlCommand(query, connection)) { command.Parameters.AddWithValue( "@TableName" , tableName); using (SqlDataReader reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { defaultConstraints.Add(reader.GetString( 0 ), reader.GetString( 1 )); } } } } return defaultConstraints; } private async Task< bool > CheckTableExistsAsync( string tableName) { using (SqlConnection connection = new SqlConnection(_targetConnectionString)) { await connection.OpenAsync(); string query = @ " SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName AND TABLE_TYPE = 'BASE TABLE'" ; using (SqlCommand command = new SqlCommand(query, connection)) { command.Parameters.AddWithValue( "@TableName" , tableName); int count = ( int )await command.ExecuteScalarAsync(); return count > 0 ; } } } private async Task SynchronizeTableDataFullAsync ( string tableName) { try { LogMessage($ "开始全量同步表 {tableName} 的数据..." ); using (SqlConnection sourceConnection = new SqlConnection(_sourceConnectionString)) { await sourceConnection.OpenAsync(); DataTable sourceData = new DataTable(); string selectQuery = $ "SELECT * FROM [{tableName}]" ; using (SqlCommand command = new SqlCommand(selectQuery, sourceConnection)) { command.CommandTimeout = 300 ; using (SqlDataAdapter adapter = new SqlDataAdapter(command)) { adapter.Fill(sourceData); } } LogMessage($ "从源数据库读取到 {sourceData.Rows.Count} 行数据" ); using (SqlConnection targetConnection = new SqlConnection(_targetConnectionString)) { await targetConnection.OpenAsync(); using (SqlTransaction transaction = targetConnection.BeginTransaction()) { try { LogMessage($ "正在清空目标表 {tableName} 的数据..." ); string truncateQuery = $ "DELETE FROM [{tableName}]" ; using (SqlCommand truncateCommand = new SqlCommand(truncateQuery, targetConnection, transaction)) { truncateCommand.CommandTimeout = 300 ; int deletedRows = await truncateCommand.ExecuteNonQueryAsync(); LogMessage($ "已清空目标表 {tableName},删除了 {deletedRows} 行数据" ); } if (sourceData.Rows.Count > 0 ) { LogMessage($ "正在向目标表 {tableName} 插入 {sourceData.Rows.Count} 行数据..." ); using (SqlBulkCopy bulkCopy = new SqlBulkCopy(targetConnection, SqlBulkCopyOptions.Default, transaction)) { bulkCopy.DestinationTableName = tableName; bulkCopy.BatchSize = ( int )numericBatchSize.Value; bulkCopy.BulkCopyTimeout = 600 ; bulkCopy.EnableStreaming = true ; foreach (DataColumn column in sourceData.Columns) { bulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName); } await bulkCopy.WriteToServerAsync(sourceData); LogMessage($ "成功向目标表 {tableName} 插入 {sourceData.Rows.Count} 行数据" ); } } else { LogMessage($ "源表 {tableName} 没有数据,跳过插入操作" ); } transaction.Commit(); LogMessage($ "表 {tableName} 全量数据同步完成" ); } catch (Exception ex) { transaction.Rollback(); LogMessage($ "表 {tableName} 数据同步失败,已回滚事务: {ex.Message}" ); throw ; } } } } } catch (Exception ex) { LogMessage($ "全量同步表 {tableName} 数据时出错: {ex.Message}" ); throw ; } } private async Task< bool > ValidateSyncResultAsync( string tableName) { try { int sourceCount = 0 ; int targetCount = 0 ; using (SqlConnection sourceConnection = new SqlConnection(_sourceConnectionString)) { await sourceConnection.OpenAsync(); string sourceCountQuery = $ "SELECT COUNT(*) FROM [{tableName}]" ; using (SqlCommand command = new SqlCommand(sourceCountQuery, sourceConnection)) { sourceCount = ( int )await command.ExecuteScalarAsync(); } } using (SqlConnection targetConnection = new SqlConnection(_targetConnectionString)) { await targetConnection.OpenAsync(); string targetCountQuery = $ "SELECT COUNT(*) FROM [{tableName}]" ; using (SqlCommand command = new SqlCommand(targetCountQuery, targetConnection)) { targetCount = ( int )await command.ExecuteScalarAsync(); } } LogMessage($ "表 {tableName} 同步验证: 源表 {sourceCount} 行,目标表 {targetCount} 行" ); bool isValid = sourceCount == targetCount; if (!isValid) { LogMessage($ "警告: 表 {tableName} 同步后行数不一致!" ); } return isValid; } catch (Exception ex) { LogMessage($ "验证表 {tableName} 同步结果时出错: {ex.Message}" ); return false ; } } #endregion #region 配置保存和加载 private void BtnSaveConfig_Click (object sender, EventArgs e) { try { using (var dialog = new SaveFileDialog()) { dialog.Filter = "配置文件|*.xml" ; dialog.Title = "保存配置文件" ; if (dialog.ShowDialog() == DialogResult.OK) { var config = new SyncConfig { SourceConnectionString = txtSourceConnection.Text, TargetConnectionString = txtTargetConnection.Text, SyncStructure = chkSyncStructure.Checked, SyncData = chkSyncData.Checked, CreateTargetTables = chkCreateTargetTables.Checked, TruncateTargetTables = chkTruncateTarget.Checked, BatchSize = ( int )numericBatchSize.Value, SelectedTables = GetSelectedTables() }; var serializer = new XmlSerializer(typeof(SyncConfig)); using (var writer = new StreamWriter(dialog.FileName)) { serializer.Serialize(writer, config); } MessageBox.Show( "配置保存成功!" , "成功" , MessageBoxButtons.OK, MessageBoxIcon.Information); LogMessage($ "配置已保存到:{dialog.FileName}" ); } } } catch (Exception ex) { MessageBox.Show($ "保存配置失败:{ex.Message}" , "错误" , MessageBoxButtons.OK, MessageBoxIcon.Error); LogMessage($ "保存配置失败:{ex.Message}" ); } } private void BtnLoadConfig_Click (object sender, EventArgs e) { try { using (var dialog = new OpenFileDialog()) { dialog.Filter = "配置文件|*.xml" ; dialog.Title = "加载配置文件" ; if (dialog.ShowDialog() == DialogResult.OK) { var serializer = new XmlSerializer(typeof(SyncConfig)); using (var reader = new StreamReader(dialog.FileName)) { var config = (SyncConfig)serializer.Deserialize(reader); txtSourceConnection.Text = config.SourceConnectionString; txtTargetConnection.Text = config.TargetConnectionString; chkSyncStructure.Checked = config.SyncStructure; chkSyncData.Checked = config.SyncData; chkCreateTargetTables.Checked = config.CreateTargetTables; chkTruncateTarget.Checked = config.TruncateTargetTables; numericBatchSize.Value = config.BatchSize; if (config.SelectedTables != null) { for ( int i = 0 ; i < checkedListBoxTables.Items.Count; i++) { var tableName = checkedListBoxTables.Items[i].ToString(); checkedListBoxTables.SetItemChecked(i, config.SelectedTables.Contains(tableName)); } } } MessageBox.Show( "配置加载成功!" , "成功" , MessageBoxButtons.OK, MessageBoxIcon.Information); LogMessage($ "配置已从文件加载:{dialog.FileName}" ); } } } catch (Exception ex) { MessageBox.Show($ "加载配置失败:{ex.Message}" , "错误" , MessageBoxButtons.OK, MessageBoxIcon.Error); LogMessage($ "加载配置失败:{ex.Message}" ); } } #endregion #region 其他事件 private void BtnExit_Click (object sender, EventArgs e) { this .Close(); } private void AboutToolStripMenuItem_Click (object sender, EventArgs e) { MessageBox.Show( "SQL Server 数据库同步工具 v1.0\n\n" + "功能特性:\n" + "- 支持完整的表结构同步(包括列定义、主键、默认值等)\n" + "- 支持全量数据同步\n" + "- 支持批量处理和事务回滚\n" + "- 支持同步结果验证\n" + "- 支持配置保存和加载\n" + "- 支持同步进度显示和详细日志\n\n" + "使用方法:\n" + "1. 配置源数据库和目标数据库连接字符串\n" + "2. 测试连接确保连接正常\n" + "3. 刷新并选择要同步的表\n" + "4. 配置同步选项\n" + "5. 开始同步操作" , "关于" , MessageBoxButtons.OK, MessageBoxIcon.Information); } private void FrmMain_FormClosing (object sender, FormClosingEventArgs e) { if (_isSyncing) { var result = MessageBox.Show( "同步操作正在进行中,确定要退出吗?" , "确认退出" , MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.No) { e.Cancel = true ; return ; } _cancellationTokenSource?.Cancel(); } } #endregion #region 辅助方法 private void SetControlsEnabled ( bool enabled) { if ( this .InvokeRequired) { if ( this .IsHandleCreated && ! this .IsDisposed) { try { this .Invoke( new Action(() => SetControlsEnabledInternal(enabled))); } catch (ObjectDisposedException) { return ; } catch (InvalidOperationException) { return ; } } } else { SetControlsEnabledInternal(enabled); } } private void SetControlsEnabledInternal ( bool enabled) { if (! this .IsDisposed) { btnTestSourceConnection.Enabled = enabled; btnTestTargetConnection.Enabled = enabled; btnRefreshTables.Enabled = enabled; btnSelectAll.Enabled = enabled; btnSelectNone.Enabled = enabled; btnStartSync.Enabled = enabled && !_isSyncing; btnSaveConfig.Enabled = enabled; btnLoadConfig.Enabled = enabled; } } private void SetSyncMode ( bool syncing) { if ( this .InvokeRequired) { if ( this .IsHandleCreated && ! this .IsDisposed) { try { this .Invoke( new Action(() => SetSyncModeInternal(syncing))); } catch (ObjectDisposedException) { return ; } catch (InvalidOperationException) { return ; } } } else { SetSyncModeInternal(syncing); } } private void SetSyncModeInternal ( bool syncing) { if (! this .IsDisposed) { _isSyncing = syncing; btnStartSync.Enabled = !syncing; btnCancel.Enabled = syncing; txtSourceConnection.Enabled = !syncing; txtTargetConnection.Enabled = !syncing; checkedListBoxTables.Enabled = !syncing; groupBoxOptions.Enabled = !syncing; toolStripProgressBar.Visible = syncing; toolStripStatusLabel.Text = syncing ? "正在同步..." : "就绪" ; } } private void UpdateProgress ( int current, int total) { if ( this .InvokeRequired) { if ( this .IsHandleCreated && ! this .IsDisposed) { try { this .Invoke( new Action(() => UpdateProgressInternal(current, total))); } catch (ObjectDisposedException) { return ; } catch (InvalidOperationException) { return ; } } } else { UpdateProgressInternal(current, total); } } private void UpdateProgressInternal ( int current, int total) { if (! this .IsDisposed) { progressBarMain.Value = current; labelProgress.Text = $ "{current}/{total}" ; toolStripProgressBar.Value = ( int )(( double )current / total * 100 ); } } private void LogMessage ( string message) { if ( this .InvokeRequired) { if ( this .IsHandleCreated && ! this .IsDisposed) { try { this .Invoke( new Action(() => LogMessageInternal(message))); } catch (ObjectDisposedException) { return ; } catch (InvalidOperationException) { return ; } } } else { LogMessageInternal(message); } } private void LogMessageInternal ( string message) { if (richTextBoxLog != null && !richTextBoxLog.IsDisposed) { var timestamp = DateTime.Now.ToString( "yyyy-MM-dd HH:mm:ss" ); richTextBoxLog.AppendText($ "[{timestamp}] {message}\n" ); richTextBoxLog.ScrollToCaret(); } } #endregion } #region 配置和选项类 [Serializable] public class SyncConfig { public string SourceConnectionString { get; set ; } public string TargetConnectionString { get; set ; } public bool SyncStructure { get; set ; } public bool SyncData { get; set ; } public bool CreateTargetTables { get; set ; } public bool TruncateTargetTables { get; set ; } public int BatchSize { get; set ; } public List< string > SelectedTables { get; set ; } } public class SyncOptions { public bool SyncStructure { get; set ; } public bool SyncData { get; set ; } public bool CreateTargetTables { get; set ; } public bool TruncateTargetTables { get; set ; } public int BatchSize { get; set ; } } #endregion }
⚡ 性能优化技巧 批量处理
流式传输
EnableStreaming=true减少内存占用 事务控制
超时设置
异步操作
🚨 常见坑点提醒 ⚠️ 字符串长度问题 :nvarchar(max)在某些情况下会变成nvarchar(4000)
⚠️ 主键约束 :删除表时要注意外键依赖关系
⚠️ 事务超时 :大数据量同步时适当增加超时时间
⚠️ 内存溢出 :超大表建议分批处理
🎯 总结 这个C#数据库同步工具帮你解决了三个核心问题:
自动化
安全性
高效性
SqlBulkCopy+批量处理,性能提升10倍以上 无论你是在做数据迁移、环境部署还是数据备份,这套代码都能为你节省大量时间。更重要的是,它是完全开源的,你可以根据自己的需求进行定制。
文章中源代码见:
附件:csharp-code-main.zip
阅读原文:原文链接
该文章在 2025/7/9 12:20:54 编辑过