Dapper作为轻量级的ORM框架,默认支持简单的单对象映射,但在实际业务中经常会遇到实体包含嵌套复杂类型的情况,比如订单实体中包含用户实体、商品实体等多层嵌套结构,这时候就需要使用特定的映射方式来实现深层对象的映射。
Dapper嵌套映射的基础场景
假设我们有两个嵌套的实体类,用户信息和用户详情信息是一对一的关系,用户实体中包含用户详情的嵌套对象:
// 用户详情实体
public class UserDetail
{
public int DetailId { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
}
// 用户实体,包含嵌套的UserDetail对象
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
public UserDetail Detail { get; set; }
}
对应的数据库查询会同时返回用户表和用户详情表的字段,这时候就需要把查询结果拆分到不同的对象中。
使用Query方法的分割映射
Dapper的Query方法支持通过分割函数来实现嵌套对象的映射,核心思路是把查询结果按照某个分割字段拆分,分别映射到不同的对象上。
下面是具体的实现代码:
using Dapper;
using System.Data.SqlClient;
using System.Linq;
public class UserRepository
{
private readonly string _connectionString = "Server=.;Database=TestDB;Trusted_Connection=True;";
public User GetUserWithDetail(int userId)
{
using (var connection = new SqlConnection(_connectionString))
{
// 查询语句同时关联用户表和用户详情表
string sql = @"SELECT u.Id AS UserId, u.Name AS UserName,
d.Id AS DetailId, d.Address, d.Phone
FROM Users u
LEFT JOIN UserDetails d ON u.Id = d.UserId
WHERE u.Id = @UserId";
// 使用分割映射,分割字段是DetailId,当DetailId变化时拆分对象
var user = connection.Query<User, UserDetail, User>(sql,
(userObj, detailObj) =>
{
userObj.Detail = detailObj;
return userObj;
},
new { UserId = userId },
splitOn: "DetailId" // 指定分割字段,对应查询结果中的DetailId列
).FirstOrDefault();
return user;
}
}
}
这里需要注意splitOn参数的设置,它指定了查询结果中哪个字段开始属于第二个对象的映射字段,默认是Id,如果第二个对象的起始字段不是Id就需要手动指定。
一对多嵌套场景的映射
如果嵌套关系是用户包含多个订单的一对多场景,实体结构如下:
// 订单实体
public class Order
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
public int UserId { get; set; }
}
// 用户实体,包含订单集合
public class UserWithOrders
{
public int UserId { get; set; }
public string UserName { get; set; }
public List<Order> Orders { get; set; } = new List<Order>();
}
一对多的映射需要手动处理对象的合并,因为Dapper不会自动把多条记录合并到同一个集合里:
public UserWithOrders GetUserWithOrders(int userId)
{
using (var connection = new SqlConnection(_connectionString))
{
string sql = @"SELECT u.Id AS UserId, u.Name AS UserName,
o.Id AS OrderId, o.Amount, o.UserId
FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
WHERE u.Id = @UserId";
var userDict = new Dictionary<int, UserWithOrders>();
var result = connection.Query<UserWithOrders, Order, UserWithOrders>(sql,
(user, order) =>
{
// 如果字典中不存在当前用户,就先添加
if (!userDict.TryGetValue(user.UserId, out var currentUser))
{
currentUser = user;
userDict.Add(user.UserId, currentUser);
}
// 如果订单不为空,就添加到用户的订单集合中
if (order != null)
{
currentUser.Orders.Add(order);
}
return currentUser;
},
new { UserId = userId },
splitOn: "OrderId"
).FirstOrDefault();
return userDict.Values.FirstOrDefault();
}
}
自定义映射规则处理复杂场景
如果遇到更复杂的嵌套结构,比如三层以上的嵌套,或者字段名和实体属性名不匹配的情况,可以使用Dapper的自定义映射功能:
using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
// 自定义类型映射器
public class CustomTypeMapper : SqlMapper.ITypeMap
{
private readonly Dictionary<string, string> _propertyColumnMapping;
public CustomTypeMapper(Dictionary<string, string> propertyColumnMapping)
{
_propertyColumnMapping = propertyColumnMapping;
}
public ConstructorInfo FindConstructor(string[] names, Type[] types)
{
return typeof(User).GetConstructor(Type.EmptyTypes);
}
public ConstructorInfo FindExplicitConstructor()
{
return null;
}
public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
{
return null;
}
public SqlMapper.IMemberMap GetMember(string columnName)
{
// 如果列名在映射字典中,就返回对应的属性映射
if (_propertyColumnMapping.TryGetValue(columnName, out var propertyName))
{
var property = typeof(User).GetProperty(propertyName);
if (property != null)
{
return new DefaultMemberMap(property);
}
}
return null;
}
}
// 默认成员映射实现
public class DefaultMemberMap : SqlMapper.IMemberMap
{
private readonly PropertyInfo _property;
public DefaultMemberMap(PropertyInfo property)
{
_property = property;
}
public string ColumnName => _property.Name;
public Type MemberType => _property.PropertyType;
public PropertyInfo Property => _property;
public FieldInfo Field => null;
public ParameterInfo Parameter => null;
}
public class CustomMappingRepository
{
public void TestCustomMapping()
{
// 注册自定义映射规则
var mapping = new Dictionary<string, string>
{
{ "user_id", "UserId" },
{ "user_name", "UserName" },
{ "detail_id", "DetailId" },
{ "user_address", "Address" }
};
SqlMapper.SetTypeMap(typeof(User), new CustomTypeMapper(mapping));
using (var connection = new SqlConnection(_connectionString))
{
string sql = @"SELECT user_id, user_name, detail_id, user_address FROM UserView WHERE user_id = 1";
var user = connection.QueryFirstOrDefault<User>(sql);
}
}
}
映射注意事项
- 分割字段
splitOn必须存在于查询结果中,且是第二个及之后对象的第一个映射字段 - 一对多映射时需要手动处理对象的去重和集合合并,避免重复创建父对象
- 自定义映射适合字段名和属性名差异较大的场景,不需要修改实体类就可以实现映射
- 如果嵌套对象可能为null,映射时需要做空值判断,避免出现空引用异常
通过上述几种方式,就可以覆盖大部分Dapper嵌套复杂类型和深层对象映射的场景,开发者可以根据实际的业务结构选择合适的映射方案。