Dapper 使用教程

目录

  1. 什么是 Dapper?
  2. 为什么选择 Dapper?
  3. 环境准备
  4. 核心概念:IDbConnection
  5. 基础 CRUD 操作
    • 查询数据
    • 执行命令 (增、删、改)
    • 事务处理
  6. 进阶用法
    • 查询并映射到多个对象 (One-to-Many)
    • 查询并映射到多个对象 (Many-to-Many)
    • 动态查询
    • 多结果集查询
    • 参数化查询
  7. 最佳实践
  8. 完整示例项目

什么是 Dapper?

Dapper 是一个轻量级、高性能的 .NET 对象映射器,它由 Stack Overflow 团队开发并开源,主要用途是让你能够用最少的代码将数据库查询结果(如 DataTableDataReader)映射到 .NET 对象(POCO/Model)。

你可以把它理解为 ADO.NET 的一个“超集”或“微扩展”,它不替代 ADO.NET,而是在 ADO.NET 的基础上提供了一层简洁的封装,让你写 SQL 的同时,能享受 ORM 带来的便利。

核心思想: "Write SQL, not code." (写 SQL,而不是写代码)。

为什么选择 Dapper?

  • 高性能:Dapper 非常快,性能接近于原生 ADO.NET,因为它只是简单地反射了对象的属性并赋值,它的性能通常比 Entity Framework、NHibernate 等重量级 ORM 高出很多。
  • 轻量级:Dapper 只是一个单独的 DLL 文件,没有复杂的配置和依赖,对项目侵入性极小。
  • 简单易学:如果你会写 SQL,你几乎不需要学习任何新东西就能上手 Dapper,API 设计非常直观。
  • 灵活可控:你可以自由地编写任何你想要的 SQL 查询,包括复杂的存储过程、多表连接查询等,不像某些 ORM 那样会生成难以优化的 SQL。
  • 无状态:Dapper 本身是无状态的,它不维护对象上下文或会话,这让你可以完全控制数据库连接的生命周期。

环境准备

  1. 安装 NuGet 包: 在你的 .NET 项目(如 .NET Core / .NET 5/6/7/8)中,通过 NuGet 包管理器控制台或 Visual Studio 的 NuGet 包管理器安装 Dapper

    Install-Package Dapper
  2. 准备数据库: 我们以一个简单的 Users 表为例。

    CREATE TABLE Users (
        Id INT PRIMARY KEY IDENTITY(1,1),
        Name NVARCHAR(100) NOT NULL,
        Email NVARCHAR(255) NOT NULL,
        Age INT,
        CreatedAt DATETIME2 DEFAULT GETDATE()
    );
    -- 插入一些测试数据
    INSERT INTO Users (Name, Email, Age) VALUES
    ('Alice', 'alice@example.com', 30),
    ('Bob', 'bob@example.com', 25),
    ('Charlie', 'charlie@example.com', 35);

核心概念:IDbConnection

Dapper 的所有扩展方法都是作为 System.Data.IDbConnection 接口的扩展方法存在的,这意味着任何实现了该接口的数据库连接类都可以使用 Dapper。

  • SQL Server: System.Data.SqlClient.SqlConnection
  • MySQL: MySql.Data.MySqlClient.MySqlConnection
  • SQLite: Microsoft.Data.Sqlite.SqliteConnection
  • PostgreSQL: Npgsql.NpgsqlConnection

使用 Dapper 的第一步就是创建并打开一个 IDbConnection 对象。

using System.Data.SqlClient;
using Dapper;
// 创建连接字符串 (建议从配置文件中读取)
string connectionString = "Server=your_server;Database=your_db;User Id=your_user;Password=your_password;";
// 创建连接对象
using (IDbConnection db = new SqlConnection(connectionString))
{
    // 打开连接 (Dapper 的大多数方法在执行前会自动检查连接状态,如果未打开会自动打开,但显式打开是好习惯)
    db.Open();
    // 在这里执行 Dapper 操作...
    // using 语句会确保连接在代码块结束时被正确关闭和释放
}

