Dapper是.NET平台下的一款轻量级、高性能的半自动ORM框架,由StackOverflow公司开发。相比Entity Framework框架,Dapper非常轻量级,我们使用Dapper时仍需要手动编写SQL语句,数据的映射则由Dapper完成。
官方Github地址:https://github.com/DapperLib/Dapper
本篇后续我们以最新的.NET Core 8环境进行演示。我们可以使用Visual Studio内置的NuGet包管理工具安装Dapper,对于.NET Core环境,我们也可以直接使用dotnet
命令行工具安装。
dotnet add package Dapper
此外我们还需要安装对应数据库的Provider,我们这里以MySQL作为例子。
dotnet add package MySql.Data
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提供了许多方法用于增删改查映射操作,这里我们详细学习一下这些操作方法。
实际开发中(尤其是使用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语句,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中的关联查询用于单条SQL返回多个嵌套对象数据的情况,这通常意味着你的SQL中包含JOIN
操作。注意Dapper所谓的关联查询和全功能ORM框架的关联查询有很大区别,Dapper中所谓的“关联查询”仍是一种简单的数据记录到实体类对象的映射配置,它并没有像全功能ORM框架那样有懒加载等功能,Dapper的这个关联查询需要手动指定一个拆分列对结果行进行拆分,因此这对SQL语句查询结果列的顺序也有要求。这听起来可能有点难以理解,我们直接看一个例子。
下面代码中,User
和UserCategory
是我们定义的两个实体类对象,它们是一对多关系,其中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从数据行的哪一列上开始拆分。前面我们的函数参数列表是User
、UserCategory
,在SQL中,我们查询的字段顺序就要和User
对象、user_category_id
拆分列、UserCategory
对象这个顺序保持一致。
理解了上面的操作,我们便学会Dapper中的“关联查询”了。
Dapper框架支持异步操作,前面介绍的各种方法如Query
、Execute
等都有对应的异步版本QueryAsync
、ExecuteAsync
等,使用方法和同步版本基本一致。在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")
));