Dapper

Dapper是.NET平台下的一款轻量级、高性能的半自动ORM框架,由StackOverflow公司开发。相比Entity Framework框架,Dapper非常轻量级,我们使用Dapper时仍需要手动编写SQL语句,数据的映射则由Dapper完成。

官方Github地址:https://github.com/DapperLib/Dapper

安装Dapper

本篇后续我们以最新的.NET Core 8环境进行演示。我们可以使用Visual Studio内置的NuGet包管理工具安装Dapper,对于.NET Core环境,我们也可以直接使用dotnet命令行工具安装。

dotnet add package Dapper

此外我们还需要安装对应数据库的Provider,我们这里以MySQL作为例子。

dotnet add package MySql.Data

在控制台工程中使用Dapper例子

Dapper使用起来非常简单,下面是查询单个对象的例子。

using Dapper;
using MySql.Data.MySqlClient;
using System.Data;
using System.Text.Json;

string connectionString = "Server=localhost;Database=netstore;User=root;Password=root;CharSet=utf8mb4;";
using (IDbConnection connection = new MySqlConnection(connectionString))
{
    string sql = "select user_id as UserId, username as Username, email as Email, password as Password from t_user where user_id = @UserId";
    connection.Open();
    User user = connection.QuerySingle<User>(sql, new { UserId = 1 });
    Console.WriteLine(JsonSerializer.Serialize(user));
}

代码中,我们首先创建了MySqlConnection连接对象,它实现了IDbConnection接口,而Dapper为连接对象添加了许多扩展方法,QuerySingle方法可以用于查询单个对象,它的泛型参数是映射的类型,函数参数分别是SQL语句和查询参数。

查询完成后,我们将这个对象转换成了JSON并打印了出来。

注意:对于用户输入的查询字段,我们必须使用参数化查询占位符,不可以拼接SQL字符串,否则将产生SQL注入漏洞。不仅仅是Dapper,任何ORM框架或是操作数据库的类库都不可以使用用户的输入内容拼接SQL字符串。

Dapper单表增删改查

Dapper提供了许多方法用于增删改查映射操作,这里我们详细学习一下这些操作方法。

查询映射配置

实际开发中(尤其是使用MySQL),我们可能经常遇到数据库的命名规范和C#对象属性命名规范不同的情况,一种解决方案是像上面例子一样在SQL中使用as定义查询结果列的名字,但这种方式只是一个相对取巧的办法,比较规范的做法是手动配置Dapper处理数据库查询结果字段的映射关系。下面例子中,User是我们对应表结构的实体类。

using Dapper;

namespace Gacfox.DemoDapper.Models;

public class User
{
    public required long UserId { get; set; }
    public required string Username { get; set; }
    public required string Email { get; set; }
    public required string Password { get; set; }

    public static CustomPropertyTypeMap TypeMap => new(typeof(User), (type, columnName) =>
    {
        Dictionary<string, string> userColumnDict = new Dictionary<string, string>()
        {
            { "user_id", "UserId" },
            { "username", "Username" },
            { "email", "Email" },
            { "password", "Password" }
        };

        return type.GetProperties()
            .First(prop =>
                userColumnDict.ContainsKey(columnName) && prop.Name == userColumnDict[columnName]);
    });
}

代码中TypeMap是一个Dapper的CustomPropertyTypeMap字段映射配置,这个配置对象其实很好理解,构造函数的第二个参数是一个函数,函数中传入类型和数据库字段名,我们只需要在这里定义数据库字段和实体类属性名的映射逻辑即可。

下面代码首先向Dapper框架注册了User实体类的映射配置,然后调用Dapper查询数据库。

using Dapper;
using Gacfox.DemoDapper.Models;
using MySql.Data.MySqlClient;
using System.Data;
using System.Text.Json;

// 设置字段映射
SqlMapper.SetTypeMap(typeof(User), User.TypeMap);

string connectionString = "Server=localhost;Database=netstore;User=root;Password=root;CharSet=utf8mb4;";
using (IDbConnection connection = new MySqlConnection(connectionString))
{
    string sql = "select user_id, username, email, password from t_user where user_id = @UserId";
    connection.Open();
    User user = connection.QuerySingle<User>(sql, new { UserId = 1 });
    Console.WriteLine(JsonSerializer.Serialize(user));
}

调用Dapper查询数据库代码和之前例子类似。

查询单行数据

Dapper中,查询单行数据主要有以下几个方法。

方法名 说明
QuerySingle<T> 查询单行数据,如果没有或返回多行则会抛出InvalidOperationException
QuerySingleOrDefault<T> 查询单行数据,如果没有返回null,如果返回多行抛出InvalidOperationException
QueryFirst<T> 取查询结果的第一行数据,如果没有返回InvalidOperationException
QueryFirstOrDefault<T> 取查询结果的第一行数据,如果没有返回null

查询单行数据的代码例子前面我们已经给出,这里就不重复编写了。

查询多行数据

查询多行数据可以使用Query方法。

方法名 说明
Query<T> 查询指定类型的可迭代对象

下面例子我们将多行查询结果转换为List对象。

string connectionString = "Server=localhost;Database=netstore;User=root;Password=root;CharSet=utf8mb4;";
using (IDbConnection connection = new MySqlConnection(connectionString))
{
    string sql = "select user_id, username, email, password from t_user";
    connection.Open();
    List<User> userList = connection.Query<User>(sql).ToList();
    Console.WriteLine(JsonSerializer.Serialize(userList));
}

查询标量

方法名 说明
ExecuteScalar<T> 查询指定类型标量

代码例子如下。

string connectionString = "Server=localhost;Database=netstore;User=root;Password=root;CharSet=utf8mb4;";
using (IDbConnection connection = new MySqlConnection(connectionString))
{
    string sql = "select count(*) from t_user";
    connection.Open();
    int count = connection.ExecuteScalar<int>(sql);
    Console.WriteLine(count);
}

