Dapper ORM 教程:轻量级、高性能的“Micro-ORM”

目录

  1. 什么是 Dapper?
  2. 为什么选择 Dapper?
  3. 环境准备
  4. 基础 CRUD 操作
    • 查询单条数据
    • 查询多条数据
    • 参数化查询
    • 插入数据
    • 更新数据
    • 删除数据
  5. 进阶用法
    • 多映射与一对一查询
    • 多映射与一对多查询
    • 动态查询
    • 存储过程调用
    • 事务处理
  6. 最佳实践与技巧
    • IDbConnection 的管理
    • Dapper 扩展方法(如 Dapper.Contrib)
    • 异步方法
    • 性能考虑

什么是 Dapper?

Dapper 是一个由 Sam Saffron (Stack Overflow 团队成员) 开发的轻量级、高性能的 Micro-ORM (微型对象关系映射) 库。

dapper orm 教程
(图片来源网络,侵删)
  • Micro-ORM: 相比于 Entity Framework (EF) 或 NHibernate 这样功能全面的 ORM 框架,Dapper 只提供了最核心的数据映射功能,它不负责模型创建、数据库迁移、变更跟踪等复杂功能。
  • 高性能: Dapper 的核心是一个扩展方法库,它通过 Emit 动态生成 IL 代码来执行数据读取和对象映射,其性能非常接近于手写 ADO.NET 代码,因此被誉为“King of Micro-ORMs”。
  • 简单易用: Dapper 的 API 非常简洁,只需要学习几个核心方法,就能上手进行数据库操作。

核心思想: Dapper 不会改变你写 SQL 的方式,它只是帮你把执行 SQL 后返回的 DataReader 结果集,自动映射成你定义的 C# 对象。


为什么选择 Dapper?

特性 Dapper Entity Framework (EF Core)
性能 极高,接近原生 ADO.NET 良好,但通常低于 Dapper
控制力 完全控制 SQL,可以写任意复杂的 SQL 通过 LINQ 或 Fluent API,有时无法生成最优 SQL
学习曲线 非常低,只需懂 SQL 和 C# 较高,需要理解 EF 的概念和机制
功能 轻量,只做映射 重量级,包含 Change Tracking, Unit of Work, Migration 等
适用场景 读多写少、对性能要求高、SQL 复杂的项目 中小型项目、快速开发、对 SQL 不熟悉的团队

如果你追求极致的性能,需要精细控制 SQL,或者你的项目逻辑非常复杂以至于 EF 难以应对,Dapper 是绝佳选择。


环境准备

  1. 安装 NuGet 包: 在你的 .NET 项目 (如 .NET 6/7/8 Console, Web API, WinForms) 中,通过 NuGet 包管理器控制台或管理器界面安装 Dapper

    Install-Package Dapper
  2. 创建数据模型: 我们将使用一个简单的 User 模型作为示例。

    dapper orm 教程
    (图片来源网络,侵删)
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public DateTime CreatedAt { get; set; }
    }
  3. 准备数据库: 创建一个数据库和一张 Users 表。

    CREATE DATABASE MyDb;
    GO
    USE MyDb;
    GO
    CREATE TABLE Users (
        Id INT PRIMARY KEY IDENTITY(1,1),
        Name NVARCHAR(100) NOT NULL,
        Email NVARCHAR(255) NOT NULL UNIQUE,
        CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE()
    );
    GO

基础 CRUD 操作

所有 Dapper 的扩展方法都来自于 System.Data 命名空间下的 IDbConnection 接口,你需要一个实现了该接口的数据库连接对象(如 SqlConnection, MySqlConnection 等)。

using System;
using System.Data;
using System.Data.SqlClient;
using Dapper;
using System.Collections.Generic;
public class UserRepository
{
    private readonly string _connectionString;
    public UserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }
    // 辅助方法:获取数据库连接
    private IDbConnection GetConnection()
    {
        return new SqlConnection(_connectionString);
    }
}

查询单条数据: QueryFirst, QueryFirstOrDefault

使用 QueryFirstOrDefault 是最安全的方式,当查询没有结果时返回 null,避免异常。