基础 CRUD 操作

我们定义一个与 Users 表对应的 C# 模型类。

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; } // 使用可空类型对应数据库中的 NULL
    public DateTime CreatedAt { get; set; }
}

1 查询数据

Dapper 提供了多种查询方法,最常用的是 Query<T>()

  • 查询单条记录

    // 假设 db 连接已打开
    string sql = "SELECT * FROM Users WHERE Id = @Id";
    // Query<T> 返回一个 IEnumerable<T>
    // 使用 FirstOrDefault() 来获取单个对象,如果不存在则返回 null
    var user = db.Query<User>(sql, new { Id = 1 }).FirstOrDefault();
    Console.WriteLine($"User: {user.Name}, Email: {user.Email}");
    • sql: 你的 SQL 查询语句。
    • new { Id = 1 }: 这是一个匿名对象,Dapper 会将其属性名和值作为参数化查询的参数。@Id 是 SQL 中的参数占位符。强烈推荐使用参数化查询,以防止 SQL 注入!
  • 查询多条记录

    string sql = "SELECT * FROM Users WHERE Age > @MinAge";
    // Query<T> 直接返回一个 IEnumerable<T>,包含所有匹配的记录
    var users = db.Query<User>(sql, new { MinAge = 28 });
    foreach (var u in users)
    {
        Console.WriteLine($"User: {u.Name}, Age: {u.Age}");
    }

2 执行命令 (增、删、改)

对于不返回数据的操作(如 INSERT, UPDATE, DELETE),使用 Execute() 方法,它返回一个 int 值,表示受影响的行数。

  • 插入数据

    string sql = "INSERT INTO Users (Name, Email, Age) VALUES (@Name, @Email, @Age)";
    var newUserData = new { Name = "David", Email = "david@example.com", Age = 28 };
    // Execute 返回受影响的行数
    int affectedRows = db.Execute(sql, newUserData);
    Console.WriteLine($"{affectedRows} row(s) inserted.");
  • 更新数据

    string sql = "UPDATE Users SET Name = @Name, Age = @Age WHERE Id = @Id";
    var updateData = new { Id = 2, Name = "Bob Smith", Age = 26 };
    int affectedRows = db.Execute(sql, updateData);
    Console.WriteLine($"{affectedRows} row(s) updated.");
  • 删除数据

    string sql = "DELETE FROM Users WHERE Id = @Id";
    int affectedRows = db.Execute(sql, new { Id = 3 });
    Console.WriteLine($"{affectedRows} row(s) deleted.");

3 事务处理

Dapper 对事务的支持非常简单,你只需要在 IDbConnection 上创建一个事务对象,并将其传递给 QueryExecute 方法即可。

string insertSql = "INSERT INTO Users (Name, Email, Age) VALUES (@Name, @Email, @Age)";
string deleteSql = "DELETE FROM Users WHERE Id = @Id";
// 1. 创建一个连接
using (IDbConnection db = new SqlConnection(connectionString))
{
    db.Open();
    // 2. 开始一个事务
    using (IDbTransaction transaction = db.BeginTransaction())
    {
        try
        {
            // 3. 在执行方法中传入 transaction 对象
            db.Execute(insertSql, new { Name = "Eve", Email = "eve@example.com", Age = 40 }, transaction);
            db.Execute(deleteSql, new { Id = 4 }, transaction); // 假设 Id=4 的用户不存在
            // 4. 如果所有操作都成功,提交事务
            transaction.Commit();
            Console.WriteLine("Transaction committed successfully.");
        }
        catch (Exception ex)
        {
            // 5. 如果发生任何异常,回滚事务
            transaction.Rollback();
            Console.WriteLine($"Transaction failed and was rolled back. Error: {ex.Message}");
        }
    }
}

进阶用法

1 查询并映射到多个对象 (One-to-Many)

这是 Dapper 最强大的功能之一,假设我们有 PostsComments 表,一个帖子可以有多个评论。

场景:查询所有帖子及其对应的评论列表。

模型类:

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public List<Comment> Comments { get; set; } = new List<Comment>();
}
public class Comment
{
    public int CommentId { get; set; }
    public string Content { get; set; }
    public int PostId { get; set; }
}

SQL 查询:

我们需要在 SQL 中使用 AS 关键字为结果集中的列指定别名,并确保列名与 C# 属性名匹配或通过 Dapper.SqlMapper.SetTypeMap 等方式配置,最简单的方式是让列名和属性名一致(或忽略大小写)。

SELECT 
    p.PostId, p.Title,
    c.CommentId, c.Content, c.PostId AS PostId_Comment -- 避免列名冲突
FROM Posts p
LEFT JOIN Comments c ON p.PostId = c.PostId
ORDER BY p.PostId;

Dapper 代码:

string sql = @"
    SELECT 
        p.PostId, p.Title,
        c.CommentId, c.Content, c.PostId AS PostId_Comment
    FROM Posts p
    LEFT JOIN Comments c ON p.PostId = c.PostId
    ORDER BY p.PostId;";
// 使用 Query<T> 的重载方法,传入一个 Action<IDataReader, T> 来处理多映射
var postMap = new Dictionary<int, Post>();
var posts = db.Query<Post, Comment, Post>(
    sql,
    (post, comment) =>
    {
        // Dapper 会为每一行数据调用这个委托
        // 1. 检查是否已经处理过这个 Post
        if (!postMap.TryGetValue(post.PostId, out var existingPost))
        {
            existingPost = post;
            postMap.Add(post.PostId, existingPost);
        }
        // 2. comment 不为空,则添加到列表中
        if (comment != null)
        {
            existingPost.Comments.Add(comment);
        }
        return existingPost;
    },
    splitOn: "CommentId" // 告诉 Dapper 从哪个列开始映射到下一个对象
).Distinct().ToList(); // 使用 Distinct 去重,因为一个帖子有 N 条评论,就会返回 N 行
foreach (var post in posts)
{
    Console.WriteLine($"Post: {post.Title}");
    foreach (var comment in post.Comments)
    {
        Console.WriteLine($"  - Comment: {comment.Content}");
    }
}

2 动态查询

Dapper 可以直接将查询结果映射到 dynamic 对象,这在处理不固定结构的表或临时查询时非常有用。

string sql = "SELECT Name, Age FROM Users WHERE Age > @MinAge";
// Query<dynamic> 返回一个 IEnumerable<dynamic>
var users = db.Query<dynamic>(sql, new { MinAge = 25 });
foreach (var user in users)
{
    // 访问动态对象的属性
    Console.WriteLine($"Dynamic User: {user.Name}, Age: {user.Age}");
}

3 多结果集查询

一个查询可以返回多个结果集,Dapper 可以轻松处理这种情况。

SQL:

SELECT * FROM Users WHERE Age > 30; -- 第一个结果集
SELECT COUNT(*) FROM Users;        -- 第二个结果集

Dapper 代码:

string sql = "SELECT * FROM Users WHERE Age > 30; SELECT COUNT(*) FROM Users;";
// QueryMultiple 用于处理多结果集
using (var multi = db.QueryMultiple(sql))
{
    // 读取第一个结果集,并映射到 User 列表
    var olderUsers = multi.Read<User>().ToList();
    // 读取第二个结果集,它是一个单行单列的值
    var userCount = multi.ReadFirst<int>(); // ReadFirst 直接读取第一个结果的第一行第一列
    Console.WriteLine("Users older than 30:");
    foreach (var user in olderUsers)
    {
        Console.WriteLine($"- {user.Name}");
    }
    Console.WriteLine($"Total users in database: {userCount}");
}

