如何使用Dapper映射嵌套对象的列表

160

我目前使用Entity Framework进行数据库访问,但想看看Dapper。我的类看起来像这样:

public class Course{
   public string Title{get;set;}
   public IList<Location> Locations {get;set;}
   ...
}

public class Location{
   public string Name {get;set;}
   ...
}

因此,可以在多个位置上授课。Entity Framework 会为我执行映射,因此我的 Course 对象会填充一个位置列表。对于 Dapper,我该如何处理这种情况?是否可能,或者我必须分几步查询来完成?


2
相关问题:https://dev59.com/5mw15IYBdhLWcg3w0fAV - Jeroen K
1
这是我的解决方案:https://dev59.com/5mw15IYBdhLWcg3w0fAV#57395072 - Sam Sch
8个回答

215

或者,您可以使用一个带有查找的查询:

var lookup = new Dictionary<int, Course>();
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course))
            lookup.Add(c.Id, course = c);
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(l); /* Add locations to course */
        return course;
     }).AsQueryable();
var resultList = lookup.Values;

请查看这里 https://www.tritac.com/blog/dappernet-by-example/


15
这帮我节省了大量时间。我需要进行一个修改,其他人可能也需要这样做,就是加入 splitOn 参数,因为我没有使用默认的 "Id"。 - Bill Sambrone
2
对于 LEFT JOIN,您将在位置列表中获得一个空项。通过 var items = lookup.Values; items.ForEach(x => x.Locations.RemoveAll(y => y == null)); 将它们移除。 - Choco Smith
1
@BillSambrone 你好,我有一个字符串列表而不是位置列表,我应该在哪里分割? - Quentin
2
对于LEFT JOIN:不需要再进行另一个Foreach。只需在添加之前检查即可:if(l!= null)course.Locations.Add(l)。 - jpgrassi
2
由于您正在使用字典。如果您使用QueryMultiple并分别查询课程和位置,然后使用相同的字典将位置分配给课程,这样会更快吗?本质上是相同的事情,减去了内部连接,这意味着SQL不会传输太多字节? - MIKE
显示剩余2条评论

64

Dapper不是完整的ORM,它不处理查询和类似操作的神奇生成。

对于您的特定示例,以下内容可能有效:

获取课程:

var courses = cnn.Query<Course>("select * from Courses where Category = 1 Order by CreationDate");

获取相关的映射:

var mappings = cnn.Query<CourseLocation>(
   "select * from CourseLocations where CourseId in @Ids", 
    new {Ids = courses.Select(c => c.Id).Distinct()});

获取相关位置

var locations = cnn.Query<Location>(
   "select * from Locations where Id in @Ids",
   new {Ids = mappings.Select(m => m.LocationId).Distinct()}
);

把它全部映射起来

让读者来完成这个步骤,你需要创建一些地图并迭代你的课程,将位置填入其中。

注意:如果你的查询超过了2100条(对于Sql Server),使用in技巧可能无效,此时你可能需要修改查询语句为select * from CourseLocations where CourseId in (select Id from Courses ... ),如果是这种情况,最好一次性获取所有结果使用QueryMultiple


1
感谢你的澄清,Sam。像你上面描述的那样,我只是运行了第二个查询来获取位置,并手动将它们分配给课程。我只是想确保我没有错过任何可以让我用一个查询完成的东西。 - b3n
3
在一个大型应用程序中,如果集合经常以类似示例中的方式暴露在域对象上,你认为这段代码应该放在哪里?(假设您希望从代码中的多个不同位置调用类似完全构造的[Course]实体)是在构造函数中?在类工厂中?还是其他地方? - tbone
我对CourseLocations感到困惑,这是问题中从未提到的一个表/结构。OP只列出了2个实体 - 这只是假设可能存在一个中间表吗? - PandaWood

46

无需使用lookup字典

var coursesWithLocations = 
    conn.Query<Course, Location, Course>(@"
        SELECT c.*, l.*
        FROM Course c
        INNER JOIN Location l ON c.LocationId = l.Id                    
        ", (course, location) => {
            course.Locations = course.Locations ?? new List<Location>();
            course.Locations.Add(location); 
            return course;
        }).AsQueryable();

6
唯一的问题是您将在每个位置记录上复制标题。如果每门课程有许多位置,则可能会产生大量数据重复,这将增加带宽,需要更长时间进行解析/映射,并使用更多内存来读取所有内容。 - Daniel Lorenz
21
我不确定这是否按照我预期的方式工作。我有一个包含3个相关对象的父对象。我使用的查询返回3行,第一列描述了每个行对应的父对象,这些值在每一行中都是相同的;通过ID分割可以识别每个唯一的子对象。我的结果是3个重复的父对象,每个都有3个孩子... 应该是一个父对象和3个孩子。 - topwik
5
@topwik 是正确的。对我来说也没有按预期工作。 - Maciej Pszczolinski
6
我最终在这段代码中得到了三个父元素,每个父元素都有一个子元素。不确定为什么我的结果与 @topwik 不同,但它仍然无法正常工作。 - th3morg
6
这个答案是错误的,因为在数据库中有一门课程和三个地点,会返回三门课程,每门课程只有一个地点。 - Delphi.Boy
显示剩余5条评论