关于分页查询

Dapper并没有内置通用的分页查询功能,因此我们需要基于所用数据库的分页SQL实现。

执行非查询SQL语句

增删改都属于非查询SQL语句,Dapper中我们可以使用Execute方法执行这类SQL语句。

方法名 说明
Execute 执行非查询SQL语句,返回值是受影响的行数

代码例子如下。

string connectionString = "Server=localhost;Database=netstore;User=root;Password=root;CharSet=utf8mb4;";
using (IDbConnection connection = new MySqlConnection(connectionString))
{
    string sql = "insert into t_user (username, email, password) values (@Username, @Email, @Password)";
    connection.Open();
    int affectedRows = connection.Execute(sql, new
    {
        Username = "斯派克",
        Email = "spike@gmail.com",
        Password = "123456"
    });
    Console.WriteLine(affectedRows);
}

Dapper关联查询

Dapper中的关联查询用于单条SQL返回多个嵌套对象数据的情况,这通常意味着你的SQL中包含JOIN操作。注意Dapper所谓的关联查询和全功能ORM框架的关联查询有很大区别,Dapper中所谓的“关联查询”仍是一种简单的数据记录到实体类对象的映射配置,它并没有像全功能ORM框架那样有懒加载等功能,Dapper的这个关联查询需要手动指定一个拆分列对结果行进行拆分,因此这对SQL语句查询结果列的顺序也有要求。这听起来可能有点难以理解,我们直接看一个例子。

下面代码中,UserUserCategory是我们定义的两个实体类对象,它们是一对多关系,其中User包含UserCategory的单向引用。

using Dapper;

namespace Gacfox.DemoDapper.Models;

public class User
{
    public required long UserId { get; set; }
    public required string Username { get; set; }
    public required string Email { get; set; }
    public required string Password { get; set; }
    public UserCategory? UserCategory { get; set; }

    public static CustomPropertyTypeMap TypeMap => new(typeof(User), (type, columnName) =>
    {
        Dictionary<string, string> userColumnDict = new Dictionary<string, string>()
        {
            { "user_id", "UserId" },
            { "username", "Username" },
            { "email", "Email" },
            { "password", "Password" }
        };

        return type.GetProperties()
            .First(prop =>
                userColumnDict.ContainsKey(columnName) && prop.Name == userColumnDict[columnName]);
    });
}
using Dapper;

namespace Gacfox.DemoDapper.Models;

public class UserCategory
{
    public required long UserCategoryId { get; set; }
    public required string CategoryName { get; set; }

    public static CustomPropertyTypeMap TypeMap => new(typeof(UserCategory), (type, columnName) =>
    {
        Dictionary<string, string> userCategoryColumnDict = new Dictionary<string, string>()
        {
            { "user_category_id", "UserCategoryId" },
            { "category_name", "CategoryName" }
        };

        return type.GetProperties()
            .First(prop =>
                userCategoryColumnDict.ContainsKey(columnName) && prop.Name == userCategoryColumnDict[columnName]);
    });
}

下面代码我们使用JOIN查询取出了User列表,同时关联取出UserCategory

using Dapper;
using Gacfox.DemoDapper.Models;
using MySql.Data.MySqlClient;
using System.Data;
using System.Text.Json;

// 设置字段映射
SqlMapper.SetTypeMap(typeof(User), User.TypeMap);
SqlMapper.SetTypeMap(typeof(UserCategory), UserCategory.TypeMap);

string connectionString = "Server=localhost;Database=netstore;User=root;Password=root;CharSet=utf8mb4;";
using (IDbConnection connection = new MySqlConnection(connectionString))
{
    string sql =
        "select u.user_id,u.username,u.email,u.password,uc.user_category_id,uc.category_name from t_user u inner join t_user_category uc on u.user_category_id=uc.user_category_id";
    connection.Open();
    List<User> userList = connection.Query<User, UserCategory, User>(sql, ((user, category) =>
    {
        user.UserCategory = category;
        return user;
    }), splitOn: "user_category_id").ToList();
    Console.WriteLine(JsonSerializer.Serialize(userList));
}

首先我们可以注意到,代码中我们使用了Query方法进行查询,不过这里泛型参数指定了3个,这3个泛型参数和后面的函数参数类型是对应的,3个泛型参数分别是函数的参数列表类型和返回值类型,它们都是JOIN查询中涉及需要Dapper为我们自动映射的对象,函数内部我们设置了数据实体对象的关联关系。

注意Query方法中我们还添加了splitOn这个命名参数,它是用于“分割”数据的字段名,它告知了Dapper从数据行的哪一列上开始拆分。前面我们的函数参数列表是UserUserCategory,在SQL中,我们查询的字段顺序就要和User对象、user_category_id拆分列、UserCategory对象这个顺序保持一致。

理解了上面的操作,我们便学会Dapper中的“关联查询”了。

Dapper异步操作

Dapper框架支持异步操作,前面介绍的各种方法如QueryExecute等都有对应的异步版本QueryAsyncExecuteAsync等,使用方法和同步版本基本一致。在ASP.NET Core服务端开发中,结合Dapper时使用这些异步写法能够有效提升系统吞吐量。

ASP.NET Core中集成Dapper

ASP.NET Core中集成Dapper非常简单,我们在Program.cs中将IDbConnection对象注册为服务即可,在DI容器托管的对象中我们通过依赖注入获取IDbConnection,实体类的定义和Dapper框架的调用和前面例子相同。

builder.Services.AddScoped<IDbConnection>(sp => new MySqlConnection(
    builder.Configuration.GetConnectionString("DefaultConnection")
));
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap