// ============================================================================ // 【文件说明】HwPortalMyBatisExecutor.cs - MyBatis 风格 SQL 执行器 // ============================================================================ // 这个类是"类 MyBatis"架构的核心执行器,负责把 XML 中定义的 SQL 转换为真实的数据库操作。 // // 【架构定位】 // 在 Java MyBatis 中: // SqlSession sqlSession = sqlSessionFactory.openSession(); // UserMapper mapper = sqlSession.getMapper(UserMapper.class); // User user = mapper.selectById(1L); // // 在这个 C# 实现中: // HwPortalMyBatisExecutor executor = ...; // 由 DI 容器注入 // User user = await executor.QuerySingleAsync("UserMapper", "selectById", new { id = 1L }); // // 两者区别: // - Java MyBatis 用动态代理生成 Mapper 接口的实现 // - 这里用执行器模式,直接通过字符串名称调用,更简单但不够类型安全 // // 【为什么需要这个类?】 // 1. 兼容迁移:从 Java 迁移过来的项目有大量 XML SQL 定义 // 2. 复杂 SQL:有些 SQL 用 Lambda/LINQ 很难表达,原生 SQL 更直接 // 3. 动态条件:MyBatis 的 标签支持动态 SQL 组装 // ============================================================================ namespace Admin.NET.Plugin.HwPortal; /// /// MyBatis 风格 SQL 执行器。 /// /// 【C# 语法知识点 - sealed 密封类】 /// sealed 关键字表示"密封类",不能被继承。 /// /// 为什么要密封? /// 1. 安全性:防止子类篡改核心逻辑 /// 2. 性能:编译器可以对密封类做优化(如方法内联) /// 3. 设计意图:这个类就是最终实现,不需要扩展 /// /// 对比 Java: /// Java 用 final 关键字:public final class MyBatisExecutor { ... } /// C# 用 sealed 关键字:public sealed class HwPortalMyBatisExecutor { ... } /// /// /// 【依赖注入设计】 /// 构造函数接收两个依赖: /// - ISqlSugarClient:SqlSugar ORM 的数据库客户端 /// - HwPortalMapperRegistry:XML Mapper 的注册表(负责解析 SQL) /// /// 这是"依赖倒置原则"的体现:高层模块(执行器)不依赖低层模块(具体数据库实现), /// 而是依赖抽象(接口)。 /// /// public sealed class HwPortalMyBatisExecutor { // 【C# 语法知识点 - readonly 字段】 // readonly 表示"只读字段",只能在构造函数中赋值,之后不能修改。 // // 为什么用 readonly? // 1. 防止意外修改:数据库客户端不应该被替换 // 2. 线程安全:readonly 字段天然线程安全 // 3. 依赖注入的最佳实践:注入的依赖通常都应该是 readonly // // 对比 Java: // Java 用 final 关键字:private final SqlSession sqlSession; // C# 用 readonly 关键字:private readonly ISqlSugarClient _db; // // ISqlSugarClient 是 SqlSugar ORM 的数据库客户端抽象。 private readonly ISqlSugarClient _db; // 【命名约定】 // _registry 以下划线开头,表示私有字段。 // 这是 C# 的常见命名规范(尤其是依赖注入的字段)。 // // Registry 是"注册表"模式: // - 负责把 XML 中定义的 SQL 语句解析成可执行的 SQL // - 缓存解析结果,避免每次执行都重新解析 XML // - 处理参数替换(#{paramName} -> @paramName) private readonly HwPortalMapperRegistry _registry; /// /// 构造函数(依赖注入)。 /// /// 【C# 语法知识点 - 构造函数】 /// 构造函数是创建对象时自动调用的特殊方法,负责初始化对象状态。 /// /// 语法特点: /// - 方法名必须与类名相同 /// - 没有返回值(连 void 都不写) /// - 可以重载(多个参数不同的构造函数) /// /// 对比 Java: /// Java 的构造函数语法几乎一样: /// public HwPortalMyBatisExecutor(ISqlSugarClient db, HwPortalMapperRegistry registry) { /// this.db = db; /// this.registry = registry; /// } /// /// C# 的区别: /// - 可以用 this 简化:this._db = db;(但 C# 更常用 _db = db; 省略 this) /// - Java 习惯用 this.db = db; 区分成员变量和参数 /// /// /// 【依赖注入容器的工作流程】 /// 1. 应用启动时,Startup.cs 中注册服务: /// services.AddScoped(); /// services.AddSingleton(); /// /// 2. 当某个类(如 HwWebService)需要 HwPortalMyBatisExecutor 时: /// public HwWebService(HwPortalMyBatisExecutor executor) { ... } /// /// 3. DI 容器检查 HwPortalMyBatisExecutor 的构造函数需要什么: /// - 需要 ISqlSugarClient -> 容器创建/获取实例 /// - 需要 HwPortalMapperRegistry -> 容器创建/获取实例(因为是 Singleton,用已有的) /// /// 4. 容器调用这个构造函数,传入所需的依赖 /// /// 5. 返回创建好的 HwPortalMyBatisExecutor 实例给请求者 /// /// 整个过程是"递归解析依赖":A 依赖 B,B 依赖 C,容器会自动按顺序创建 C->B->A。 /// /// /// SqlSugar 数据库客户端(由 DI 容器提供) /// Mapper 注册表(由 DI 容器提供) public HwPortalMyBatisExecutor(ISqlSugarClient db, HwPortalMapperRegistry registry) { _db = db; _registry = registry; } /// /// 查询列表(返回多条记录)。 /// /// 【C# 语法知识点 - 泛型方法 <T>】 /// public Task<List<T>> QueryListAsync<T>(...) /// /// 这里的 <T> 是泛型参数,表示"这个方法可以处理任何类型"。 /// /// 调用示例: /// List<HwProductInfo> products = await executor.QueryListAsync<HwProductInfo>( /// "HwProductInfoMapper", /// "selectList", /// new { categoryId = 1 } /// ); /// /// 类型推断简化: /// var products = await executor.QueryListAsync<HwProductInfo>(...); /// // 或者直接: /// List<HwProductInfo> products = await executor.QueryListAsync(...); /// // 编译器会根据赋值目标推断 T 是 HwProductInfo /// /// 对比 Java: /// Java 的泛型方法写法: /// public <T> List<T> queryList(String mapperName, String statementId, Object parameter) { ... } /// /// 关键差异: /// - Java 的泛型有"类型擦除",运行时 List<T> 只是 List,T 变成了 Object /// - C# 的泛型是"真实泛型",运行时 T 就是具体的类型,性能更好 /// /// /// 【C# 语法知识点 - 默认参数值】 /// object parameter = null 表示参数有默认值 null。 /// /// 调用时可以省略: /// QueryListAsync<HwProductInfo>("Mapper", "selectAll"); // parameter 自动为 null /// /// 对比 Java: /// Java 不支持默认参数值,需要方法重载: /// public <T> List<T> queryList(String mapperName, String statementId) { /// return queryList(mapperName, statementId, null); /// } /// public <T> List<T> queryList(String mapperName, String statementId, Object parameter) { ... } /// /// C# 的默认参数更简洁,减少代码重复。 /// /// /// 【C# 语法知识点 - Task<T> 异步返回类型】 /// Task<List<T>> 表示"一个异步操作,最终会返回 List<T>"。 /// /// 这是 .NET 异步编程的基础: /// - Task:表示一个异步操作(不返回值的异步方法用 Task) /// - Task<T>:表示返回 T 类型的异步操作 /// /// 对比 Java: /// Java 的异步返回类型: /// - CompletableFuture<T>(Java 8+) /// - ListenableFuture<T>(Guava) /// - Future<T>(基础版,功能弱) /// /// C# 的 Task 比 Java 的 Future 功能更强大,语言级别支持 await/async。 /// /// /// 返回的数据类型(如 HwProductInfo) /// Mapper 名称(对应 XML 文件名,如 "HwProductInfoMapper") /// SQL 语句 ID(对应 XML 中的 id,如 "selectList") /// 查询参数对象(可选,null 表示无参数) /// 数据列表的异步任务 public Task> QueryListAsync(string mapperName, string statementId, object parameter = null) { // 泛型方法: // T 表示“希望数据库结果被映射成什么类型”。 // 例如 QueryListAsync(...) 最终会返回 List。 HwPortalPreparedSql preparedSql = _registry.Prepare(mapperName, statementId, parameter); return _db.Ado.SqlQueryAsync(preparedSql.Sql, preparedSql.Parameters); } /// /// 查询单条记录。 /// /// 【方法实现分析】 /// 这个方法的实现是"先查列表再取第一条": /// 1. 调用 QueryListAsync 获取列表 /// 2. 用 FirstOrDefault() 取第一条(或 null) /// /// 优点: /// - 代码复用:复用 QueryListAsync 的逻辑 /// - 简单易懂:初学者容易理解 /// /// 缺点: /// - 性能略差:数据库可能返回多条,浪费带宽 /// - 语义不清:LIMIT 1 应该在 SQL 层做,但这里依赖 XML 定义 /// /// 【优化建议】 /// 生产环境应该在 XML 的 SQL 里加 LIMIT 1(MySQL)或 TOP 1(SQL Server), /// 确保数据库只返回一条记录。 /// /// /// 【C# 语法知识点 - FirstOrDefault()】 /// FirstOrDefault() 是 LINQ 方法,返回集合的第一个元素,如果没有则返回默认值。 /// /// 对于引用类型(如 HwProductInfo),默认值是 null。 /// 对于值类型(如 int),默认值是 0。 /// /// 对比 Java: /// Java Stream:list.stream().findFirst().orElse(null) /// /// C# 的 FirstOrDefault() 更简洁。 /// /// /// 【C# 语法知识点 - async/await 详解】 /// async 标记方法为异步,await 等待异步操作完成。 /// /// 执行流程: /// 1. 调用 QueryListAsync,它返回一个 Task(尚未完成的任务) /// 2. await 表示"暂停这个方法,等待任务完成后再继续" /// 3. 等待期间,当前线程可以处理其他请求(不阻塞) /// 4. 任务完成后,方法继续执行,返回结果 /// /// 关键点: /// - await 只能在 async 方法中使用 /// - await 不会阻塞线程,只是让出控制权 /// - 编译器会把 async/await 转换成状态机代码(类似回调,但更优雅) /// /// /// 返回的数据类型 /// Mapper 名称 /// SQL 语句 ID /// 查询参数(可选) /// 单条记录或 null 的异步任务 public async Task QuerySingleAsync(string mapperName, string statementId, object parameter = null) { // 当前实现为了简单,先查列表再取第一条。 // 对初学者来说,这种写法比直接封装多种执行路径更好理解。 List items = await QueryListAsync(mapperName, statementId, parameter); return items.FirstOrDefault(); } /// /// 执行非查询 SQL(增删改)。 /// /// 【业务场景】 /// 用于执行 INSERT、UPDATE、DELETE 等不返回结果集的 SQL。 /// 返回"影响行数": /// - 插入:返回插入的行数(通常是 1) /// - 更新:返回实际修改的行数 /// - 删除:返回实际删除的行数 /// /// 示例: /// int rows = await executor.ExecuteAsync("UserMapper", "updateStatus", new { userId = 1, status = "active" }); /// if (rows > 0) { /* 更新成功 */ } /// /// /// 【C# 语法知识点 - async/await 返回值】 /// return await _db.Ado.ExecuteCommandAsync(...) /// /// 这里的 await 会: /// 1. 等待数据库操作完成 /// 2. 解包 Task<int> 得到 int 结果 /// 3. 返回这个 int /// /// 如果去掉 await 直接返回 Task: /// return _db.Ado.ExecuteCommandAsync(...); // 编译错误,类型不匹配 /// /// 必须 await 才能拿到实际值。 /// /// /// Mapper 名称 /// SQL 语句 ID /// 执行参数(可选) /// 影响行数的异步任务 public async Task ExecuteAsync(string mapperName, string statementId, object parameter = null) { HwPortalPreparedSql preparedSql = _registry.Prepare(mapperName, statementId, parameter); return await _db.Ado.ExecuteCommandAsync(preparedSql.Sql, preparedSql.Parameters); } /// /// 插入记录并返回自增主键。 /// /// 【业务场景】 /// 新增记录时,数据库会自动生成主键(自增 ID)。 /// 这个方法执行 INSERT 后,立即查询并返回生成的主键值。 /// /// 典型用法: /// var newUser = new User { Name = "张三" }; // Id 还未知 /// long newId = await executor.InsertReturnIdentityAsync("UserMapper", "insert", newUser); /// newUser.Id = newId; // 回填主键 /// /// 对比 Java MyBatis: /// Java 配置 useGeneratedKeys="true" keyProperty="id" 后, /// MyBatis 会自动把生成的主键设置到实体的 id 属性,不需要手动查询。 /// /// 这里采用"执行 INSERT + 查询 LAST_INSERT_ID()"的方式, /// 虽然多了一次查询,但更通用(不依赖具体 ORM 的主键回填机制)。 /// /// /// 【MySQL LAST_INSERT_ID() 说明】 /// LAST_INSERT_ID() 是 MySQL 的函数,返回当前连接最近生成的自增主键。 /// /// 关键点: /// 1. 是"当前连接"级别的,不受其他连接影响(线程安全) /// 2. 只对 AUTO_INCREMENT 列有效 /// 3. 一次插入多行时,返回第一行的 ID /// /// 其他数据库的等效写法: /// - SQL Server: SELECT SCOPE_IDENTITY() /// - PostgreSQL: RETURNING id 或 SELECT currval('seq_name') /// - Oracle: RETURNING id INTO ...(需要存储过程) /// /// /// 【C# 语法知识点 - 为什么这里不用默认参数?】 /// 注意这个方法没有 parameter = null,而是要求必须传 parameter。 /// /// 原因: /// 1. 插入操作必须有数据,不可能无参数插入 /// 2. 强制调用方提供参数,避免空引用错误 /// 3. 语义清晰:插入什么必须明确指定 /// /// 设计原则: /// - 可选的用默认参数(如查询条件) /// - 强制的不用默认参数(如插入数据) /// /// /// Mapper 名称 /// SQL 语句 ID /// 插入数据对象(必须) /// 自增主键值的异步任务 public async Task InsertReturnIdentityAsync(string mapperName, string statementId, object parameter) { HwPortalPreparedSql preparedSql = _registry.Prepare(mapperName, statementId, parameter); await _db.Ado.ExecuteCommandAsync(preparedSql.Sql, preparedSql.Parameters); // LAST_INSERT_ID() 是 MySQL 风格的“最近一次自增主键”读取方式。 // 这里为了保持和源 Java/MyBatis 行为一致,先这样处理。 List ids = await _db.Ado.SqlQueryAsync("SELECT LAST_INSERT_ID();"); return ids.FirstOrDefault(); } }