首页 / 资讯中心 / 文章详情

WPF MVVM 项目数据库层落地:SqlSugar 集成与性能优化实战

WPF MVVM 项目数据库层落地:SqlSugar 集成与性能优化实战 ★ FEATURED ARTICLE
简介本资源是WPF结合MVVM模式与SqlSugar ORM的入门实战教程配套工程面向具备一定C#基础、希望掌握桌面端分层架构与数据库访问的开发者。内容围绕MySql数据库展开涵盖数据库类库创建、数据功能接口定义以及泛型方法实现帮助读者理解业务逻辑层与数据访问层的解耦思路并熟悉ViewModel中的数据绑定与命令处理。压缩包共1612个文件以549个dll、450个xml、52个cs源码、6个xaml界面文件及3个csproj工程文件为主另含nupkg依赖包与png示意图整体约123.95MB目录结构完整可直接还原项目环境。目前已有576人学习下载。通过该实例读者可获取一套可运行的WPFMVVMSqlSugar项目骨架掌握接口抽象、泛型复用与单元测试的落地方式为后续构建结构清晰、易维护的桌面应用提供参考。1. 从一次“能跑但不敢改”的 WPF 项目说起很多 WPF 项目死在第二周界面能点数据库能连但没人敢动代码。ViewModel 里塞着 SqlConnectionXAML 后台写着 SQL 字符串改一个字段要翻五个文件。这个标题要解决的正是这件事——用 MVVM 把界面和逻辑拆开用 SqlSugar 把数据库访问收口做成一个能长期维护的桌面项目骨架。它适合两类人刚学完 WPF 基础语法、想找一个真实项目练手的开发者以及手上有一堆 WinForm 老项目、准备迁到 WPF 的工程师。SqlSugar 在这里的角色不是“又一个 ORM”而是把增删改查、事务、连接池这些脏活从 ViewModel 里彻底剥离出去的那层。这一篇是系列第三部分重点落在数据库实例的完整落地前面讲过的界面骨架不再重复。2. 为什么 MVVM 项目里数据库层必须单独抽出来2.1 ViewModel 直接连数据库的三个真实代价先看一段很多人写过的代码。ViewModel 里直接 new 一个连接查询结果塞进 ObservableCollection界面上看着没问题// 反例ViewModel 里直接操作数据库 public class UserViewModel : INotifyPropertyChanged { public ObservableCollectionUser Users { get; set; } new(); public void LoadUsers() { using var conn new SqlConnection(Server.;DatabaseDemo;Trusted_ConnectionTrue;); conn.Open(); var cmd new SqlCommand(SELECT * FROM Users, conn); var reader cmd.ExecuteReader(); while (reader.Read()) { Users.Add(new User { Id (int)reader[Id], Name reader[Name].ToString() }); } } }这段代码能跑但代价在后面才显现。第一单元测试没法写因为 ViewModel 和真实数据库绑死了想测一个排序逻辑得先装一个 SQL Server。第二连接字符串散落在各个 ViewModel 里换一次环境要全局搜索替换。第三异步查询、事务、异常重试这些逻辑会在每个 ViewModel 里重复一遍改一处漏三处。MVVM 的核心不是“ViewModel 不能碰数据”而是“ViewModel 不应该知道数据从哪来”。它只关心拿到一个ListUser至于这个列表来自 SQL Server、SQLite 还是内存缓存是仓储层的事。2.2 SqlSugar 在分层里的位置和选型理由SqlSugar 是一个国产 ORM在中小型桌面项目里比 EF Core 更轻。它的几个特性正好对上 WPF 项目的痛点启动快没有 EF 那种首次查询编译模型的延迟支持 DbFirst 和 CodeFirst 两种模式老项目迁过来可以先映射现有表内置连接池和异步 API配合 WPF 的 async/await 不会卡 UI 线程。分层结构我一般这样切层职责依赖View (XAML)只做绑定和样式无ViewModel命令、状态、调用 ServiceService 接口Service业务规则、事务边界Repository 接口RepositorySqlSugar 的增删改查ISqlSugarClientEntity纯 POCO 类无关键点是 Repository 依赖ISqlSugarClient接口而不是具体实例这样测试时可以换成内存实现。SqlSugar 官方提供了SqlSugarScope它是线程安全的适合在 WPF 这种多线程环境里做单例。2.3 用 SqlSugarScope 注册全局客户端在App.xaml.cs里做一次性注册整个应用共用一个 Scope// App.xaml.cs public partial class App : Application { public static ISqlSugarClient Db { get; private set; } protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); Db new SqlSugarScope(new ConnectionConfig { ConnectionString Server.;DatabaseDemoDb;Trusted_ConnectionTrue;TrustServerCertificateTrue;, DbType DbType.SqlServer, IsAutoCloseConnection true, // 用完自动归还连接池 InitKeyType InitKeyType.Attribute // 用特性标记主键和自增 }, db { // 全局调试事件开发期打开上线关掉 db.Aop.OnLogExecuting (sql, pars) { Debug.WriteLine(sql); }; }); } }IsAutoCloseConnection true是桌面项目的必设项。WPF 应用不像 Web 有请求生命周期如果手动管理连接很容易在某个 ViewModel 里忘了 Close连接池耗尽后界面直接卡死。设成 true 后 SqlSugar 在每次操作结束自动归还连接。InitKeyType.Attribute表示主键信息从实体类的特性上读下面定义实体时会用到。Aop.OnLogExecuting是开发期的后悔药SQL 打出来一眼就能看出是参数传错还是索引没走。3. 实体映射与 CodeFirst 建库的完整步骤3.1 用特性标注实体类实体类保持纯净只加 SqlSugar 的特性不继承任何基类// Models/User.cs [SugarTable(Sys_User)] // 指定表名不写则用类名 public class User { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public int Id { get; set; } [SugarColumn(Length 50, IsNullable false)] public string UserName { get; set; } [SugarColumn(Length 100, IsNullable true)] public string Email { get; set; } [SugarColumn(ColumnDataType datetime, IsNullable true)] public DateTime? CreatedAt { get; set; } [SugarColumn(IsIgnore true)] // 不映射到数据库的字段 public string DisplayText ${UserName} ({Email}); }IsIdentity true告诉 SqlSugar 这个主键由数据库自增插入时不用赋值。IsIgnore true用在只给界面看的计算属性上避免 SqlSugar 尝试把它建成列。ColumnDataType在跨数据库时有用比如从 SQL Server 换到 SQLitedatetime 的写法不一样显式指定能减少迁移时的意外。3.2 CodeFirst 初始化数据库和种子数据在应用启动时调用一次初始化表不存在就建存在就跳过// Data/DbInitializer.cs public static class DbInitializer { public static void Init(ISqlSugarClient db) { // 建库如果数据库本身不存在 db.DbMaintenance.CreateDatabase(); // 建表泛型传入所有实体 db.CodeFirst.InitTables(typeof(User), typeof(Order)); // 种子数据只在表为空时插入 if (!db.QueryableUser().Any()) { db.Insertable(new ListUser { new User { UserName admin, Email admindemo.com, CreatedAt DateTime.Now }, new User { UserName guest, Email guestdemo.com, CreatedAt DateTime.Now } }).ExecuteCommand(); } } }CreateDatabase()只对 SQL Server、MySQL 这类支持 CREATE DATABASE 的库有效SQLite 会直接跳过。InitTables是幂等的重复调用不会报错它内部会对比表结构差异并尝试 ALTER但生产环境我建议关掉自动改表只用来首次建表。种子数据用Any()判断而不是Count() 0前者生成SELECT TOP 1在数据量大时快得多。这个细节在用户表上万行后能省下明显的启动时间。3.3 在 ViewModel 里通过仓储调用仓储接口定义在 Service 层之下ViewModel 只看到接口// Repositories/IUserRepository.cs public interface IUserRepository { TaskListUser GetAllAsync(); Taskint AddAsync(User user); Taskbool UpdateAsync(User user); Taskbool DeleteAsync(int id); } // Repositories/UserRepository.cs public class UserRepository : IUserRepository { private readonly ISqlSugarClient _db; public UserRepository(ISqlSugarClient db) _db db; public TaskListUser GetAllAsync() _db.QueryableUser().OrderBy(u u.Id).ToListAsync(); public Taskint AddAsync(User user) _db.Insertable(user).ExecuteReturnIdentityAsync(); public Taskbool UpdateAsync(User user) _db.Updateable(user).ExecuteCommandAsync().ContinueWith(t t.Result 0); public Taskbool DeleteAsync(int id) _db.DeleteableUser().Where(u u.Id id) .ExecuteCommandAsync().ContinueWith(t t.Result 0); }ExecuteReturnIdentityAsync在插入后返回自增主键省去再查一次。Updateable(user)默认按主键更新所有列如果只想更新部分字段用.UpdateColumns(u new { u.UserName, u.Email })指定能减少不必要的写入。ViewModel 里注入接口命令绑定到异步方法// ViewModels/UserListViewModel.cs public class UserListViewModel : ObservableObject { private readonly IUserRepository _repo; public ObservableCollectionUser Users { get; } new(); public IAsyncRelayCommand LoadCommand { get; } public UserListViewModel(IUserRepository repo) { _repo repo; LoadCommand new AsyncRelayCommand(LoadAsync); } private async Task LoadAsync() { var list await _repo.GetAllAsync(); Users.Clear(); foreach (var u in list) Users.Add(u); } }AsyncRelayCommand来自 CommunityToolkit.Mvvm它自动处理命令执行期间的 CanExecute 状态避免用户连点按钮触发多次查询。Users.Clear()后逐个 Add 而不是直接替换集合引用是因为界面已经绑定了这个集合实例替换引用不会触发刷新。4. 增删改查落地时最容易翻车的五个地方4.1 现象界面卡死三秒原因是没有异步现象是点击加载按钮后窗口无响应鼠标转圈。原因是用了同步的ToList()而不是ToListAsync()查询在 UI 线程上执行。解决是把所有仓储方法改成异步版本ViewModel 里用await调用。SqlSugar 的异步 API 底层是Task.Run包装不会阻塞 UI 线程。4.2 现象插入中文变问号原因是连接字符串缺字符集现象是插入“张三”后数据库里显示乱码。原因多半是 MySQL 连接串没指定CharSetutf8mb4或者 SQL Server 的列排序规则不是中文。解决是在连接字符串里显式加字符集参数建表时字符串列用nvarchar而不是varchar。SqlSugar 的[SugarColumn(Length 50)]默认生成 nvarchar但如果手动改过表结构就要检查。4.3 现象更新整行导致并发覆盖原因是没加版本列现象是两个用户同时编辑同一条记录后保存的覆盖了先保存的。原因是用Updateable(entity)全列更新没有乐观锁。解决是在实体里加一个[SugarColumn(IsEnableUpdateVersionValidation true)] public int Version { get; set; }SqlSugar 会在更新时自动带上版本条件版本不匹配则更新影响行数为零业务层据此提示用户刷新。4.4 现象连接池耗尽原因是 Scope 被重复创建现象是运行一段时间后报“超时时间已到但是未能从池中获取连接”。原因是在每个 ViewModel 里 new 了一个 SqlSugarScope每个 Scope 有自己的连接池。解决是全局只创建一个 Scope通过依赖注入或静态属性传递。WPF 里没有内置 DI 容器时用App.Db静态属性最简单项目大了再引入 Microsoft.Extensions.DependencyInjection。4.5 现象CodeFirst 把生产表删了原因是误用 InitTables现象是部署后表结构被改数据丢失。原因是InitTables在检测到列类型不匹配时会尝试 ALTER某些情况下会重建表。解决是生产环境禁用 CodeFirst改用 DbFirst 生成实体或者把InitTables包在#if DEBUG里。上线前用 SqlSugar 的DbMaintenance.GetColumnInfosByTableName做一次结构对比人工确认后再执行变更。5. 把查询性能压到 50ms 以内的三个进阶技巧5.1 用 Select 投影减少数据传输很多查询只需要两三个字段但QueryableUser().ToList()会把所有列拉回来。改成投影var dtos await _db.QueryableUser() .Where(u u.CreatedAt DateTime.Today.AddDays(-7)) .Select(u new UserDto { Id u.Id, UserName u.UserName }) .ToListAsync();生成的 SQL 只 SELECT 需要的列在宽表上能减少一半以上的网络传输。配合Mapper还能直接映射到 DTO省去手动赋值。5.2 分页查询必须带 OrderBySqlSugar 的分页ToPageListAsync(pageIndex, pageSize)如果不加 OrderBySQL Server 会报错SQLite 则返回不确定的顺序。正确写法是先 OrderBy 再分页var page await _db.QueryableUser() .OrderBy(u u.Id, OrderByType.Desc) .ToPageListAsync(pageIndex, pageSize);OrderByType.Desc在列表页很常用最新数据排前面。注意 pageIndex 从 1 开始不是 0这个和很多前端框架的习惯不一样传错会少一页数据。5.3 用缓存扛住高频只读查询用户列表这类变化不频繁的数据可以在仓储层加一层内存缓存private ListUser _cache; private DateTime _cacheTime; public async TaskListUser GetAllAsync() { if (_cache ! null (DateTime.Now - _cacheTime).TotalSeconds 30) return _cache; _cache await _db.QueryableUser().ToListAsync(); _cacheTime DateTime.Now; return _cache; }30 秒的过期时间对大多数桌面场景够用。写操作后手动把_cache置空保证下次读拿到新数据。这个模式比引入 Redis 之类的方案轻得多桌面应用没必要上分布式缓存。5.4 验证方法用 Aop 统计慢查询在OnLogExecuting里加一个计时超过阈值的 SQL 打到日志db.Aop.OnLogExecuting (sql, pars) { var sw Stopwatch.StartNew(); db.Aop.OnLogExecuted (sql2, pars2) { sw.Stop(); if (sw.ElapsedMilliseconds 100) Debug.WriteLine($[慢查询 {sw.ElapsedMilliseconds}ms] {sql2}); }; };这个习惯帮我抓过好几次问题一次是忘了加索引导致全表扫描一次是 N1 查询在循环里查关联表。SqlSugar 的Includes可以一次性把关联数据拉回来避免循环查询。我自己的习惯是每个新项目先把 Aop 日志和慢查询阈值配好再写业务代码。等出问题再回头加往往已经积累了一堆难查的慢 SQL。数据库层这东西前期多花半小时搭骨架后期能省下几十个小时的排查。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站