ネストされたオブジェクトのリストを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;}
   ...
}

したがって、1つのコースを複数の場所で教えることができます。Entity Frameworkがマッピングを実行するので、コースオブジェクトには場所のリストが入力されます。Dapperでこれをどのように行うのですか?それは可能ですか、それともいくつかのクエリステップで行う必要がありますか?



これが私の解決策です: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()}
);

それをすべてマッピングする

これを読者に任せて、いくつかのマップを作成し、場所を入力するコースを繰り返し処理します。

2100in未満のルックアップ(SQL Server)がある場合、トリックは機能することに注意してください。それ以上の場合は、クエリを修正したい場合があります。その場合、すべての結果を1度にヤンクすることもできます。select * from CourseLocations where CourseId in (select Id from Courses ... )QueryMultiple


説明してくれたサムさん、ありがとうございます。上記のように、ロケーションをフェッチして手動でコースに割り当てる2番目のクエリを実行しています。私は、1つのクエリでそれを実行できるような何かを見逃さないようにしたかっただけです。
b3n

2
サム、例のようにコレクションがドメインオブジェクトに定期的に公開される大規模なアプリケーションでは、このコードを物理的にどこに配置することをお勧めしますか?(コード内の多数の異なる場所から、同様に完全に構​​築された[コース]エンティティを使用する場合を想定)コンストラクターで?クラス工場では?何処か別の場所?
tbone 2015

177

または、ルックアップで1つのクエリを使用できます。

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
これにより、時間を大幅に節約できました。デフォルトの「Id」を使用していなかったため、他の人が必要とする可能性のある1つの変更は、splitOn:引数を含めることです。
Bill Sambrone、2015年

1
LEFT JOINの場合、ロケーションリストにnullアイテムが表示されます。var items = lookup.Values;で削除します。items.ForEach(x => x.Locations.RemoveAll(y => y == null));
Choco Smith

1行目の最後にセミコロンがあり、 'AsQueryable()'の前のコンマを削除しない限り、これをコンパイルすることはできません。私は回答を編集しますが、私より前の62人の賛成者は大丈夫だと思っていたようです。おそらく何か不足しています...
ビットコーダー

1
LEFT JOINの場合:別のForeachを実行する必要はありません。追加する前に確認してください:if(l!= null)course.Locations.Add(l)。
jpgrassi 2017

1
辞書を使っているので。QueryMultipleを使用し、コースと場所を個別に照会した後、同じ辞書を使用して場所をコースに割り当てた場合、これはより速くなりますか?それは本質的に同じことから内部結合を除いたものであり、SQLは多くのバイトを転送しないことを意味しますか?
MIKE、

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 2017

2
これに関する唯一の問題は、すべてのLocationレコードのヘッダーを複製することです。コースごとに多くの場所がある場合、帯域幅を増やし、解析/マッピングに時間がかかり、すべてを読み取るためにより多くのメモリを使用することになる、大量のデータの重複がネットワークを通過する可能性があります。
Daniel Lorenz、

10
これが期待どおりに機能しているかどうかはわかりません。1つの親オブジェクトと3つの関連オブジェクトがあります。私が使用するクエリは3行を返します。各行で複製される親を説明する最初の列。idの分割は、それぞれの一意の子を識別します。私の結果は、3つの子を持つ3つの重複した親です。...3つの子を持つ1つの親である必要があります。
topwik 2018

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
注意すべきことが1つあります。ロケーション/コースがたくさんある場合は、ロケーションを1回ループして辞書ルックアップに入れ、N ^ 2の速度ではなくN log Nにする必要があります。より大きなデータセットで大きな違いを生みます。
Daniel Lorenz

6

(いつものように)パーティーに遅れてすみません。私にとっては、それを使用する方が簡単ですDictionaryイェルーン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;
}

4

何かが欠けています。LocationsSQLクエリで各フィールドを指定しないと、オブジェクトに入力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

誰かがそれを必要とするかどうかはわかりませんが、迅速かつ柔軟なコーディングのために、モデルなしの動的バージョンがあります。

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.