目录
介绍
Dapper是一种流行的SQL数据库映射器。通常,它将表行中的列字段映射到C#类实例中的属性以及从中映射属性。在数据管理的三层模型中,Dapper位于中间层,它从上层视图或表示层获取数据,并以数据库可以操作的形式将其呈现给较低的数据层。以类似的方式,它从数据库中获取数据,并以视图可以使用的形式将其呈现给上层。
为什么使用Dapper?
它易于使用,轻量级(47K)且速度快——大约是实体框架速度的两倍。作者是StackOverflow的一个团队,他们维护它并使用它来访问他们庞大的数据库。它并不打算替代实体框架,该框架是一个极好的对象关系映射器(ORM),具有您可能希望的所有功能,但是许多用户难以控制它,结果是采取不需要旅行的路线,并且携带的行李永远不会被打开包装。有了Dapper,您就坐在驾驶座上了。你引导它去哪里,并确定它到达那里时做什么。
实现
Dapper在 GitHub 上作为开源提供,并作为 Nuget 包提供。它不作为一个单独的实体存在,它是作为扩展方法的集合实现的,这些方法实际上扩展了IDbConnection接口。这类似于system.Linq扩展IEnumerable<T>的方式。Dapper查询的常用格式是将SQL语句作为string与语句所需的任何参数(表示为匿名类型的成员)结合使用。类似的东西。
int id = 7;
string sql = @"Select *from dbo.Employees where EmployeeID=@Id;";
using IDbConnection connection = _connectionCreator.CreateConnection();
var employee = await connection.QueryFirstOrDefaultAsync<Employee>(sql,new{Id=id});
在这里,SQL语句是从Employees表中EmployeeID列等于输入参数Id的行中选择所有列字段。“@”符号标识语句所需的输入参数。使用QueryFirstOrDefaultAsync<T>泛型方法是因为EmployeeID是主键,因此只有一条记录要搜索。匿名类型的成员名称与SQL语句引用的输入参数的名称匹配,并将其设置为局部变量id的值。Dapper将查询的输出映射到type T的实例。在此示例中,type T这被定义为Employee类。这是基本设置,还有其他Query方法从数据库请求数据,以及将命令传递到数据库的Execute方法,例如Insert、Delete和Update。通常有同一方法的异步和同步版本。Learn Dapper网站和 GitHub 上的Dapper Readme.md 文件有许多 Dapper 的优秀示例。
配置Dapper以在ASP.NET应用程序中使用
管理数据库连接
需要先打开数据库连接,然后才能使用,然后在查询完成后立即关闭和释放。这意味着每个Dapper扩展方法都有一定数量的连接管理工作。还需要引用特定于数据库服务器的IDbConnection实例。因此,这里可能存在大量代码重复,并且依赖于进行数据库调用的每个方法的所选IDbConnection类型。此问题的解决方案是将Dapper方法封装在IDatabaseContext类中,以便使用此类的任何内容都不需要知道Dapper或数据库连接。下面是调用Dapper的ExecuteAsync方法的IDatabaseContext方法的示例。
public async Task<T> QueryFirstOrDefaultAsync<T>(
string sql,
object? parameters = null,
CommandType? commandType = null)
{
using IDbConnection connection = _connectionCreator.CreateConnection();
var result = await connection.QueryFirstOrDefaultAsync<T>(sql, parameters,
commandType: commandType);
return result;
}
该ConnectionCreator.CreateConnection方法是一个工厂方法,它返回Connection的新实例。
public class MsSqlConnectionCreator : IConnectionCreator
{
protected ServerOptions _serverOptions;
public MsSqlConnectionCreator(IOptions<ServerOptions> serverOptionsSnapshot)
{
_serverOptions = serverOptionsSnapshot.Value;
}
public IDbConnection CreateConnection()
{
var connectionString = _serverOptions.MsSql;
return new SqlConnection(connectionString);
}
}
DatabaseContext从注入到其构造函数中的IOptions<T>泛型类型的实例中获取数据库连接字符串。
public SqlServerContext(IOptions<ServerOptions> serverOptionsSnapshot)
{
_serverOptions = serverOptionsSnapshot.Value;
_connectionString = _serverOptions.MsSql;
}
此技术被认为比传入配置管理器并使用管理器读取连接的替代选择更可取。它的优点是将ConfigurationManager作用域限制在Program.cs和Startup.cs中的其他应用程序构建器。appsettings.json 中ConnectionStrings部分的成员值在运行时绑定到单一实例ServerOptions类。
"ConnectionStrings": {
"MsSql": "Data Source=(localdb)\\ProjectModels;
Initial Catalog=Northwind;Integrated Security=True",
"MySql": "Server=127.0.0.1;user ID=root; Database=northwind; Password=Pa$$w0rd"
},
....
该ServerOptions类具有几个与ConnnectionStrings部分中的名称匹配的属性。
public class ServerOptions
{
//ConnectionStrings is not mapped,
//it's used to avoid a magic string when referencing
// the appsettings "ConnectionStrings" section
public const string ConnectionStrings = "ConnectionStrings";
public string MsSql { get; set; } = string.Empty;
public string MySql { get; set; } = string.Empty;
}
绑定在Program.cs中设置。
var section = builder.Configuration.GetSection("ConnectionStrings");
builder.Services.Configure<ServerOptions>(section);
管理SQL语句
拥有一个实现IDataAccess接口的类可以解决很多问题,但仍然需要由在表示层中运行的服务发出SQL语句。SQL语句基本上是数据库服务器的数据管理指令,理想情况下,应该位于数据库内部。实现此目的的方法是使用存储过程。存储过程是驻留在数据库中的预编译SQL语句。在 SQL Server对象资源管理器中,它们位于数据库的可编程性文件夹中。下面演示如何从罗斯文示例数据库调用CustOrderHist存储过程。
string customerID = "ANTON";//input param
var results = await _databaseContext.QueryAsync<ProductSummary>
(_storedProcedureId.CustomerOrderHistory,
new { CustomerID = customerID },
commandType: CommandType.StoredProcedure);
通过使用命名参数初始化commandType参数。命名参数避免了必须为在所需参数之前排序的参数输入大量null值。过程名称作为自定义StoredProcedureId类的属性传入。
public string CustomerOrderHistory { get; } = "dbo.CustOrderHist";
Northwind的Orders表中有超过800条记录,但Dapper可以在眨眼间完成此过程。
示例应用程序
示例应用程序是使用该Northwind数据库的ASP.NET 7控制台应用。它需要更新 appsettings.json 文件中的连接字符串以指向数据库服务器的实例。这些示例将使用TSql或MySql语句运行,具体取决于所选的SQL Server。它们并不是确定的,大多数都是不言自明的,但有几个可能需要一些澄清。
例 1
此查询涉及两个类。该Order类对Orders表进行建模,该Employee类对Employees表进行建模。该Order类还具有引用Employees的表主键的外键,此外,它还具有引用Employee类实例的属性。
public class Order
{
public int OrderID { get; set; }
public string? CustomerID { get; set; }
public int EmployeeID { get; set; }
public DateTime OrderDate { get; set; }
public int ShipperID { get; set; }
public Employee? Employee { get; set; }
}
SQL语句在匹配EmployeeIDs时连接两个表,并按Employee的LastName并且之后按FirstName对查询进行排序。
string sql = @"select o.EmployeeID,o.OrderId,e.EmployeeID,e.FirstName,e.LastName
from dbo.Orders o
inner join dbo.Employees e
on o.EmployeeID = e.EmployeeID
order by e.LastName, e.FirstName";
Dapper获取此查询的结果并将其映射到Order项集合。
var employeeOrders= await _databaseContext.QueryAsync<Order, Employee>(sql,
(order, employee) =>
{
order.Employee = employee;
return order;
}, splitOn: "EmployeeID");
为了使 Dapper 能够有效地映射,需要为其提供一个函数,该函数将类属性设置为实例并返回类实例。还需要告诉 Dapper 在查询返回的所有列中,与表相关的列结束和表中的列开始的位置。假定这是当遇到值为 “” 的列名时。但是,在这种情况下,相关标识列的名称是,因此需要将参数设置为该名称。OrderEmployeeEmployeeOrderOrdersEmployeesToUpper()IDEmployeeIDsplitOn
例 2
此示例包含一个SQL语句,该语句通常会导致异常,因为它具有一个集合作为输入参数,并且通常不允许使用参数集合。Dapper通过将集合解构为SQL解析器将接受的格式的一系列单个参数来规避该限制。该语句将该集合引用为@countries。局部变量countryArray使用匿名类型传递到QueryAsync方法中,其成员必须与引用的参数命名相同,并设置为局部数组实例。该查询获取数组中指定的四个国家/地区中每个国家/地区供应的产品总数。
string[] countryArray = new[] { "France", "Germany", "UK", "USA" };
string sql = @"Select Suppliers.Country As SuppliersCountry,COUNT(*) as ProductCount
From Suppliers join Products on Suppliers.SupplierID=Products.SupplierID
where Suppliers.Country in @countries
Group by Suppliers.Country
Order by ProductCount Desc;";
var results = await _dba.QueryAsync<(string SuppliersCountry, int ProductCount)>
(sql, new { countries = countryArray });
如果你不熟悉SQL,上面的语句可能看起来有点吓人。一个有用的提示是尝试从后到前阅读它。因此,从最后一个子句开始,输出按ProductCount降序排序。它仅按Suppliers.Country输入参数集合中Suppliers.Country存在的位置进行分组。在Suppliers.SupplierID等于Products.SupplierID时,通过将Suppliers表中的每一行连接到Products表中的匹配行来获得数据。所选的行和列输出将Suppliers.Country作为SuppliersCountry列的成员,并将该组中所有项目的计数作为ProductCount列的成员。在SQL中,分组表示为键/值对,其中值是聚合函数的结果,该聚合函数使用组的成员项集合作为输入参数。这是压缩到几行代码中的许多功能。SQL是编程语言的哥伦布;它比看起来更聪明。
结论
使用Dapper扩展方法作为数据库映射器是使用实体框架提供的类型的有用且有效的替代方法。在不需要实体框架的高级功能的情况下尤其如此。
经验 教训
重要的是,等待Dapper的所有async方法。简单地调用该方法并返回Task以便另一个方法可以await它,这样做并不是一个好主意。尽管该技术避免了与使用async关键字相关的开销,但它也会导致null引用异常。这些方法是从using语句中调用的,一旦返回未完成的Task方法,该using语句就会确保连接关闭,结果是Dapper保持高位和干燥。我发现这是艰难的。
https://www.codeproject.com/Articles/5351042/Dapper-The-Pocket-Rocket-Mapz