☰
C# WinForm 实现 SQL Server 数据库备份与恢复:T-SQL 与 SMO 两种方案详解
2026/10/21 8:32:41 网站建设 项目流程

简介:这份资源是面向C# WinForm开发者的数据库备份与恢复实战Demo,基于VS2008实现,适合需要为桌面应用增加数据安全保障能力的初中级开发者参考。示例围绕两种主流方案展开:一是借助SQLDMO这一COM对象模型,通过SQLServer、Database、Backup与Restore对象完成备份与恢复,控制粒度更细、错误处理更完善;二是直接执行Transact-SQL语句,用BACKUP DATABASE与RESTORE DATABASE命令快速完成操作,代码更简洁,适合轻量场景。压缩包共27个文件,约582KB,以cs源码、exe可执行程序、resx与resources资源文件、dll与pdb调试文件为主,另含sln解决方案、csproj工程文件及说明文档,结构完整可直接编译运行。目前已有501人学习下载。通过研读源码,读者可掌握连接数据库、构建SQL语句或调用SQLDMO对象、处理界面交互等关键环节,并对比两种方案的适用边界,为实际项目中的备份恢复模块提供可复用的实现思路。

1. 从一次误删事故说起:这个 C# WinForm 备份恢复 Demo 到底能干什么

某公司内部有个用了三年的物料台账系统,SQL Server 后端,前端就是典型的 C# WinForm。某天下午运维在清理磁盘时手滑删掉了数据库的 .mdf 文件,整个仓库的出入库记录瞬间归零。事后复盘发现,这套系统上线以来从来没做过一次完整备份——不是不想做,是没人知道怎么在 WinForm 里把备份恢复这套东西做对。这个 Demo 就是冲着这个场景来的:它用 C# WinForm 封装了 SQL Server 数据库的备份与恢复功能,并且给了两种实现方式,一种走 T-SQL 原生语句,一种走 SMO 对象模型。适合谁?适合手里有 WinForm 项目、数据库是 SQL Server、需要给客户或内部运维提供一个「一键备份/一键恢复」按钮的开发者。你不需要从零研究 SqlBackup 类怎么调,照着这个 Demo 把参数改一改就能嵌进自己的项目里。

2. 两种备份恢复方式的技术选型:T-SQL 与 SMO 到底怎么选

2.1 T-SQL 方式:直接执行 BACKUP/RESTORE 语句

T-SQL 方式的核心思路很简单:在 C# 里拼一条BACKUP DATABASE或RESTORE DATABASE语句,通过SqlCommand丢给 SQL Server 执行。这是最轻量的做法,不需要额外引用任何程序集,只要项目里能连上数据库就行。

先看备份的代码实现:

// 备份数据库:拼 T-SQL 语句,指定备份文件路径 public bool BackupDatabase(string dbName, string backupFilePath) { // 注意:dbName 和 backupFilePath 不能直接拼进 SQL,这里用参数化处理 // 但 BACKUP DATABASE 的数据库名不支持参数化,需要做白名单校验 if (!IsValidDbName(dbName)) return false; string sql = $@"BACKUP DATABASE [{dbName}] TO DISK = @backupPath WITH FORMAT, INIT, NAME = @backupName, STATS = 10"; using (SqlConnection conn = new SqlConnection(_masterConnStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.CommandTimeout = 0; // 大库备份可能超时,设为不限制 cmd.Parameters.AddWithValue("@backupPath", backupFilePath); cmd.Parameters.AddWithValue("@backupName", $"{dbName}-Full-{DateTime.Now:yyyyMMddHHmmss}"); conn.Open(); cmd.ExecuteNonQuery(); // 执行备份 } } return true; }

这段代码有几个关键点。第一,连接字符串必须指向master库,不能指向你要备份的那个库本身,否则会报「数据库正在使用」的错误。第二,CommandTimeout要设为 0,因为大数据库备份可能跑几分钟甚至更久,默认 30 秒根本不够。第三,WITH FORMAT, INIT表示覆盖现有备份集并重新初始化介质,适合每次全量备份的场景。第四,STATS = 10会让 SQL Server 每完成 10% 就返回一条进度消息,如果你要做进度条,可以订阅InfoMessage事件来捕获。

恢复的代码稍微复杂一些,因为恢复时数据库不能被占用:

