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#

// ============================================================================
// 【文件说明】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();
}
}