C# WinForm 数据库备份与恢复:T-SQL 与 SMO 选型避坑 简介围绕 C# WinForm 桌面应用中的数据库备份与恢复这份示例源码包面向需要掌握数据库数据保护思路的初中级开发者重点演示 SQLDMO 对象调用与直接执行备份、恢复语句两条实现路径。包内共 27 个文件含 7 个 cs 源码、2 个 exe 程序、2 个 txt 说明以及 dll、csproj、sln 等工程与资源文件压缩包约 582KB可直接对照解决方案梳理窗体交互、连接配置、备份文件路径设置与恢复覆盖选项。已有 502 人学习适合课程设计、小工具原型或既有桌面系统的数据保护功能参考示例围绕连接、命令构建与执行反馈组织代码便于替换数据库名和备份目录并延伸定时备份、日志记录、权限校验等需求。借助两种方案在部署依赖、控制粒度、异常处理和适用边界上的对比读者能较快形成可复用的实现思路与排错方向。1. 一个备份按钮背后的深夜电话C# WinForm 数据库备份与恢复要解决什么一个备份按钮背后的深夜电话往往不是段子C# WinForm 数据库备份与恢复 Demo 里最容易出问题的不是按钮没绑定事件而是备份文件路径、数据库占用和逻辑文件名这三件事同时找上门。某内部管理系统上线前做演练运维在客户端选了自己电脑上的D:\Backup\DemoDb.bak点恢复却一直报找不到文件因为 SQL Server 实例看到的是服务器端路径不是 WinForm 所在机器的路径。这套 Demo 把两种常见落地方式拆开一种用SqlCommand直接执行BACKUP DATABASE与RESTORE DATABASE适合轻量工具和需要 SQL 可审计的场景另一种用 SMO 的Backup、Restore对象把设备、进度、文件重定位收进对象模型适合要在 WinForm 里显示进度条、做多库管理的场景。它解决的是“把手工脚本变成可点击、有日志、能回显错误”的问题不是替代数据库高可用方案。适合谁正在给内部系统做运维小工具的 C# 开发者、需要把备份恢复能力交付给实施人员的项目组、刚接触 WinForm 数据库操作又不想在连接字符串和文件路径上反复踩坑的人。下面的顺序按“先选路线再落 T-SQL再落 SMO最后避坑与验证”走每一步都落到能抄的代码和参数上。2. 两条路线先分清T-SQL 脚本与 SMO 在 WinForm 里的选型边界选型不是口味问题。两种方式都能完成备份和恢复但失败时的可控性、部署成本和进度反馈差别很大。T-SQL 方式把动作写成 SQLWinForm 只负责传路径、执行命令、读消息SMO 方式把动作拆成对象代码更像在操作一个数据库管理模型。先把边界定清楚后面写按钮事件才不会来回改。2.1 T-SQL 脚本方式一条 BACKUP DATABASE 背后发生了什么BACKUP DATABASE不是把.mdf文件复制一份。SQL Server 做的是页级备份加日志一致性处理备份文件里包含备份头、文件列表、数据页和必要的日志信息。恢复时也不是把文件拷回去就完事要先读备份头里的逻辑文件名再用MOVE把数据文件和日志文件落到目标目录。很多“恢复成功但数据不对”的问题根子在于只恢复了数据文件日志文件还指向原路径或者MOVE的逻辑名写错了。T-SQL 方式的优点是依赖少一个SqlConnection连到master拼出BACKUP DATABASE [DemoDb] TO DISK backupFile WITH INIT, CHECKSUM, STATS 10就能跑。SQL 可读DBA 拿到日志也能判断问题。缺点是数据库名不能直接用参数占位必须做白名单校验再用方括号包起来进度只有STATS回显的文本WinForm 要做进度条得自己解析错误号要自己翻译成用户能看懂的话。我一般把 T-SQL 方式用在单库、低频、交付给少量运维人员的工具里。它的边界是不做备份计划、不做多设备备份、不做差异链管理只做“选一个库、选一个路径、备份或恢复”的闭环。超过这个范围代码里会出现大量字符串拼接和错误分支维护成本反而比 SMO 高。2.2 SMO 对象方式Backup 类与 Restore 类的适用边界SMO 是 SQL Server 的管理对象模型。Server代表实例Backup代表一次备份动作Restore代表一次恢复动作RelocateFile负责把逻辑文件重定位到新物理路径。它把 T-SQL 里那些容易拼错的参数收进了属性Backup.Action BackupActionType.Database、Backup.Initialize true、Restore.ReplaceDatabase true、Restore.NoRecovery false。代码读起来更像在描述动作而不是在拼 SQL。SMO 的优势在进度和文件列表。Backup.PercentComplete和Restore.PercentComplete能直接给百分比WinForm 的进度条不用去解析STATS文本Restore.ReadFileList(server)直接返回DataTable拿LogicalName、PhysicalName、Type比执行RESTORE FILELISTONLY再读结果集更顺。要做“恢复前先展示文件清单让用户改目标目录”的交互SMO 更省事。代价是部署。工程里要引用 SMO 程序集常见做法是在 NuGet 里搜SqlManagementObjects选与目标实例相近的版本再把依赖 dll 一起带到交付目录。SMO 对象不要跨线程共享SqlBackup和SqlRestore是同步阻塞调用放在 WinForm 的 UI 线程会假死得用Task.Run包起来。PercentComplete事件也不在 UI 线程直接改ProgressBar会翻车要用BeginInvoke切回去。选型建议很直接只做单库备份恢复、不想带额外 dll走 T-SQL要在界面上显示百分比、要读文件清单让用户改路径、要批量管理多个库走 SMO。两条路线可以并存Demo 里用两个按钮分别触发共用一个日志框和连接字符串后面排查时也好对比。2.3 在 WinForm 里搭一个最小可复现的实验台界面不用花哨控件够用就行。下面这张表是 Demo 里最小的控件清单重点看“关键点”那一列很多恢复失败不是代码问题是路径填错了。控件用途关键点cboDatabase选择要备份或恢复的库从sys.databases读不让用户手输txtBackupPath备份文件路径必须填数据库服务器可见的路径txtRestorePath恢复文件路径同上不是客户端本机路径txtDataDir/txtLogDir恢复目标目录服务器端目录必须已存在btnBackup/btnRestore执行按钮执行期间禁用防止重复点progressBar1进度反馈不确定进度时用MarqueetxtLog日志框跨线程更新用Invoke或BeginInvoke连接字符串放在配置文件里不要硬编码在按钮事件里。下面这段代码负责连master读数据库列表只显示非系统库。database_id 4是常见做法能过滤掉master、tempdb、model、msdb这几个系统库TrustServerCertificateTrue只在演示环境图省事正式环境应该走证书校验。// 连接字符串放 App.config界面启动时只读一次 private static readonly string MasterConn Serverlocalhost;Databasemaster;Integrated SecurityTrue;TrustServerCertificateTrue;; private void InitDatabaseList() { using var conn new SqlConnection(MasterConn); using var cmd new SqlCommand( SELECT name FROM sys.databases WHERE database_id 4 ORDER BY name, conn); conn.Open(); using var reader cmd.ExecuteReader(); while (reader.Read()) { cboDatabase.Items.Add(reader.GetString(0)); } }这段代码的逻辑是用MasterConn连到实例的master库查sys.databases拿用户库列表塞进ComboBox。参数说明Serverlocalhost换成实际实例名Integrated SecurityTrue表示走 Windows 身份验证如果走 SQL 账号就改成User Id和Passworddatabase_id 4是过滤系统库的常用边界但不是绝对安全交付前最好再维护一个排除名单。数据库名从下拉框来不让用户手输是为了避免把用户输入直接拼进 SQL。恢复目标库如果允许手输也要在后台做一次白名单校验只允许字母、数字和下划线。路径参数则统一用SqlParameter传不要把路径拼进 SQL 字符串。注意工具里所有备份文件路径、恢复目标目录都必须按“数据库服务器看到的路径”填。WinForm 在客户端跑SQL Server 在服务器跑两边看到的D:\不是同一个盘。3. T-SQL 方式落地BACKUP DATABASE 与 RESTORE FILELISTONLY 的命令链T-SQL 方式落地时不要在一个按钮里堆一整段 SQL 字符串。拆成三步备份、读文件清单、恢复。每步一个方法日志通过回调回传给 WinForm。这样恢复失败时能定位到“是备份没写成”“是文件清单没读到”还是“是 MOVE 参数不对”而不是只看到一句“恢复失败”。3.1 备份拼出带校验和进度回显的 BACKUP 语句数据库名不能用SqlParameter占位所以先做标识符校验再用方括号包起来。下面这个QuoteIdentifier只允许字母、数字和下划线遇到其他字符直接抛异常。它不是万能的但能挡住大部分误输入和拼接风险。private static string QuoteIdentifier(string name) { if (string.IsNullOrWhiteSpace(name) || !Regex.IsMatch(name, ^[\p{L}\p{N}_]$)) { throw new ArgumentException(数据库名只允许字母、数字和下划线); } return [ name.Replace(], ]]) ]; }逻辑说明Regex.IsMatch做白名单校验Replace(], ]])处理方括号转义。参数说明name来自下拉框或受控输入如果业务库名确实包含连字符或空格不要放宽正则后直接拼接应该改成从sys.databases查到的真实名称做比对再走方括号转义。这个方法的边界是“只接受常规标识符”不适合把所有合法 SQL 标识符都放进来。备份方法用SqlCommand执行BACKUP DATABASE路径用backupFile参数传CommandTimeout设为 0避免大库备份超过默认 30 秒被客户端掐断。InfoMessage事件用来接STATS 10回显的进度文本WinForm 可以把它追加到日志框。public static async Task BackupDatabaseAsync( string masterConn, string dbName, string backupFile, Actionstring log, CancellationToken ct default) { string db QuoteIdentifier(dbName); using var conn new SqlConnection(masterConn); await conn.OpenAsync(ct); conn.InfoMessage (_, e) { foreach (SqlError err in e.Errors) log?.Invoke(err.Message); }; using var cmd new SqlCommand($ BACKUP DATABASE {db} TO DISK backupFile WITH INIT, CHECKSUM, STATS 10;, conn); cmd.CommandTimeout 0; cmd.Parameters.Add(backupFile, SqlDbType.NVarChar, 4000).Value backupFile; await cmd.ExecuteNonQueryAsync(ct); }这段代码的逻辑是连master挂InfoMessage接进度执行带INIT、CHECKSUM、STATS的备份语句。参数说明masterConn连实例的master库dbName是要备份的库名先过QuoteIdentifierbackupFile是服务器端路径必须用参数传INIT表示覆盖同名备份文件CHECKSUM让备份过程写校验和STATS 10每 10% 回显一次CommandTimeout 0表示不设客户端超时但网络中断时命令可能挂起交付工具里要加日志和人工取消说明。备份路径的权限是第一个坑。SQL Server 服务账户要对目标目录有写权限如果路径是 UNC 共享服务账户还要有网络访问权限。常见做法是先在服务器上建一个专用备份目录给 SQL Server 服务账户读写权限再让 WinForm 只传这个目录下的文件名。不要指望客户端选一个本机路径就能备份成功。3.2 恢复前先读文件清单RESTORE FILELISTONLY 与 MOVE 参数恢复之前必须读备份文件里的逻辑文件列表。RESTORE FILELISTONLY只读备份头不会改数据库可以反复执行。它返回的LogicalName是恢复时MOVE的依据PhysicalName是备份时的原路径只能参考不能照搬。Type为D表示数据文件L表示日志文件。public sealed class BackupFileInfo { public string LogicalName { get; set; } ; public string PhysicalName { get; set; } ; public string Type { get; set; } ; // D数据L日志 } public static async TaskListBackupFileInfo ReadBackupFileListAsync( string masterConn, string backupFile, CancellationToken ct default) { var list new ListBackupFileInfo(); using var conn new SqlConnection(masterConn); await conn.OpenAsync(ct); using var cmd new SqlCommand( RESTORE FILELISTONLY FROM DISK backupFile, conn); cmd.CommandTimeout 0; cmd.Parameters.Add(backupFile, SqlDbType.NVarChar, 4000).Value backupFile; using var reader await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { list.Add(new BackupFileInfo { LogicalName reader[LogicalName].ToString() ?? , PhysicalName reader[PhysicalName].ToString() ?? , Type reader[Type].ToString() ?? }); } return list; }逻辑说明连master执行RESTORE FILELISTONLY把LogicalName、PhysicalName、Type读进列表。参数说明backupFile仍然是服务器端路径CommandTimeout 0防止大备份文件读头超时返回列表后恢复方法要按Type分别拼数据文件和日志文件的目标路径。目标文件名建议加库名前缀避免恢复到同一目录时覆盖原文件。读文件清单时常见的一个误判是“逻辑文件名等于数据库名”。不一定。逻辑文件名是建库时定的可能和库名完全不同。恢复时MOVE的第一个参数必须用LogicalName不是数据库名。把这两个混了SQL Server 会报“逻辑文件不受此数据库信任”或者直接找不到文件。3.3 恢复执行与 WinForm 事件绑定别让 UI 线程背锅恢复方法先读文件清单再拼MOVE子句。数据库名仍然走QuoteIdentifier备份路径走参数目标路径拼进 SQL 前做单引号转义。恢复前用SINGLE_USER WITH ROLLBACK IMMEDIATE踢掉现有连接恢复后用MULTI_USER放回来。生产库上这一步要谨慎交付工具里应该加确认框。public static async Task RestoreDatabaseAsync( string masterConn, string dbName, string backupFile, string dataDir, string logDir, Actionstring log, CancellationToken ct default) { string db QuoteIdentifier(dbName); var files await ReadBackupFileListAsync(masterConn, backupFile, ct); var moves new StringBuilder(); foreach (var f in files) { string dir f.Type L ? logDir : dataDir; string fileName dbName _ Path.GetFileName(f.PhysicalName); string target Path.Combine(dir, fileName); moves.AppendLine( $, MOVE N{f.Log p a hrefhttps://download.csdn.net/download/liulu132/5900429 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p