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


127

我目前正在使用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做到这一点,甚至有可能还是我必须在几个查询步骤中做到这一点?



这是我的解决方案:stackoverflow.com/a/57395072/8526957
Sam Sch

Answers:


57

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()}
);

全部映射

将其留给读者,您将创建一些地图,然后遍历包含位置的课程。

请注意in如果您的查询少于2100个(Sql Server),则可以使用该技巧,如果查询次数较多,则可能需要将查询修改select * from CourseLocations where CourseId in (select Id from Courses ... )为这种情况,那么您最好一次使用所有结果即可QueryMultiple


感谢您的澄清,山姆。像您上面描述的那样,我只是在运行第二个查询以获取位置并将其手动分配给课程。我只是想确保自己不会错过任何可以让我用一个查询来完成的事情。
2011年

2
萨姆(Sam),在一个大型应用程序中(如示例所示),定期在域对象上公开集合,您会建议将此代码放在物理位置?(假设您想从代码中的许多不同地方使用一个类似的完全构造的[Course]实体)在构造函数中?在班级工厂?别的地方?
tbone

177

或者,您可以对查询使用一个查询:

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/


9
这节省了我很多时间。我需要其他人可能需要进行的一项修改是包含splitOn:参数,因为我没有使用默认的“ Id”。
Bill Sambrone 2015年

1
对于LEFT JOIN,您将在位置列表中获得一个空项目。通过var items = lookup.Values删除它们;items.ForEach(x => x.Locations.RemoveAll(y => y == null));
Choco Smith

除非我在第1行的末尾加上分号并删除“ AsQueryable()”之前的逗号,否则无法编译此代码。我会编辑答案,但在我之前的62个支持者似乎认为还可以,也许我丢失了某些东西……
bitcoder

1
对于LEFT JOIN:不需要在其上进行另一个Foreach。只需在添加它之前检查一下:if(l!= null)course.Locations.Add(l)。
jpgrassi

1
由于您正在使用字典。如果您分别使用QueryMultiple和查询的课程和位置,然后使用同一词典为课程分配位置,这会更快吗?本质上是同一件事减去内部联接,这意味着sql不会传输那么多字节吗?
MIKE '18年

43

无需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();

3
这非常好-我认为这应该是选择的答案。但是,这样做的人要提防*,因为这会影响性能。
cr1pto

2
唯一的问题是,您将在每个位置记录上复制标题。如果每个课程有很多位置,则可能会有大量的数据重复遍及整个网络,这将增加带宽,花费更长的时间进行解析/映射以及使用更多的内存来读取所有这些数据。
丹尼尔·洛伦兹

10
我不确定这是否按预期工作。我有1个父对象和3个相关对象。我使用的查询返回三行。第一列描述父级,每行重复一遍;id上的拆分将标识每个唯一的孩子。我的结果是有3个孩子的3个重复父母。...应该是有3个孩子的1个父母。
topwik

2
@topwik是正确的。它也不符合我的预期。
Maciej Pszczolinski

3
实际上,我最终得到了3个父母,每个父母都有1个孩子,并带有此代码。不知道为什么我的结果与@topwik不同,但是仍然不起作用。
th3morg19年

29

我知道我真的来晚了,但是还有另一种选择。您可以在此处使用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();
}

3
需要注意的一件事。如果有很多位置/课程,则应循环浏览这些位置并将其放入字典查找中,这样就可以得到N log N而不是N ^ 2的速度。在较大的数据集中有很大的不同。
丹尼尔·洛伦兹

6

很抱歉迟到聚会(像往常一样)。对我来说,它更容易使用Dictionary就像吉荣ķ那样,在性能和可读性方面。另外,为了避免跨位置的标头乘法,我使用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;
}

4

缺了点什么。如果未Locations在SQL查询中指定每个字段,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.*在查询中使用的是位置列表,但没有数据。


0

不知道是否有人需要它,但是我有没有Model的动态版本,可以快速灵活地进行编码。

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;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.