You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

368 lines
16 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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