在 ABP vNext中优雅集成 Dapper

#王者杯·14天创作挑战营·第1期#

🚀 在 ABP 框架中优雅集成 Dapper:一站式封装事务、多结果集与分页查询 🎉

随着业务复杂度的提升和性能要求的不断攀升,虽然 EF Core 功能强大,但在某些场景下,基于 Dapper 的原生 SQL 查询依然不可或缺。本文将结合 ABP vNext,介绍如何封装一个通用且高效的 DapperHelper,帮助你:

  • 🔥 性能需求:在高并发或数据量大的场景下,EF Core 的 LINQ-to-Entities 生成 SQL 可能略显臃肿,Dapper 原生 SQL 性能更优
  • 🔗 一致性封装:项目中零散地使用 Dapper 很容易导致连接、事务管理分散,难以统一维护
  • 🔧 灵活性:需要同时支持单表 CRUD、复杂联表、分页、甚至存储过程等多种用例

借助 ABP 的依赖注入与 EF Core 的连接管理能力,我们可以优雅地将 Dapper 封装成一个可插拔、可扩展的组件。


📖 背景与动机

  • 🔥 性能需求:在高并发或数据量大的场景下,EF Core 的 LINQ-to-Entities 生成 SQL 可能略显臃肿,Dapper 原生 SQL 性能更优
  • 🔗 一致性封装:项目中零散地使用 Dapper 很容易导致连接、事务管理分散,难以统一维护
  • 🔧 灵活性:需要同时支持单表 CRUD、复杂联表、分页、甚至存储过程等多种用例

借助 ABP 的依赖注入与 EF Core 的连接管理能力,我们可以优雅地将 Dapper 封装成一个可插拔、可扩展的组件。


📋 接口定义:IDapperHelper

using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;

namespace YourProject.Dapper
{
    /// <summary>
    /// Dapper 操作接口定义,封装常用数据库操作。
    /// </summary>
    public interface IDapperHelper
    {
        /// <summary>
        /// 执行查询并返回多个实体对象。
        /// </summary>
        /// <typeparam name="T">返回类型</typeparam>
        /// <param name="sql">SQL 查询语句</param>
        /// <param name="parameters">查询参数(可选)</param>
        /// <param name="commandType">命令类型(可选)</param>
        /// <param name="cancellationToken">取消令牌(可选)</param>
        /// <returns>对象集合</returns>
        Task<IEnumerable<T>> QueryAsync<T>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default
        );

        /// <summary>
        /// 执行查询并返回首个对象,或默认值。
        /// </summary>
        /// <typeparam name="T">返回类型</typeparam>
        /// <param name="sql">SQL 查询语句</param>
        /// <param name="parameters">查询参数(可选)</param>
        /// <param name="commandType">命令类型(可选)</param>
        /// <param name="cancellationToken">取消令牌(可选)</param>
        /// <returns>首个对象或 null</returns>
        Task<T?> QueryFirstOrDefaultAsync<T>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default
        );

        /// <summary>
        /// 执行非查询语句(如 INSERT、UPDATE、DELETE)。
        /// </summary>
        /// <param name="sql">SQL 执行语句</param>
        /// <param name="parameters">参数对象(可选)</param>
        /// <param name="commandType">命令类型(可选)</param>
        /// <param name="cancellationToken">取消令牌(可选)</param>
        /// <returns>受影响的行数</returns>
        Task<int> ExecuteAsync(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default
        );

        /// <summary>
        /// 显式开启事务并执行操作。
        /// </summary>
        /// <param name="action">事务操作委托,提供连接与事务对象</param>
        /// <param name="cancellationToken">取消令牌(可选)</param>
        /// <returns>异步任务</returns>
        Task ExecuteInTransactionAsync(
            Func<IDbConnection, IDbTransaction, CancellationToken, Task> action,
            CancellationToken cancellationToken = default
        );

        /// <summary>
        /// 查询并读取两个结果集。
        /// </summary>
        /// <typeparam name="T1">第一个结果集类型</typeparam>
        /// <typeparam name="T2">第二个结果集类型</typeparam>
        /// <param name="sql">包含多个结果集的 SQL 语句</param>
        /// <param name="parameters">查询参数(可选)</param>
        /// <param name="commandType">命令类型(可选)</param>
        /// <param name="cancellationToken">取消令牌(可选)</param>
        /// <returns>包含两个集合的元组</returns>
        Task<(IEnumerable<T1>, IEnumerable<T2>)> QueryMultipleAsync<T1, T2>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default
        );

        /// <summary>
        /// 查询并读取三个结果集。
        /// </summary>
        /// <typeparam name="T1">第一个结果集类型</typeparam>
        /// <typeparam name="T2">第二个结果集类型</typeparam>
        /// <typeparam name="T3">第三个结果集类型</typeparam>
        /// <param name="sql">包含多个结果集的 SQL 语句</param>
        /// <param name="parameters">查询参数(可选)</param>
        /// <param name="commandType">命令类型(可选)</param>
        /// <param name="cancellationToken">取消令牌(可选)</param>
        /// <returns>包含三个集合的元组</returns>
        Task<(IEnumerable<T1>, IEnumerable<T2>, IEnumerable<T3>)> QueryMultipleAsync<T1, T2, T3>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default
        );

        /// <summary>
        /// 执行分页查询。
        /// </summary>
        /// <typeparam name="T">返回类型</typeparam>
        /// <param name="baseSql">基础 SQL(不含分页)</param>
        /// <param name="pageIndex">页码(从 1 开始)</param>
        /// <param name="pageSize">每页记录数</param>
        /// <param name="parameters">查询参数(可选)</param>
        /// <param name="commandType">命令类型(可选)</param>
        /// <param name="cancellationToken">取消令牌(可选)</param>
        /// <returns>分页结果集合</returns>
        Task<IEnumerable<T>> QueryPagedAsync<T>(
            string baseSql,
            int pageIndex,
            int pageSize,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default
        );
    }
}
  • 统一接口:所有常见操作一应俱全
  • 🚀 异步友好:基于 async/await,可传递 CancellationToken
  • 🧩 可扩展:支持多结果集与分页