public User GetUserById(int id)
{
    using (var connection = GetConnection())
    {
        // SQL 语句中必须包含与 User 属性名匹配的列
        string sql = "SELECT Id, Name, Email, CreatedAt FROM Users WHERE Id = @Id";
        // Dapper 会自动将 @Id 参数化,防止 SQL 注入
        // QueryFirstOrDefault 会返回第一个匹配的用户,如果没有则返回 null
        return connection.QueryFirstOrDefault<User>(sql, new { Id = id });
    }
}

查询多条数据: Query, QueryAsync

Query 方法返回一个 IEnumerable<T>

dapper orm 教程
(图片来源网络,侵删)
public IEnumerable<User> GetAllUsers()
{
    using (var connection = GetConnection())
    {
        string sql = "SELECT Id, Name, Email, CreatedAt FROM Users";
        // Query 方法会自动将结果映射到 User 对象的集合
        return connection.Query<User>(sql);
    }
}

参数化查询

Dapper 强烈推荐使用参数化查询,它既安全又高效,有两种方式:

  1. 匿名对象 (推荐): new { PropertyName = value }

    // 上面的 GetUserById 和 GetAllUsers 已经展示了这种方式
    connection.Query<User>("SELECT * FROM Users WHERE Name = @Name", new { Name = "John Doe" });
  2. 动态对象: new { dynamic }

    connection.Query<User>("SELECT * FROM Users WHERE CreatedAt > @Date", new { Date = DateTime.Now.AddDays(-7) });

插入数据: Execute

Execute 方法用于执行 INSERT, UPDATE, DELETE 等不返回结果集的命令,并返回受影响的行数。

public int AddUser(User user)
{
    using (var connection = GetConnection())
    {
        string sql = "INSERT INTO Users (Name, Email) VALUES (@Name, @Email); SELECT CAST(SCOPE_IDENTITY() as int)";
        // Execute 的最后一个参数是命令类型,这里我们返回新插入行的 ID
        // Dapper 可以轻松处理 "SELECT ...; INSERT ..." 这样的复合语句
        var newId = connection.Query<int>(sql, user).Single();
        return newId;
    }
}

注意: 为了获取新插入记录的 ID,我们使用了 ; SELECT SCOPE_IDENTITY() as int 这种 SQL Server 的标准做法,Dapper 会执行整个命令并返回 SELECT 查询的结果。

更新数据: Execute

public bool UpdateUser(User user)
{
    using (var connection = GetConnection())
    {
        string sql = "UPDATE Users SET Name = @Name, Email = @Email WHERE Id = @Id";
        // Execute 返回受影响的行数
        int affectedRows = connection.Execute(sql, user);
        return affectedRows > 0;
    }
}

删除数据: Execute

public bool DeleteUser(int id)
{
    using (var connection = GetConnection())
    {
        string sql = "DELETE FROM Users WHERE Id = @Id";
        int affectedRows = connection.Execute(sql, new { Id = id });
        return affectedRows > 0;
    }
}

进阶用法

多映射与一对一查询

假设我们有 PostBlog 两个模型,一个 Post 属于一个 Blog。

public class Blog
{
    public int Id { get; set; }
    public string Url { get; set; }
}
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }
    public string Title { get; set; }
    public Blog Blog { get; set; } // 一对一关系
}

我们需要写一个 JOIN 查询,并告诉 Dapper 如何将结果映射到这两个对象。

public Post GetPostWithBlog(int postId)
{
    using (var connection = GetConnection())
    {
        string sql = @"SELECT p.Id, p.BlogId, p.Title, b.Id, b.Url 
                       FROM Posts p 
                       INNER JOIN Blogs b ON p.BlogId = b.Id 
                       WHERE p.Id = @PostId";
        // Query 是一个泛型方法,可以指定多个类型
        // 第一个参数是主要结果类型,后面的参数是关联类型
        // splitOn 参数告诉 Dapper 从哪个列开始解析下一个对象 (默认是 "Id")
        var post = connection.Query<Post, Blog, Post>(
            sql,
            (post, blog) => 
            {
                post.Blog = blog; // 手动关联对象
                return post;
            },
            new { PostId = postId },
            splitOn: "BlogId" // 指定 Blog 对象的 Id 列名
        ).FirstOrDefault();
        return post;
    }
}

多映射与一对多查询

