Bagaimana cara memetakan daftar objek bersarang dengan Dapper

127

Saat ini saya menggunakan Entity Framework untuk akses db saya tetapi ingin melihat Dapper. Saya memiliki kelas seperti ini:

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

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

Jadi satu mata kuliah dapat diajarkan di beberapa lokasi. Entity Framework melakukan pemetaan untuk saya sehingga objek Kursus saya diisi dengan daftar lokasi. Bagaimana saya akan melakukan hal ini dengan Dapper, apakah mungkin atau saya harus melakukannya dalam beberapa langkah kueri?

b3n
sumber
Pertanyaan terkait: stackoverflow.com/questions/6379155/…
Jeroen K
inilah solusi saya: stackoverflow.com/a/57395072/8526957
Sam Sch

Jawaban:

57

Dapper bukanlah ORM yang lengkap, Dapper tidak menangani pembuatan kueri ajaib dan semacamnya.

Untuk contoh khusus Anda, berikut ini mungkin akan berhasil:

Ambil kursusnya:

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

Ambil pemetaan yang relevan:

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

Ambil lokasi yang relevan

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

Petakan semuanya

Meninggalkan ini kepada pembaca, Anda membuat beberapa peta dan beralih melalui kursus Anda yang diisi dengan lokasi.

Peringatan bahwa intrik ini akan berfungsi jika Anda memiliki kurang dari 2100 pencarian (Sql Server), jika Anda memiliki lebih banyak, Anda mungkin ingin mengubah kueri menjadi select * from CourseLocations where CourseId in (select Id from Courses ... )jika itu masalahnya, Anda mungkin juga menarik semua hasil sekaligus menggunakanQueryMultiple

Sam Saffron
sumber
Terima kasih atas klarifikasi Sam. Seperti yang Anda jelaskan di atas, saya hanya menjalankan kueri kedua yang mengambil Lokasi dan secara manual menugaskannya ke kursus. Saya hanya ingin memastikan bahwa saya tidak melewatkan sesuatu yang memungkinkan saya melakukannya dengan satu kueri.
b3n
2
Sam, dalam ~ aplikasi besar di mana koleksi secara teratur diekspos pada objek domain seperti pada contoh, di mana Anda akan merekomendasikan kode ini ditempatkan secara fisik ? (Dengan asumsi Anda ingin menggunakan entitas [Kursus] yang dibangun sepenuhnya serupa dari berbagai tempat berbeda dalam kode Anda) Di konstruktor? Di pabrik kelas? Di tempat lain?
Tbone
178

Alternatifnya, Anda bisa menggunakan satu kueri dengan pencarian:

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;

Lihat di sini https://www.tritac.com/blog/dappernet-by-example/

Jeroen K
sumber
9
Ini menghemat banyak waktu. Satu modifikasi yang saya perlukan yang mungkin diperlukan orang lain adalah menyertakan argumen splitOn: karena saya tidak menggunakan "Id" default.
Bill Sambrone
1
Untuk LEFT JOIN Anda akan mendapatkan item null di daftar lokasi. Hapus mereka dengan var items = lookup.Values; items.ForEach (x => x.Locations.RemoveAll (y => y == null));
Choco Smith
Saya tidak dapat mengkompilasi ini kecuali saya memiliki titik koma di akhir baris 1 dan menghapus koma sebelum 'AsQuerable ()'. Saya akan mengedit jawabannya tetapi 62 pemberi suara sebelum saya tampaknya berpikir tidak apa-apa, mungkin saya melewatkan sesuatu ...
bitcoder
1
Untuk LEFT JOIN: Tidak perlu melakukan Foreach lain di atasnya. Cukup periksa sebelum menambahkannya: if (l! = Null) course.Locations.Add (l).
jpgrassi
1
Karena Anda menggunakan kamus. Apakah ini akan lebih cepat jika Anda menggunakan QueryMultiple dan menanyakan kursus dan lokasi secara terpisah lalu menggunakan kamus yang sama untuk menetapkan lokasi ke kursus? Ini pada dasarnya hal yang sama dikurangi inner join yang berarti sql tidak akan mentransfer sebanyak byte?
MIKE
43

Tidak perlu lookupKamus

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();
tchelidze.dll
sumber
3
Ini luar biasa - ini menurut saya harus menjadi jawaban yang dipilih. Namun, orang-orang yang melakukan ini berhati-hati dalam melakukan * karena hal itu dapat memengaruhi kinerja.
cr1pto
2
Satu-satunya masalah dengan ini adalah Anda akan menduplikasi header di setiap rekaman Lokasi. Jika ada banyak lokasi per kursus, mungkin ada sejumlah besar duplikasi data yang melintasi kabel yang akan meningkatkan bandwidth, membutuhkan waktu lebih lama untuk mengurai / memetakan, dan menggunakan lebih banyak memori untuk membaca semua itu.
Daniel Lorenz
10
Saya tidak yakin ini berfungsi seperti yang saya harapkan. Saya memiliki 1 objek induk dengan 3 objek terkait. kueri yang saya gunakan mendapatkan tiga baris kembali. kolom pertama yang menjelaskan induk yang digandakan untuk setiap baris; pemisahan pada id akan mengidentifikasi setiap anak unik. hasil saya adalah 3 orang tua duplikat dengan 3 anak .... harus satu orang tua dengan 3 anak.
topwik
2
@topwik benar. itu juga tidak bekerja seperti yang diharapkan untuk saya.
Maciej Pszczolinski
3
Saya benar-benar berakhir dengan 3 orang tua, masing-masing 1 anak dengan kode ini. Tidak yakin mengapa hasil saya berbeda dari @topwik, tapi tetap saja tidak berhasil.
th3morg
29

Saya tahu saya sangat terlambat untuk ini, tetapi ada pilihan lain. Anda dapat menggunakan QueryMultiple di sini. Sesuatu seperti ini:

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();
}
Daniel Lorenz
sumber
3
Satu hal yang perlu diperhatikan. Jika ada banyak lokasi / kursus, Anda harus mengulang melalui lokasi satu kali dan meletakkannya dalam pencarian kamus sehingga Anda memiliki N log N bukannya N ^ 2 kecepatan. Membuat perbedaan besar dalam kumpulan data yang lebih besar.
Daniel Lorenz
6

Maaf terlambat ke pesta (seperti biasa). Bagi saya, lebih mudah menggunakan a Dictionary, seperti yang dilakukan Jeroen K , dalam hal kinerja dan keterbacaan. Juga, untuk menghindari penggandaan tajuk di seluruh lokasi , saya gunakan Distinct()untuk menghapus potensi dup:

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;
}
Francisco Tena
sumber
4

Sesuatu yang hilang. Jika Anda tidak menentukan setiap bidang dari Locationsdalam kueri SQL, objek Locationtidak dapat diisi. Lihatlah:

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;

Menggunakan l.*dalam kueri, saya memiliki daftar lokasi tetapi tanpa data.

Eduardo Pires
sumber
0

Tidak yakin apakah ada yang membutuhkannya, tetapi saya memiliki versi dinamis tanpa Model untuk pengkodean cepat & fleksibel.

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;
Kiichi
sumber