最佳实践

  1. 使用 using 语句管理连接:始终使用 using 语句来确保 IDbConnection 对象在使用后被正确关闭和释放,即使在发生异常时也是如此。
  2. 优先使用参数化查询:永远不要使用字符串拼接来构建 SQL,这极易导致 SQL 注入攻击,Dapper 的匿名对象完美支持参数化查询。
  3. 保持 SQL 的清晰和可维护性:虽然 Dapper 给了你写任意 SQL 的自由,但也要确保 SQL 本身是易于阅读和维护的,对于复杂的查询,考虑使用存储过程。
  4. 处理空值:当数据库中的列可能为 NULL 时,在 C# 模型中使用可空类型(如 int?, DateTime?)。
  5. 分离数据访问层:不要在你的 UI 层或业务逻辑层中直接写 Dapper 代码,创建一个仓储类或服务类来封装所有数据库操作,遵循关注点分离的原则。
// 示例:UserRepository.cs
public class UserRepository
{
    private readonly string _connectionString;
    public UserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }
    public User GetUserById(int id)
    {
        using (IDbConnection db = new SqlConnection(_connectionString))
        {
            string sql = "SELECT * FROM Users WHERE Id = @Id";
            return db.Query<User>(sql, new { Id = id }).FirstOrDefault();
        }
    }
    // ... 其他方法
}

完整示例项目

这是一个控制台应用的完整示例,展示了所有 CRUD 操作。

// Program.cs
using System;
using System.Data.SqlClient;
using System.Linq;
using Dapper;
// 定义 User 模型
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; }
    public DateTime CreatedAt { get; set; }
}
class Program
{
    static void Main(string[] args)
    {
        // !!! 替换为你的实际连接字符串 !!!
        string connectionString = "Server=localhost;Database=DapperDemo;Trusted_Connection=True;TrustServerCertificate=True;";
        // 1. 创建用户
        Console.WriteLine("Creating a new user...");
        using (IDbConnection db = new SqlConnection(connectionString))
        {
            var newUser = new { Name = "Frank", Email = "frank@example.com", Age = 50 };
            string insertSql = "INSERT INTO Users (Name, Email, Age) VALUES (@Name, @Email, @Age); SELECT CAST(SCOPE_IDENTITY() as int)";
            int newUserId = db.Query<int>(insertSql, newUser).Single();
            Console.WriteLine($"Created user with ID: {newUserId}");
        }
        // 2. 查询所有用户
        Console.WriteLine("\nAll users:");
        using (IDbConnection db = new SqlConnection(connectionString))
        {
            string sql = "SELECT * FROM Users";
            var users = db.Query<User>(sql).ToList();
            foreach (var user in users)
            {
                Console.WriteLine($"ID: {user.Id}, Name: {user.Name}, Email: {user.Email}, Age: {user.Age}");
            }
        }
        // 3. 更新用户
        Console.WriteLine("\nUpdating user with ID 2...");
        using (IDbConnection db = new SqlConnection(connectionString))
        {
            var updateData = new { Id = 2, Name = "Bob Updated", Age = 27 };
            string updateSql = "UPDATE Users SET Name = @Name, Age = @Age WHERE Id = @Id";
            int affectedRows = db.Execute(updateSql, updateData);
            Console.WriteLine($"{affectedRows} row(s) updated.");
        }
        // 4. 删除用户
        Console.WriteLine("\nDeleting user with ID 5 (if exists)...");
        using (IDbConnection db = new SqlConnection(connectionString))
        {
            string deleteSql = "DELETE FROM Users WHERE Id = @Id";
            int affectedRows = db.Execute(deleteSql, new { Id = 5 });
            Console.WriteLine($"{affectedRows} row(s) deleted.");
        }
    }
}

Dapper 是一个强大而简单的工具,它完美地平衡了性能和灵活性,当你需要:

  • 极致的性能:特别是在读多写少的场景下。
  • 完全的 SQL 控制:需要编写复杂的、特定于数据库的查询。
  • 轻量级集成:不想被一个庞大的 ORM 框架所束缚。

Dapper 是你的不二之选,对于简单的 CRUD 应用,它几乎可以满足所有需求;对于大型应用,它可以作为 EF Core 等重量级 ORM 的一个有力补充,专门处理性能关键路径的代码。

希望这份教程能帮助你快速上手 Dapper!