一个 Blog 可以有多个 Post

public class Blog
{
    public int Id { get; set; }
    public string Url { get; set; }
    public List<Post> Posts { get; set; } = new List<Post>(); // 一对多关系
}

我们需要使用 GroupBy 来将多个 Post 关联到一个 Blog。

public Blog GetBlogWithPosts(int blogId)
{
    using (var connection = GetConnection())
    {
        string sql = @"SELECT b.Id, b.Url, p.Id, p.BlogId, p.Title 
                       FROM Blogs b 
                       LEFT JOIN Posts p ON b.Id = p.BlogId 
                       WHERE b.Id = @BlogId";
        // 使用 GroupBy 来处理一对多关系
        var blogDictionary = new Dictionary<int, Blog>();
        connection.Query<Blog, Post, Blog>(
            sql,
            (blog, post) => 
            {
                // 如果字典中没有这个 blog,则添加进去
                if (!blogDictionary.TryGetValue(blog.Id, out var currentBlog))
                {
                    currentBlog = blog;
                    currentBlog.Posts = new List<Post>();
                    blogDictionary.Add(currentBlog.Id, currentBlog);
                }
                // post 不为空 (防止 LEFT JOIN 导致的空行),则添加到列表
                if (post != null)
                {
                    currentBlog.Posts.Add(post);
                }
                return currentBlog;
            },
            new { BlogId = blogId },
            splitOn: "PostId"
        ).Distinct().FirstOrDefault(); // 使用 Distinct 去除重复的 Blog 对象
        return blogDictionary.Values.FirstOrDefault();
    }
}

动态查询

Dapper 可以将查询结果映射为 dynamic 对象,这在处理不确定列名的查询时非常有用。

public dynamic GetDynamicUserData(int id)
{
    using (var connection = GetConnection())
    {
        string sql = "SELECT Id, Name, Email FROM Users WHERE Id = @Id";
        return connection.QueryFirstOrDefault(sql, new { Id = id });
    }
}
// 使用
var user = GetDynamicUserData(1);
Console.WriteLine(user.Name); // 可以通过属性名访问

存储过程调用

调用存储过程非常简单,只需设置 commandType 即可。

public User GetUserByStoredProcedure(int id)
{
    using (var connection = GetConnection())
    {
        var p = new DynamicParameters();
        p.Add("@Id", id); // 输入参数
        p.Add("@Name", dbType: DbType.String, direction: ParameterDirection.Output); // 输出参数
        connection.Execute("sp_GetUserById", p, commandType: CommandType.StoredProcedure);
        // 获取输出参数的值
        string userName = p.Get<string>("@Name");
        // 或者你也可以让存储过程返回一个结果集,然后用 Query 获取
        // var user = connection.QueryFirstOrDefault<User>("sp_GetUserById", p, commandType: CommandType.StoredProcedure);
        return new User { Name = userName }; // 这里简化了,实际中可能需要更复杂的逻辑
    }
}

事务处理

Dapper 的事务处理与 ADO.NET 原生方式一致。

public void TransferFunds(int fromUserId, int toUserId, decimal amount)
{
    using (var connection = GetConnection())
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            try
            {
                // 扣款
                connection.Execute("UPDATE Accounts SET Balance = Balance - @Amount WHERE UserId = @FromUserId",
                    new { Amount = amount, FromUserId = fromUserId }, transaction);
                // 付款
                connection.Execute("UPDATE Accounts SET Balance = Balance + @Amount WHERE UserId = @ToUserId",
                    new { Amount = amount, ToUserId = toUserId }, transaction);
                // 如果所有操作都成功,则提交事务
                transaction.Commit();
            }
            catch
            {
                // 如果发生任何异常,则回滚事务
                transaction.Rollback();
                throw; // 重新抛出异常
            }
        }
    }
}

最佳实践与技巧

IDbConnection 的管理

  • 使用 using 语句: 这是必须的!IDbConnection 实现了 IDisposable,使用 using 可以确保连接在使用完毕后被正确关闭和释放,避免连接泄露。

    // 正确
    using (var connection = new SqlConnection(_connectionString))
    {
        // ... do work ...
    }
    // 错误!连接不会被关闭
    // var connection = new SqlConnection(_connectionString);
    // ... do work ...
  • 依赖注入: 在大型应用中,不要在每个方法中都创建连接,应该通过依赖注入容器(如 ASP.NET Core 的内置容器)将 IDbConnection 或一个封装了连接的仓储类注入到需要的地方。