🔨 核心实现:DapperHelper

using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;

namespace YourProject.EntityFrameworkCore.Dapper
{
    /// <summary>
    /// Dapper 操作辅助类,支持事务控制、日志记录、多结果集与分页查询。
    /// </summary>
    public class DapperHelper : IDapperHelper, ITransientDependency
    {
        private readonly IDbContextProvider<YourProjectDbContext> _dbContextProvider;
        private readonly ILogger<DapperHelper> _logger;

        public DapperHelper(
            IDbContextProvider<YourProjectDbContext> dbContextProvider,
            ILogger<DapperHelper> logger)
        {
            _dbContextProvider = dbContextProvider;
            _logger = logger;
        }

        private async Task<(IDbConnection, IDbTransaction?)> GetConnectionAndTransactionAsync()
        {
            var dbContext = _dbContextProvider.GetDbContext();
            var connection = dbContext.Database.GetDbConnection();
            if (connection.State != ConnectionState.Open)
            {
                await connection.OpenAsync();
            }
            var transaction = dbContext.Database.CurrentTransaction?.GetDbTransaction();
            return (connection, transaction);
        }

        public async Task<IEnumerable<T>> QueryAsync<T>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default)
        {
            var (conn, tran) = await GetConnectionAndTransactionAsync();
            _logger.LogDebug("QueryAsync SQL: {Sql}, Params: {@Params}", sql, parameters);
            return await conn.QueryAsync<T>(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
        }

        public async Task<T?> QueryFirstOrDefaultAsync<T>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default)
        {
            var (conn, tran) = await GetConnectionAndTransactionAsync();
            _logger.LogDebug("QueryFirstOrDefaultAsync SQL: {Sql}, Params: {@Params}", sql, parameters);
            return await conn.QueryFirstOrDefaultAsync<T>(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
        }

        public async Task<int> ExecuteAsync(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default)
        {
            var (conn, tran) = await GetConnectionAndTransactionAsync();
            _logger.LogDebug("ExecuteAsync SQL: {Sql}, Params: {@Params}", sql, parameters);
            return await conn.ExecuteAsync(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
        }

        public async Task ExecuteInTransactionAsync(
            Func<IDbConnection, IDbTransaction, CancellationToken, Task> action,
            CancellationToken cancellationToken = default)
        {
            var dbContext = await _dbContextProvider.GetDbContextAsync();
            var strategy = dbContext.Database.CreateExecutionStrategy();

            await strategy.ExecuteAsync(async execCt =>
            {
                await using var efTransaction = await dbContext.Database.BeginTransactionAsync(execCt);
                var connection = dbContext.Database.GetDbConnection();

                if (connection.State != ConnectionState.Open)
                {
                    await connection.OpenAsync(execCt);
                }

                var dbTransaction = efTransaction.GetDbTransaction();

                try
                {
                    _logger.LogDebug("Begin Transaction");
                    await action(connection, dbTransaction, execCt);
                    await efTransaction.CommitAsync(execCt);
                    _logger.LogDebug("Transaction Committed");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Transaction Rolled Back");
                    await efTransaction.RollbackAsync(execCt);
                    throw;
                }
            }, cancellationToken);
        }

        public async Task<(IEnumerable<T1>, IEnumerable<T2>)> QueryMultipleAsync<T1, T2>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default)
        {
            var (conn, tran) = await GetConnectionAndTransactionAsync();
            _logger.LogDebug("QueryMultiple<T1, T2> SQL: {Sql}, Params: {@Params}", sql, parameters);
            using var multi = await conn.QueryMultipleAsync(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
            var first = await multi.ReadAsync<T1>();
            var second = await multi.ReadAsync<T2>();
            return (first, second);
        }

        public async Task<(IEnumerable<T1>, IEnumerable<T2>, IEnumerable<T3>)> QueryMultipleAsync<T1, T2, T3>(
            string sql,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default)
        {
            var (conn, tran) = await GetConnectionAndTransactionAsync();
            _logger.LogDebug("QueryMultiple<T1, T2, T3> SQL: {Sql}, Params: {@Params}", sql, parameters);
            using var multi = await conn.QueryMultipleAsync(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
            var first = await multi.ReadAsync<T1>();
            var second = await multi.ReadAsync<T2>();
            var third = await multi.ReadAsync<T3>();
            return (first, second, third);
        }

        public async Task<IEnumerable<T>> QueryPagedAsync<T>(
            string baseSql,
            int pageIndex,
            int pageSize,
            object? parameters = null,
            CommandType commandType = CommandType.Text,
            CancellationToken cancellationToken = default)
        {
            var dynamicParams = new DynamicParameters(parameters);
            dynamicParams.Add("Offset", (pageIndex - 1) * pageSize);
            dynamicParams.Add("PageSize", pageSize);

            string pagedSql;

            // === 请取消注释你使用的数据库类型 ===

            // -- SQL Server / PostgreSQL(默认) --
            pagedSql = $"{baseSql} OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";

            // -- MySQL --
            // pagedSql = $"{baseSql} LIMIT @PageSize OFFSET @Offset";
            
            _logger.LogDebug("QueryPaged SQL: {Sql}, Params: {@Params}", pagedSql, dynamicParams);

            var (conn, tran) = await GetConnectionAndTransactionAsync();
            return await conn.QueryAsync<T>(new CommandDefinition(pagedSql, dynamicParams, tran, cancellationToken: cancellationToken, commandType: commandType));
        }
    }
}

🚀 实现机制详解

📐 功能支持矩阵

功能名称是否支持备注说明
查询(单/多行)使用 QueryAsync / QueryFirstOrDefaultAsync 实现
执行 SQL兼容 INSERT/UPDATE/DELETE
事务控制使用 EF Core 的事务封装
多结果集支持 QueryMultipleAsync
分页查询跨数据库 SQL 模板切换
日志记录使用 ILogger 输出完整 SQL 与参数
依赖注入与 ABP 生命周期完美集成

🛠️ 方法清单表

方法名称返回类型典型用途
QueryAsync<T>IEnumerable<T>返回多行数据
QueryFirstOrDefaultAsync<T>T?查询单条数据
ExecuteAsyncint执行写操作
QueryMultipleAsync<T1, T2>(IEnumerable<T1>, IEnumerable<T2>)多结果集支持
ExecuteInTransactionAsyncTask控制事务性操作
QueryPagedAsync<T>IEnumerable<T>分页查询支持

🔌 获取连接与事务:统一资源管理

GetConnectionAndTransactionAsync 方法负责从 DbContext 中获取数据库连接与当前事务,确保 Dapper 操作与 EF Core 操作共享事务上下文,避免数据一致性问题。

private async Task<(IDbConnection, IDbTransaction?)> GetConnectionAndTransactionAsync()
  • 如果连接未打开,则自动打开
  • 支持从 EF 的 Database.CurrentTransaction 中提取事务对象

🔍 查询方法:简化数据读取

封装了常见的查询方法,包括:

  • QueryAsync<T>:通用多行查询
  • QueryFirstOrDefaultAsync<T>:只取首行,适合详情或单值查询
  • QueryMultipleAsync<T1, T2, T3>:支持多结果集

每个方法均支持:

  • 可选参数(匿名对象)
  • 命令类型控制(TextStoredProcedure
  • 日志记录 SQL 与参数
_logger.LogDebug("QueryAsync SQL: {Sql}, Params: {@Params}", sql, parameters);

🧾 命令执行:安全调用数据库写操作

ExecuteAsync 提供插入、更新、删除等操作,封装事务处理逻辑,保证安全性与一致性。

public async Task<int> ExecuteAsync(...)
  • 返回受影响的行数
  • 适合用于业务层判断操作是否成功

🔁 事务封装:优雅处理重试与失败回滚

通过 EF Core 的 ExecutionStrategy 实现的 ExecuteInTransactionAsync 支持自动重试机制,是数据库死锁或临时错误的可靠应对方式。

await strategy.ExecuteAsync(async execCt =>
{
    using var efTransaction = await dbContext.Database.BeginTransactionAsync(execCt);
    ...
});
  • 自动打开连接、开启事务
  • 自动提交或捕获异常并回滚
  • 支持日志记录事务状态(开始、提交、回滚)

📦 多结果集:高效处理复杂查询

使用 QueryMultipleAsync 提供最多三组数据集的读取能力,适用于多表联合查询、主从数据结构等场景。

using var multi = await conn.QueryMultipleAsync(...);
var first = await multi.ReadAsync<T1>();
  • 避免多次连接调用
  • 提高性能与一致性

📄 分页查询:数据库兼容的分页封装

QueryPagedAsync 提供跨数据库(PostgreSQL、SQL Server、MySQL)兼容的分页支持。

pagedSql = $"{baseSql} OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
// MySQL 示例(可替换)
pagedSql = $"{baseSql} LIMIT @PageSize OFFSET @Offset";
  • 自动构造分页参数
  • 支持自定义 SQL 拼接
  • 日志记录执行语句,便于调试
🗂️ 数据库分页语法对比
数据库类型分页语法样例
SQL Server / PostgreSQLOFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
MySQLLIMIT @PageSize OFFSET @Offset
Oracle需使用 ROWNUM 或子查询嵌套

🧪 使用示例

🔍 查询活跃用户

public async Task<List<UserDto>> GetActiveUsersAsync(CancellationToken cancellationToken)
{
    var sql = "SELECT Id, UserName, Email FROM AbpUsers WHERE IsActive = @Active";
    return (await _dapper.QueryAsync<UserDto>(sql, new { Active = true }, cancellationToken: cancellationToken)).AsList();
}

🔍 分页查询

public async Task<PagedResultDto<UserDto>> GetPagedUsersAsync(int pageIndex, int pageSize, CancellationToken cancellationToken)
{
    var count = await _dapper.QueryFirstOrDefaultAsync<int>("SELECT COUNT(*) FROM AbpUsers", cancellationToken: cancellationToken);
    var dataSql = "SELECT Id, UserName, Email FROM AbpUsers ORDER BY CreationTime DESC";
    var items = await _dapper.QueryPagedAsync<UserDto>(dataSql, pageIndex, pageSize, cancellationToken: cancellationToken);

    return new PagedResultDto<UserDto>
    {
        TotalCount = count,
        Items = items.AsList()
    };
}

🔍 多结果集查询

public async Task<UserWithRolesResult> GetUsersAndRolesAsync(CancellationToken cancellationToken)
{
    var sql = @"
        SELECT Id, UserName, Email FROM AbpUsers;
        SELECT Id, Name FROM AbpRoles;";
    var (users, roles) = await _dapper.QueryMultipleAsync<UserDto, RoleDto>(sql, cancellationToken: cancellationToken);
    return new UserWithRolesResult { Users = users.AsList(), Roles = roles.AsList() };
}

✏️ 更新并记录日志

public async Task UpdateUserAndLogAsync(Guid userId, string newEmail, CancellationToken cancellationToken)
{
    try
    {
        await _dapper.ExecuteInTransactionAsync(async (conn, tran, ct) =>
        {
            await conn.ExecuteAsync(new CommandDefinition(
                "UPDATE AbpUsers SET Email = @Email WHERE Id = @Id",
                new { Email = newEmail, Id = userId }, tran, cancellationToken: ct));

            await conn.ExecuteAsync(new CommandDefinition(
                "INSERT INTO OperationLogs (UserId, Action, Timestamp) VALUES (@UserId, 'UpdateEmail', NOW())",
                new { UserId = userId }, tran, cancellationToken: ct));
        }, cancellationToken);
        _logger.LogInformation("Successfully updated user {UserId}'s email", userId);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to update user {UserId}'s email", userId);
        throw;
    }
}

🧱完整代码

public class UserQueryAppService : ApplicationService
{
    private readonly IDapperHelper _dapper;
    private readonly ILogger<UserQueryAppService> _logger;

    public UserQueryAppService(IDapperHelper dapper, ILogger<UserQueryAppService> logger)
    {
        _dapper = dapper;
        _logger = logger;
    }

    public async Task<List<UserDto>> GetActiveUsersAsync(CancellationToken cancellationToken)
    {
        var sql = "SELECT Id, UserName, Email FROM AbpUsers WHERE IsActive = @Active";
        return (await _dapper.QueryAsync<UserDto>(sql, new { Active = true }, cancellationToken: cancellationToken)).AsList();
    }

    public async Task<PagedResultDto<UserDto>> GetPagedUsersAsync(int pageIndex, int pageSize, CancellationToken cancellationToken)
    {
        var count = await _dapper.QueryFirstOrDefaultAsync<int>("SELECT COUNT(*) FROM AbpUsers", cancellationToken: cancellationToken);
        var dataSql = "SELECT Id, UserName, Email FROM AbpUsers ORDER BY CreationTime DESC";
        var items = await _dapper.QueryPagedAsync<UserDto>(dataSql, pageIndex, pageSize, cancellationToken: cancellationToken);

        return new PagedResultDto<UserDto>
        {
            TotalCount = count,
            Items = items.AsList()
        };
    }

    public async Task<UserWithRolesResult> GetUsersAndRolesAsync(CancellationToken cancellationToken)
    {
        var sql = @"
            SELECT Id, UserName, Email FROM AbpUsers;
            SELECT Id, Name FROM AbpRoles;";
        var (users, roles) = await _dapper.QueryMultipleAsync<UserDto, RoleDto>(sql, cancellationToken: cancellationToken);
        return new UserWithRolesResult { Users = users.AsList(), Roles = roles.AsList() };
    }

    public async Task UpdateUserAndLogAsync(Guid userId, string newEmail, CancellationToken cancellationToken)
    {
        try
        {
            await _dapper.ExecuteInTransactionAsync(async (conn, tran, ct) =>
            {
                await conn.ExecuteAsync(new CommandDefinition(
                    "UPDATE AbpUsers SET Email = @Email WHERE Id = @Id",
                    new { Email = newEmail, Id = userId }, tran, cancellationToken: ct));

                await conn.ExecuteAsync(new CommandDefinition(
                    "INSERT INTO OperationLogs (UserId, Action, Timestamp) VALUES (@UserId, 'UpdateEmail', NOW())",
                    new { UserId = userId }, tran, cancellationToken: ct));
            }, cancellationToken);
            _logger.LogInformation("Successfully updated user {UserId}'s email", userId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to update user {UserId}'s email", userId);
            throw;
        }
    }
}

✅ 常见问题解答(FAQ)

Q1: 如何支持 MySQL 分页?

  • 将分页 SQL 修改为 LIMIT @PageSize OFFSET @Offset,并注释掉 SQL Server 的写法。

Q2: 是否支持 IN 查询?

  • Dapper 原生支持列表参数,例如:SELECT * FROM Users WHERE Id IN @Ids

Q3: 如何单元测试 DapperHelper?

  • 可以注入 Mock 的 IDbConnection 或 EF 的 InMemory Provider。

Q4: 是否支持 Oracle?

  • 支持,但分页 SQL 语法需适配 ROWNUM

🎯 总结与扩展

  • 🚫 零侵入:既可走 EF Core 的 UoW,也可随时手动开启 Dapper 事务
  • ♻️ 代码复用:所有模块只需注入 IDapperHelper 即可调用
  • 🔍 日志审计:内置 ILogger,方便定位慢 SQL 与参数
  • 🌱 易于扩展:可基于此基础,继续实现存储过程、批量插入、分库查询等高级功能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值