// 恢复数据库:先切到 master,再强制断开目标库的连接 public bool RestoreDatabase(string dbName, string backupFilePath) { if (!IsValidDbName(dbName)) return false; // 第一步:将数据库设为单用户模式,踢掉所有连接 string singleUserSql = $@"IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @dbName) BEGIN ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; END"; // 第二步:执行 RESTORE string restoreSql = $@"RESTORE DATABASE [{dbName}] FROM DISK = @backupPath WITH REPLACE, RECOVERY, STATS = 10"; using (SqlConnection conn = new SqlConnection(_masterConnStr)) { conn.Open(); // 先踢连接 using (SqlCommand cmd1 = new SqlCommand(singleUserSql, conn)) { cmd1.Parameters.AddWithValue("@dbName", dbName); cmd1.ExecuteNonQuery(); } // 再恢复 using (SqlCommand cmd2 = new SqlCommand(restoreSql, conn)) { cmd2.CommandTimeout = 0; cmd2.Parameters.AddWithValue("@backupPath", backupFilePath); cmd2.ExecuteNonQuery(); } // 第三步:恢复多用户模式 string multiUserSql = $@"ALTER DATABASE [{dbName}] SET MULTI_USER"; using (SqlCommand cmd3 = new SqlCommand(multiUserSql, conn)) { cmd3.ExecuteNonQuery(); } } return true; }

恢复逻辑里最容易翻车的地方是「数据库正在使用」这个报错。很多人只写了RESTORE语句,结果执行时发现连接池里还有活跃连接,直接失败。所以必须先SET SINGLE_USER WITH ROLLBACK IMMEDIATE,它会强制回滚所有未完成事务并断开连接。恢复完成后再切回MULTI_USER,否则应用程序连不上。

2.2 SMO 方式:用 Microsoft.SqlServer.Management.Smo 对象模型

SMO 是 SQL Server 提供的一套 .NET 管理对象库,把备份恢复封装成了对象和方法调用,不用自己拼 SQL。用 SMO 的好处是代码可读性更强,而且能拿到更细粒度的进度百分比。

先要在项目里引用两个程序集:Microsoft.SqlServer.ConnectionInfo.dll和Microsoft.SqlServer.Smo.dll。这两个 DLL 通常在 SQL Server 安装目录的 SDK 文件夹里,或者通过 NuGet 包Microsoft.SqlServer.SqlManagementObjects安装。

备份的 SMO 写法:

