作为一名C#开发者,我经常遇到这样的场景:通过VPN访问内网的SQL Server,然后需要将数据同步到外网数据库。手动操作?太低效!写脚本?容易出错!
今天我要分享一个完整的C# WinForms数据库同步工具,不仅支持表结构同步,还能进行批量数据同步,关键是代码完全开源,拿来就能用!
在企业级应用中,数据库同步是个老大难问题:
我们的C#同步工具具备以下核心功能:
✅ 智能表结构同步:自动识别列类型、主键、默认值
✅ 高效批量数据传输:使用SqlBulkCopy提升性能
✅ 事务保护:确保数据一致性
✅ 进度监控:实时显示同步状态
✅ 配置管理:支持保存和加载同步配置
C#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;
}
}
C#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;
}
}
C#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;
}
C#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); // 🔓 解锁界面
}
}
C#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);
}
}
C#[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);
}
}
C#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
}
⚠️ 字符串长度问题:nvarchar(max)在某些情况下会变成nvarchar(4000)
⚠️ 主键约束:删除表时要注意外键依赖关系
⚠️ 事务超时:大数据量同步时适当增加超时时间
⚠️ 内存溢出:超大表建议分批处理
这个C#数据库同步工具帮你解决了三个核心问题:
无论你是在做数据迁移、环境部署还是数据备份,这套代码都能为你节省大量时间。更重要的是,它是完全开源的,你可以根据自己的需求进行定制。
💡 问题讨论:你在做数据库同步时遇到过哪些棘手问题?欢迎在评论区分享你的经验!
🔗 技术交流:想要完整的项目代码或者有其他C#开发问题?私信我获取更多资源!
觉得有用请转发给更多同行 🚀
相关信息
通过网盘分享的文件:AppSqlServerSynTool.zip 链接: https://pan.baidu.com/s/1Vg23SruBWtRpGiatGzKzng?pwd=5vda 提取码: 5vda --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!