ฉันจะแมปรายการวัตถุที่ซ้อนกันด้วย Dapper ได้อย่างไร


129

ฉันกำลังใช้ 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 ทำการแมปให้ฉันดังนั้นอ็อบเจ็กต์หลักสูตรของฉันจึงถูกเติมด้วยรายการตำแหน่งที่ตั้ง ฉันจะทำอย่างไรกับ Dapper เป็นไปได้หรือไม่หรือต้องทำในขั้นตอนการสืบค้นหลายขั้นตอน


คำถามที่เกี่ยวข้อง: stackoverflow.com/questions/6379155/…
Jeroen K

นี่คือวิธีแก้ปัญหาของฉัน: stackoverflow.com/a/57395072/8526957
Sam Sch

คำตอบ:


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


ขอบคุณสำหรับคำชี้แจง Sam อย่างที่คุณอธิบายไว้ข้างต้นฉันแค่เรียกใช้การสืบค้นที่สองเพื่อดึงสถานที่และกำหนดให้กับหลักสูตรด้วยตนเอง ฉันแค่อยากให้แน่ใจว่าฉันจะไม่พลาดบางสิ่งที่จะทำให้ฉันสามารถทำได้ด้วยแบบสอบถามเดียว
b3n

2
แซมในแอปพลิเคชันขนาดใหญ่ที่มีการเปิดเผยคอลเลกชันบนวัตถุโดเมนเป็นประจำดังตัวอย่างคุณจะแนะนำให้โค้ดนี้อยู่ที่ใด (สมมติว่าคุณต้องการใช้เอนทิตี [หลักสูตร] ที่สร้างขึ้นในทำนองเดียวกันจากที่ต่างๆมากมายในโค้ดของคุณ) ในตัวสร้าง? ในโรงงานระดับ? ที่อื่น?
tbone

178

หรือคุณสามารถใช้แบบสอบถามเดียวกับการค้นหา:

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

1
สำหรับ LEFT JOIN คุณจะได้รับรายการว่างในรายการตำแหน่ง ลบออกโดย var items = lookup.Values; itemsForEach (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

44

ไม่จำเป็นต้องใช้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
ปัญหาเดียวคือคุณจะทำซ้ำส่วนหัวในทุกระเบียนตำแหน่ง หากมีสถานที่หลายแห่งต่อหลักสูตรอาจเป็นการทำซ้ำข้อมูลจำนวนมากที่เกิดขึ้นระหว่างการเชื่อมต่อซึ่งจะเพิ่มแบนด์วิดท์ใช้เวลาแยกวิเคราะห์ / แมปนานขึ้นและใช้หน่วยความจำมากขึ้นเพื่ออ่านข้อมูลทั้งหมด
Daniel Lorenz

10
ฉันไม่แน่ใจว่ามันทำงานได้ตามที่ฉันคาดไว้ ฉันมี 1 ออบเจ็กต์พาเรนต์ที่มี 3 อ็อบเจกต์ที่เกี่ยวข้อง แบบสอบถามที่ฉันใช้ได้รับกลับสามแถว คอลัมน์แรกที่อธิบายถึงพาเรนต์ที่ซ้ำกันสำหรับแต่ละแถว การแยก ID จะระบุเด็กแต่ละคนที่ไม่ซ้ำกัน ผลลัพธ์ของฉันคือผู้ปกครองที่ซ้ำกัน 3 คนและมีลูก 3 คน .... ควรเป็นผู้ปกครอง 1 คนที่มีลูก 3 คน
topwik

2
@topwik พูดถูก มันก็ไม่เป็นไปตามที่ฉันคาดไว้เช่นกัน
Maciej Pszczolinski

3
ฉันลงเอยด้วยผู้ปกครอง 3 คนเด็ก 1 คนในแต่ละคนมีรหัสนี้ ไม่แน่ใจว่าทำไมผลลัพธ์ของฉันถึงแตกต่างจาก @topwik แต่ก็ยังไม่ได้ผล
th3morg

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 สร้างความแตกต่างอย่างมากในชุดข้อมูลขนาดใหญ่
Daniel Lorenz

6

ขออภัยที่มางานปาร์ตี้ช้า (เช่นเคย) สำหรับผมมันเป็นเรื่องง่ายที่จะใช้Dictionary, เช่น Jeroen K ได้ในแง่ของประสิทธิภาพและความสามารถในการอ่าน นอกจากนี้เพื่อหลีกเลี่ยงการคูณส่วนหัวในสถานที่ต่างๆฉันใช้Distinct()เพื่อลบการ dups ที่อาจเกิดขึ้น:

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;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.