using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; public bool BackupBySmo(string dbName, string backupFilePath) { ServerConnection conn = new ServerConnection(_serverIp, _user, _password); Server server = new Server(conn); Database db = server.Databases[dbName]; Backup backup = new Backup(); backup.Action = BackupActionType.Database; // 全量备份 backup.Database = dbName; backup.Devices.AddDevice(backupFilePath, DeviceType.File); backup.Initialize = true; // 覆盖现有备份集 backup.Checksum = true; // 启用校验和 backup.ContinueAfterError = false; // 进度回调:每完成 1% 触发一次 backup.PercentComplete += (sender, e) => { int percent = e.Percent; // 这里可以更新 UI 进度条,注意跨线程 UpdateProgress(percent); }; backup.SqlBackup(server); // 执行备份 return true; }

SMO 的PercentComplete事件是它比 T-SQL 方式更实用的地方。T-SQL 的STATS只能按固定间隔返回消息,而 SMO 能拿到精确的百分比。但要注意,这个事件是在后台线程触发的,更新 WinForm 控件时必须Invoke回 UI 线程,否则会抛跨线程异常。

恢复的 SMO 写法:

public bool RestoreBySmo(string dbName, string backupFilePath) { ServerConnection conn = new ServerConnection(_serverIp, _user, _password); Server server = new Server(conn); Restore restore = new Restore(); restore.Action = RestoreActionType.Database; restore.Database = dbName; restore.Devices.AddDevice(backupFilePath, DeviceType.File); restore.ReplaceDatabase = true; // 等价于 WITH REPLACE // 关键:设置恢复后的文件路径,避免原路径不存在导致失败 restore.RelocateFiles.Add(new RelocateFile(dbName, Path.Combine(_dataDir, $"{dbName}.mdf"))); restore.RelocateFiles.Add(new RelocateFile($"{dbName}_log", Path.Combine(_dataDir, $"{dbName}_log.ldf"))); restore.PercentComplete += (sender, e) => { UpdateProgress(e.Percent); }; // 先杀掉目标库的所有连接 server.KillAllProcesses(dbName); restore.SqlRestore(server); return true; }

SMO 恢复里有个很隐蔽的坑:如果备份文件来自另一台机器,原数据库的文件路径可能在新机器上不存在,RESTORE会直接报错。所以要用RelocateFiles把数据文件和日志文件重定向到当前服务器的有效路径。这个操作在 T-SQL 里对应MOVE ... TO ...子句,但 SMO 把它封装成了集合操作,写起来更直观。

2.3 两种方式的对比与选型建议

对比维度T-SQL 方式SMO 方式
额外依赖无,只需 SqlClient需引用 SMO 程序集
进度反馈只能靠 STATS 消息精确百分比事件
文件重定位手写 MOVE 子句RelocateFiles 集合
部署复杂度低,单文件即可需携带多个 DLL
适合场景轻量工具、快速集成功能完整的管理端

我一般会这样选:如果只是给内部运维做个简单的备份按钮,T-SQL 方式足够了,部署时不用操心 DLL 版本问题。如果要做成一个完整的数据库管理工具,带进度条、带日志、带文件重定位,那就上 SMO。Demo 里两种都给了,你可以根据项目实际情况挑一种嵌进去。

3. WinForm 界面与备份恢复逻辑的对接:从按钮到进度条

3.1 界面控件布局与事件绑定

Demo 的界面很朴素:一个文本框显示数据库连接信息,一个下拉框选数据库名,一个按钮触发备份,一个按钮触发恢复,底部一个进度条加一个日志文本框。重点不是界面多好看,而是逻辑怎么串起来。

备份按钮的点击事件大概长这样:

private async void btnBackup_Click(object sender, EventArgs e) { string dbName = cboDatabases.SelectedItem?.ToString(); if (string.IsNullOrEmpty(dbName)) { MessageBox.Show("请先选择数据库"); return; } // 弹出保存对话框,让用户选备份文件存放位置 using (SaveFileDialog sfd = new SaveFileDialog()) { sfd.Filter = "备份文件|*.bak"; sfd.FileName = $"{dbName}_{DateTime.Now:yyyyMMddHHmmss}.bak"; if (sfd.ShowDialog() != DialogResult.OK) return; btnBackup.Enabled = false; // 防止重复点击 txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] 开始备份 {dbName}...\r\n"); try { // 用 Task.Run 把备份放到后台线程,避免卡死 UI bool result = await Task.Run(() => BackupDatabase(dbName, sfd.FileName)); txtLog.AppendText(result ? $"[{DateTime.Now:HH:mm:ss}] 备份完成\r\n" : $"[{DateTime.Now:HH:mm:ss}] 备份失败\r\n"); } catch (Exception ex) { txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] 异常:{ex.Message}\r\n"); } finally { btnBackup.Enabled = true; } } }

这里用async/await加Task.Run是关键。备份大库时如果直接在 UI 线程执行,整个窗口会假死,用户以为程序崩了就直接杀进程,备份中断还可能留下损坏的备份文件。放到后台线程后,UI 保持响应,进度条也能正常刷新。

3.2 进度反馈与日志输出的线程安全处理

前面提到 SMO 的PercentComplete事件在后台线程触发,直接更新 ProgressBar 会抛异常。正确的做法是用Invoke或BeginInvoke切回 UI 线程:

private void UpdateProgress(int percent) { if (progressBar1.InvokeRequired) { // 跨线程时通过委托回到 UI 线程 progressBar1.BeginInvoke(new Action<int>(UpdateProgress), percent); } else { progressBar1.Value = Math.Min(percent, 100); lblPercent.Text = $"{percent}%"; } }

日志文本框也一样,所有AppendText调用都要走这个模式。我见过有人图省事直接开CheckForIllegalCrossThreadCalls = false,当时不报错了,但后面 UI 随机崩溃,排查了半天才定位到是跨线程访问控件导致的。这个属性只是把异常吞掉了,不是解决问题。

3.3 连接字符串的构造与数据库列表加载

Demo 里连接字符串是动态拼的,因为要支持用户输入服务器地址和账号:

// 构造连接字符串,注意 master 库用于备份恢复操作 private string BuildConnStr(string database = "master") { SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = txtServer.Text.Trim(); // 服务器地址 builder.InitialCatalog = database; // 默认连 master builder.UserID = txtUser.Text.Trim(); builder.Password = txtPwd.Text; builder.ConnectTimeout = 15; // 连接超时 15 秒 builder.TrustServerCertificate = true; // 开发环境跳过证书验证 return builder.ConnectionString; } // 加载数据库列表到下拉框 private void LoadDatabases() { cboDatabases.Items.Clear(); using (SqlConnection conn = new SqlConnection(BuildConnStr())) { conn.Open(); // 排除系统库,只显示用户数据库 string sql = "SELECT name FROM sys.databases WHERE database_id > 4"; using (SqlCommand cmd = new SqlCommand(sql, conn)) using (SqlDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) { cboDatabases.Items.Add(reader.GetString(0)); } } } }

用SqlConnectionStringBuilder而不是手拼字符串,是为了避免密码里有特殊字符时连接串解析出错。database_id > 4这个条件过滤掉了 master、tempdb、model、msdb 四个系统库,防止用户误操作备份系统库。

4. 避坑指南:备份恢复 Demo 落地时最容易翻车的五个地方

4.1 备份文件路径权限不足

现象:执行备份时报「操作系统错误 5(拒绝访问)」。

原因:SQL Server 服务账号对备份目标文件夹没有写权限。很多人把备份路径设成桌面或某个用户目录,但 SQL Server 是以服务账号运行的,跟当前登录用户不是一回事。

解决:把备份路径设成 SQL Server 默认备份目录,或者给NT SERVICE\MSSQLSERVER账号授予目标文件夹的写权限。在代码里可以加一个路径合法性检查,提前提示用户。

4.2 恢复时数据库仍被占用

现象:RESTORE执行后报「因为数据库正在使用,所以无法获得对数据库的独占访问权」。

原因:连接池里还有未释放的连接,或者有其他程序正在访问该库。

解决:恢复前必须执行ALTER DATABASE [xxx] SET SINGLE_USER WITH ROLLBACK IMMEDIATE。注意ROLLBACK IMMEDIATE会强制回滚未提交事务,生产环境要确认没有关键业务正在写入。如果用的是 SMO,调用server.KillAllProcesses(dbName)效果一样。

4.3 备份文件版本不兼容

现象:在高版本 SQL Server 上备份的文件,拿到低版本上恢复时报「数据库版本不兼容」。

原因:SQL Server 不支持从高版本备份恢复到低版本实例,这是硬限制。

解决:没有绕过的方法。要么升级目标服务器版本,要么用导出脚本/BCP 等方式迁移数据。在 Demo 里可以在恢复前检查备份文件的版本信息,提前给出提示而不是等报错。

4.4 大数据库备份导致 UI 假死

现象:点击备份按钮后窗口无响应,用户以为程序卡死。

原因:备份操作在 UI 线程同步执行,阻塞了消息循环。

解决:用Task.Run把备份逻辑放到后台线程,配合async/await保持按钮状态可控。同时把CommandTimeout设为 0,避免大库备份超时中断。

4.5 恢复后文件路径不存在

现象:在 A 机器备份,拿到 B 机器恢复时报「找不到文件路径」。

原因:备份文件里记录了原数据库的数据文件和日志文件的绝对路径,B 机器上可能没有对应目录。

解决:恢复时用MOVE子句(T-SQL)或RelocateFiles(SMO)把文件重定向到目标机器的有效路径。Demo 里默认重定向到 SQL Server 的 DATA 目录,你也可以让用户在界面上手动指定。

5. 进阶技巧:把备份恢复做成可配置的自动化任务

Demo 给的是手动触发的方式,但实际项目里更常见的是定时自动备份。我一般会在 WinForm 里加一个简单的定时器配置,让用户设定每天几点备份、保留多少份历史文件。核心逻辑不复杂:

// 清理过期备份:只保留最近 N 份 private void CleanOldBackups(string backupDir, int keepCount) { var files = new DirectoryInfo(backupDir) .GetFiles("*.bak") .OrderByDescending(f => f.CreationTime) .Skip(keepCount) // 跳过最新的 N 份 .ToList(); foreach (var file in files) { try { file.Delete(); txtLog.AppendText($"已清理过期备份:{file.Name}\r\n"); } catch (Exception ex) { txtLog.AppendText($"清理失败:{file.Name},{ex.Message}\r\n"); } } }

这个清理逻辑配合System.Windows.Forms.Timer或者System.Threading.Timer就能实现自动轮转。注意System.Windows.Forms.Timer的回调在 UI 线程执行,适合做轻量操作;备份本身还是要丢到后台线程去跑。

另一个实用技巧是备份文件的校验。备份完成后不要直接信任文件,用RESTORE VERIFYONLY验证一下:

// 验证备份文件是否完整可恢复 private bool VerifyBackup(string backupFilePath) { string sql = "RESTORE VERIFYONLY FROM DISK = @path"; using (SqlConnection conn = new SqlConnection(BuildConnStr())) { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@path", backupFilePath); try { cmd.ExecuteNonQuery(); return true; // 验证通过 } catch { return false; // 备份文件损坏 } } } }

这个验证花不了几秒钟,但能避免「备份文件存在但恢复不了」的尴尬。从那以后我每次做完备份工具都强制走一遍VERIFYONLY,确认文件真的能用才放心。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询