Dapper 扩展方法 (如 Dapper.Contrib)

官方的 Dapper 包只提供了核心的查询和执行方法,如果你想获得类似 EF 的 Insert, Update, Delete, GetAll 等便捷方法,可以使用 Dapper.Contrib 包。

Install-Package Dapper.Contrib

使用前,需要在连接上设置 Configure

using Dapper.Contrib.Extensions; // 引入命名空间
// 在连接上配置
SqlMapperExtensions.TableNameMapper = (type) => type.Name; // 将类名映射为表名
public void AddUserWithContrib(User user)
{
    using (var connection = GetConnection())
    {
        // 直接传入对象即可,Dapper.Contrib 会自动生成 INSERT 语句
        connection.Insert(user);
    }
}
public void UpdateUserWithContrib(User user)
{
    using (var connection = GetConnection())
    {
        connection.Update(user);
    }
}

注意: Dapper.Contrib 的自动映射功能很方便,但在复杂场景下(如列名与属性名不一致、需要复杂映射)不如手写 SQL 灵活。

异步方法

Dapper 提供了所有核心方法的异步版本,以避免 I/O 操作阻塞线程,这对于构建高性能的 Web 服务至关重要。

// 异步查询
public async Task<User> GetUserByIdAsync(int id)
{
    using (var connection = GetConnection())
    {
        await connection.OpenAsync(); // 异步打开连接
        return await connection.QueryFirstOrDefaultAsync<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = id });
    }
}
// 异步执行
public async Task<int> AddUserAsync(User user)
{
    using (var connection = GetConnection())
    {
        await connection.OpenAsync();
        return await connection.QuerySingleAsync<int>(/* insert sql with select identity */);
    }
}

性能考虑

  • 批量操作: 当需要插入或更新大量数据时,不要在循环中调用 Execute,这会产生大量的数据库往返,性能极差,应该使用 DataTable 或字符串拼接的方式一次性执行。

    错误示范:

    foreach(var user in users)
    {
        connection.Execute("INSERT ...", user);
    }

    正确示范 (使用 Table-Valued Parameter):

    // 1. 将 List<User> 转换为 DataTable
    var table = new DataTable();
    table.Columns.Add("Name", typeof(string));
    table.Columns.Add("Email", typeof(string));
    foreach (var user in users)
    {
        table.Rows.Add(user.Name, user.Email);
    }
    // 2. 创建参数
    var tvp = new SqlParameter
    {
        ParameterName = "@Users",
        SqlDbType = System.Data.SqlDbType.Structured,
        Value = table,
        TypeName = "dbo.UserTableType" // 你的数据库中必须定义一个对应的表类型
    };
    // 3. 执行存储过程或 SQL
    connection.Execute("sp_InsertUsers", tvp, commandType: CommandType.StoredProcedure);

Dapper 是一个强大而灵活的工具,它完美地平衡了性能和开发效率,它不会强迫你使用某种特定的开发模式,而是将控制权完全交还给开发者。

何时使用 Dapper:

  • 对性能有极致要求的应用。
  • 数据库查询非常复杂,ORM 生成的 SQL 效率低下。
  • 项目团队对 SQL 非常熟悉,希望获得完全的控制权。
  • 作为现有 EF Core 项目的补充,用于处理性能关键的热点路径。

学习 Dapper 的建议:

  1. 掌握基础: 先熟练掌握 Query, Execute, QueryFirst 等核心方法。
  2. 精通参数化: 始终使用参数化查询来保证安全和性能。
  3. 学习映射: 理解 Dapper 如何将列名映射到属性名,并掌握一对一、一对多的多映射技巧。
  4. 拥抱异步: 在任何可能阻塞 I/O 的地方,都优先使用异步方法。
  5. 实践事务: 理解事务的重要性,并学会在 Dapper 中正确使用它。

通过这份教程,你已经具备了使用 Dapper 进行日常开发的能力,多加练习,你会发现它将成为你工具箱中不可或缺的利器。