40

我知道我来晚了,但还有另外一个选择。您可以在此处使用QueryMultiple。大致如下:

var results = cnn.QueryMultiple(@"
    SELECT * 
      FROM Courses 
     WHERE Category = 1 
  ORDER BY CreationDate
          ; 
    SELECT A.*
          ,B.CourseId 
      FROM Locations A 
INNER JOIN CourseLocations B 
        ON A.LocationId = B.LocationId 
INNER JOIN Course C 
        ON B.CourseId = B.CourseId 
       AND C.Category = 1
");

var courses = results.Read<Course>();
var locations = results.Read<Location>(); //(Location will have that extra CourseId on it for the next part)
foreach (var course in courses) {
   course.Locations = locations.Where(a => a.CourseId == course.CourseId).ToList();
}

8
需要注意的一点是,如果有很多地点/课程,应该循环遍历一次地点并将它们放入字典查找中,这样可以获得N log N的速度而不是N^2的速度。在大型数据集中会有很大的差异。 - Daniel Lorenz

10

很抱歉总是迟到。对于我来说,使用Dictionary(像Jeroen K一样)在性能和可读性方面更加容易。此外,为了避免跨位置头部重复,我使用Distinct()来消除潜在的重复项:

string query = @"SELECT c.*, l.*
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id";
using (SqlConnection conn = DB.getConnection())
{
    conn.Open();
    var courseDictionary = new Dictionary<Guid, Course>();
    var list = conn.Query<Course, Location, Course>(
        query,
        (course, location) =>
        {
            if (!courseDictionary.TryGetValue(course.Id, out Course courseEntry))
            {
                courseEntry = course;
                courseEntry.Locations = courseEntry.Locations ?? new List<Location>();
                courseDictionary.Add(courseEntry.Id, courseEntry);
            }

            courseEntry.Locations.Add(location);
            return courseEntry;
        },
        splitOn: "Id")
    .Distinct()
    .ToList();

    return list;
}

6

有些东西缺失了。如果您在SQL查询中没有指定每个Locations字段,那么Location对象将无法填充。看一下:

var lookup = new Dictionary<int, Course>()
conn.Query<Course, Location, Course>(@"
    SELECT c.*, l.Name, l.otherField, l.secondField
    FROM Course c
    INNER JOIN Location l ON c.LocationId = l.Id                    
    ", (c, l) => {
        Course course;
        if (!lookup.TryGetValue(c.Id, out course)) {
            lookup.Add(c.Id, course = c);
        }
        if (course.Locations == null) 
            course.Locations = new List<Location>();
        course.Locations.Add(a);
        return course;
     },
     ).AsQueryable();
var resultList = lookup.Values;

在查询中使用l.*,我得到了位置列表但没有数据。


1

不确定是否有人需要,但我有一个动态版本,没有模型可以进行快速和灵活的编程。

var lookup = new Dictionary<int, dynamic>();
conn.Query<dynamic, dynamic, dynamic>(@"
    SELECT A.*, B.*
    FROM Client A
    INNER JOIN Instance B ON A.ClientID = B.ClientID                
    ", (A, B) => {
        // If dict has no key, allocate new obj
        // with another level of array
        if (!lookup.ContainsKey(A.ClientID)) {
            lookup[A.ClientID] = new {
                ClientID = A.ClientID,
                ClientName = A.Name,                                        
                Instances = new List<dynamic>()
            };
        }

        // Add each instance                                
        lookup[A.ClientID].Instances.Add(new {
            InstanceName = B.Name,
            BaseURL = B.BaseURL,
            WebAppPath = B.WebAppPath
        });

        return lookup[A.ClientID];
    }, splitOn: "ClientID,InstanceID").AsQueryable();

var resultList = lookup.Values;
return resultList;

0

还有一种使用JSON结果的方法。尽管已经有很好的解释,但我想到了另一种获取结果的方法。

创建一个存储过程或选择查询以JSON格式返回结果。然后将结果对象反序列化为所需的类格式。请查看示例代码。

using (var db = connection.OpenConnection())
{                
  var results = await db.QueryAsync("your_sp_name",..);
  var result = results.FirstOrDefault();    
                    
  string Json = result?.your_result_json_row;
                   
  if (!string.IsNullOrEmpty(Json))
  {
     List<Course> Courses= JsonConvert.DeserializeObject<List<Course>>(Json);
  }
    
  //map to your custom class and dto then return the result        
}

这是另一种思考方式。请审核相同内容。

相对于多映射和/或使用字典进行查找,这种方法的性能如何?假设我需要从数据库中获取1000条记录,每条记录包含8个属性和两个子列表,每个子列表包含2个对象,每个对象又包含3个属性。使用FOR JSON PATH不是会在DB服务器上产生相当大的开销(当然这取决于它需要构建的对象结构)吗? - RollerMobster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接