web-dev-qa-db-ja.com

Dapperでのマルチマッピングの正しい使用

Dapperのマルチマッピング機能を使用して、ProductItemsおよび関連する顧客のリストを返そうとしています。

[Table("Product")]
public class ProductItem
{
    public decimal ProductID { get; set; }        
    public string ProductName { get; set; }
    public string AccountOpened { get; set; }
    public Customer Customer { get; set; }
} 

public class Customer
{
    public decimal CustomerId { get; set; }
    public string CustomerName { get; set; }
}

私のdapperコードは次のとおりです

var sql = @"select * from Product p 
            inner join Customer c on p.CustomerId = c.CustomerId 
            order by p.ProductName";

var data = con.Query<ProductItem, Customer, ProductItem>(
    sql,
    (productItem, customer) => {
        productItem.Customer = customer;
        return productItem;
    },
    splitOn: "CustomerId,CustomerName"
);

これは正常に機能しますが、すべての顧客プロパティを返すには、splitOnパラメーターに完全な列リストを追加する必要があるようです。 「CustomerName」を追加しないと、nullが返されます。マルチマッピング機能の中核機能を理解し損ねていますか。毎回列名の完全なリストを追加する必要はありません。

95
Richard Forrest

私はちょうどうまくいくテストを実行しました:

var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(1 as decimal) CustomerId, 'name' CustomerName";

var item = connection.Query<ProductItem, Customer, ProductItem>(sql,
    (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();

item.Customer.CustomerId.IsEqualTo(1);

SplitOnパラメーターは、スプリットポイントとして指定する必要があります。デフォルトはIdです。複数の分割ポイントがある場合は、それらをコンマ区切りリストに追加する必要があります。

レコードセットが次のようになっているとします。

 ProductID |製品名|アカウント開設済み| CustomerId |顧客名 
 -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - -    -  - - -------------------- 

Dapperは、この順序で列を2つのオブジェクトに分割する方法を知る必要があります。大まかな外観は、顧客が列CustomerIdで始まることを示しているため、splitOn: CustomerIdです。

基礎となるテーブルの列の順序が何らかの理由で反転している場合、ここにbig警告があります。

 ProductID |製品名|アカウント開設済み| CustomerName |顧客ID  
 -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - -    -  - - -------------------- 

splitOn: CustomerIdは、ヌルの顧客名になります。

CustomerId,CustomerNameを分割ポイントとして指定すると、dapperは結果セットを3つのオブジェクトに分割しようとしていると想定します。最初は最初から始まり、2番目はCustomerIdで始まり、3番目はCustomerNameで始まります。

162
Sam Saffron

テーブルには、「select *」操作を使用して「CustomerID」のようなものが2回返される可能性のある、あなたのものと同様の名前が付けられています。したがって、Dapperはジョブを実行していますが、列が次のようになるため、分割が早すぎます(おそらく)。

(select * might return):
ProductID,
ProductName,
CustomerID, --first CustomerID
AccountOpened,
CustomerID, --second CustomerID,
CustomerName.

これにより、特に列が返される順序がわからない場合、spliton:パラメーターはあまり役に立ちません。もちろん、列を手動で指定することもできます...しかし、2017年であり、基本的なオブジェクトの取得のためにそれを行うことはほとんどありません。

私たちがしていることは、何千年にもわたって何千ものクエリで長年機能し、IDのエイリアスを使用するだけで、splitonを指定しないことです(Dapperのデフォルトの 'Id'を使用)。

select 
p.*,

c.CustomerID AS Id,
c.*

...出来上がり! DapperはデフォルトでIdでのみ分割され、そのIDはすべてのCustomer列の前に発生します。もちろん、返される結果セットに追加の列を追加しますが、どの列がどのオブジェクトに属しているかを正確に知るという追加のユーティリティにとっては、オーバーヘッドはごくわずかです。これは簡単に拡張できます。住所と国の情報が必要ですか?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

何よりも、どの列がどのオブジェクトに関連付けられているかを最小限のSQLで明確に示しています。 Dapperが残りを行います。

22
BlackjacketMack

もう1つ注意点があります。 CustomerIdフィールドがnullの場合(通常、左結合のクエリで)、DapperはCustomer = nullでProductItemを作成します。上記の例では:

var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(null as decimal) CustomerId, 'n' CustomerName";
var item = connection.Query<ProductItem, Customer, ProductItem>(sql, (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();
Debug.Assert(item.Customer == null); 

さらにもう1つ注意点/トラップがあります。 splitOnで指定されたフィールドをマッピングせず、そのフィールドにnullが含まれている場合、Dapperは関連オブジェクト(この場合は顧客)を作成して入力します。前のSQLでこのクラスを使用する方法を示すには:

public class Customer
{
    //public decimal CustomerId { get; set; }
    public string CustomerName { get; set; }
}
...
Debug.Assert(item.Customer != null);
Debug.Assert(item.Customer.CustomerName == "n");  
2

私はこれをレポジトリで一般的に行い、ユースケースに適しています。私は共有したいと思いました。おそらく誰かがこれをさらに拡張するでしょう。

欠点は次のとおりです。

  • これは、外部キーのプロパティが子オブジェクトの名前+「Id」であると仮定しています。 UnitId。
  • 1つの子オブジェクトを親にマッピングするだけです。

コード:

    public IEnumerable<TParent> GetParentChild<TParent, TChild>()
    {
        var sql = string.Format(@"select * from {0} p 
        inner join {1} c on p.{1}Id = c.Id", 
        typeof(TParent).Name, typeof(TChild).Name);

        Debug.WriteLine(sql);

        var data = _con.Query<TParent, TChild, TParent>(
            sql,
            (p, c) =>
            {
                p.GetType().GetProperty(typeof (TChild).Name).SetValue(p, c);
                return p;
            },
            splitOn: typeof(TChild).Name + "Id");

        return data;
    }
1
Dylan Hayes

次のSQLクエリ構造を想定(列名の表現、値は無関係です)

col_1 col_2 col_3 | col_n col_m | col_A col_B col_C | col_9 col_8

したがって、dapperでは、次のクエリ(QueryAsync)定義を使用します

Query<TFirst, TSecond, TThird, TFourth, TResut> (
    sql : query,
    map: Func<TFirst, TSecond, TThird, TFourth, TResut> myFunc,
    parma: optional,
    splitOn: "col_3, col_n, col_A, col_9")

ここで、TFirstが最初の部分TSecondを2番目にマッピングします。

SplitOn表現は次のように変換されます。

「col_3」という名前または別名の列が見つかるまで、すべての列をTFristにマップし、その列もマッピングに含めます。

次に、col_nから終わりまたは新しいセパレータが見つかるまでTSecondにマッピングします(マッピングcol_nにも含めます)。

次に、最後までcol_Aから始まるか、新しいセパレーターが見つかるまでTThirdにマッピングします(マッピングcol_Aにも含めます)。

次に、col_9で始まるTFourthにマップし、終了するか新しいセパレーターを見つけます(マッピングcol_9にも含めます)。

SQLクエリの列とマッピングオブジェクトの小道具は1:1の関係にあります(つまり、同じ名前にする必要があります)。SQLクエリの結果の列名が異なる場合は、エイリアスAS [Some_Alias_Name]を使用します

1
MCR