การใช้ Multimapping ใน Dapper อย่างถูกต้อง


111

ฉันกำลังพยายามใช้คุณลักษณะ Multimapping ของ 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" มันจะส่งกลับค่าว่าง ฉันไม่เข้าใจการทำงานหลักของฟีเจอร์มัลติแมป ฉันไม่ต้องการเพิ่มรายชื่อคอลัมน์ทั้งหมดทุกครั้ง


คุณจะแสดงทั้งสองตารางใน datagridview ได้อย่างไร? ตัวอย่างเล็ก ๆ น้อย ๆ จะได้รับการชื่นชมมาก
Ankur Soni

คำตอบ:


184

ฉันเพิ่งทำการทดสอบที่ใช้งานได้ดี:

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 | ProductName | เปิดบัญชี | CustomerId | ชื่อลูกค้า
--------------------------------------- ----------- --------------

Dapper จำเป็นต้องรู้วิธีแบ่งคอลัมน์ตามลำดับนี้ออกเป็น 2 ออบเจ็กต์ แสดงให้เห็นคร่าวๆดูว่าลูกค้าเริ่มต้นที่คอลัมน์เพราะฉะนั้นCustomerIdsplitOn: CustomerId

มีข้อแม้ใหญ่ที่นี่หากคอลัมน์ที่เรียงลำดับในตารางพื้นฐานถูกพลิกด้วยเหตุผลบางประการ:

ProductID | ProductName | เปิดบัญชี | ชื่อลูกค้า | รหัสลูกค้า  
--------------------------------------- ----------- --------------

splitOn: CustomerId จะส่งผลให้ชื่อลูกค้าว่างเปล่า

หากคุณระบุCustomerId,CustomerNameเป็นจุดแยก dapper จะถือว่าคุณพยายามแยกชุดผลลัพธ์ออกเป็น 3 ออบเจ็กต์ เริ่มต้นครั้งแรกที่จุดเริ่มต้นเริ่มต้นที่สองที่สามCustomerIdCustomerName


2
ขอบคุณแซม ใช่ของคุณมันเป็นลำดับการส่งคืนของคอลัมน์ที่เป็นปัญหากับ CustomerName | CustomerId ถูกส่งคืน CustomerName กำลังกลับมาเป็นโมฆะ
Richard Forrest

18
สิ่งหนึ่งที่ต้องจำไว้คือคุณไม่สามารถเว้นวรรคได้splitonกล่าวคือCustomerId,CustomerNameไม่CustomerId, CustomerNameเนื่องจาก Dapper ไม่ได้Trimผลลัพธ์ของการแยกสตริง มันจะโยนข้อผิดพลาดแยกทั่วไป วันหนึ่งขับรถฉันเป็นบ้า
jes

2
@vaheeds คุณควรใช้ชื่อคอลัมน์เสมอและอย่าใช้ดาวมันทำให้ sql ทำงานน้อยลงและคุณจะไม่ได้รับสถานการณ์ที่ลำดับคอลัมน์ไม่ถูกต้องเช่นในกรณีนี้
Harag

3
@vaheeds - เกี่ยวกับ id, Id, ID ที่ดูรหัส dapper นั้นไม่คำนึงถึงขนาดตัวพิมพ์และยังตัดแต่งข้อความสำหรับ splitOn - นี่คือ v1.50.2.0 ของ dapper
Harag

2
สำหรับใครก็ตามที่สงสัยในกรณีที่คุณต้องแบ่งคำค้นหาออกเป็น 3 ออบเจ็กต์: ในคอลัมน์เดียวชื่อ "Id" และในคอลัมน์หนึ่งชื่อ "somethingId" อย่าลืมใส่ "Id" แรกในประโยคแยก แม้ว่า Dapper จะแยกตามค่าเริ่มต้นใน "Id" แต่ในกรณีนี้จะต้องตั้งค่าอย่างชัดเจน
Sbu

28

ตารางของเรามีชื่อคล้ายกับของคุณโดยที่บางอย่างเช่น "CustomerID" อาจถูกส่งคืนสองครั้งโดยใช้การดำเนินการ "เลือก *" ดังนั้น Dapper กำลังทำงาน แต่เพิ่งแยกออกเร็วเกินไป (อาจเป็นไปได้) เนื่องจากคอลัมน์จะเป็น:

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

สิ่งนี้ทำให้พารามิเตอร์ Spliton: ไม่มีประโยชน์โดยเฉพาะอย่างยิ่งเมื่อคุณไม่แน่ใจว่าจะส่งคืนคอลัมน์ในลำดับใดแน่นอนว่าคุณสามารถระบุคอลัมน์ด้วยตนเองได้ ... แต่มันเป็นปี 2017 และเราแทบจะไม่ทำเช่นนั้นอีกต่อไปสำหรับการรับวัตถุพื้นฐาน

สิ่งที่เราทำและมันใช้งานได้ดีสำหรับการค้นหาหลายพันรายการเป็นเวลาหลายปีเพียงแค่ใช้นามแฝงสำหรับ Id และไม่ต้องระบุแยก (โดยใช้ 'Id' เริ่มต้นของ Dapper)

select 
p.*,

c.CustomerID AS Id,
c.*

... โวลา! Dapper จะแยกเฉพาะ Id ตามค่าเริ่มต้นและ Id นั้นจะเกิดขึ้นก่อนคอลัมน์ลูกค้าทั้งหมด แน่นอนว่ามันจะเพิ่มคอลัมน์พิเศษในชุดผลลัพธ์ของคุณ แต่นั่นเป็นค่าใช้จ่ายที่น้อยมากสำหรับยูทิลิตี้ที่เพิ่มเข้ามาในการรู้ว่าคอลัมน์ใดเป็นของวัตถุอะไร และคุณสามารถขยายสิ่งนี้ได้อย่างง่ายดาย ต้องการข้อมูลที่อยู่และประเทศ?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

เหนือสิ่งอื่นใดคุณแสดงให้เห็นอย่างชัดเจนในจำนวน sql น้อยที่สุดว่าคอลัมน์ใดเชื่อมโยงกับวัตถุใด Dapper ทำส่วนที่เหลือ


นี่เป็นแนวทางที่กระชับตราบเท่าที่ไม่มีตารางใดมีฟิลด์ Id
Bernard Vander Beken

ด้วยวิธีนี้ตารางจะยังคงมีฟิลด์ Id ... แต่ควรเป็น PK คุณไม่จำเป็นต้องสร้างนามแฝงดังนั้นจึงใช้งานได้น้อยลงเล็กน้อย (ฉันคิดว่ามันผิดปกติมาก (รูปแบบที่ไม่ดี?) ที่มีคอลัมน์ชื่อ 'Id' ที่ไม่ใช่ PK)
BlackjacketMack

6

สมมติว่าโครงสร้างต่อไปนี้โดยที่ '|' คือจุดของการแยกและ Ts คือเอนทิตีที่ควรใช้การแมป

       TFirst         TSecond         TThird           TFourth
------------------+-------------+-------------------+------------
col_1 col_2 col_3 | col_n col_m | col_A col_B col_C | col_9 col_8
------------------+-------------+-------------------+------------

ต่อไปนี้เป็นแบบสอบถาม dapper ที่คุณจะต้องเขียน

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

ดังนั้นเราต้องการให้ TF First แมป col_1 col_2 col_3 สำหรับ TS วินาทีที่ col_n col_m ...

expresion splitOn แปลเป็น:

เริ่มการแมปคอลัมน์ทั้งหมดลงใน TFrist จนกว่าคุณจะพบคอลัมน์ที่ชื่อหรือนามแฝงว่า 'col_3' และรวม 'col_3' ลงในผลลัพธ์การทำแผนที่

จากนั้นเริ่มการแมปเป็น TS วินาทีทุกคอลัมน์โดยเริ่มจาก 'col_n' และทำการแมปต่อไปจนกว่าจะพบตัวคั่นใหม่ซึ่งในกรณีนี้คือ 'col_A' และทำเครื่องหมายจุดเริ่มต้นของ TT การแมปครั้งที่สามและหนึ่ง

คอลัมน์ของคิวรี sql และอุปกรณ์ประกอบฉากของอ็อบเจ็กต์การแม็ปอยู่ในความสัมพันธ์ 1: 1 (หมายความว่าควรตั้งชื่อเหมือนกัน) หากชื่อคอลัมน์ที่เกิดจากคิวรี sql แตกต่างกันคุณสามารถตั้งชื่อแทนได้โดยใช้ "AS [ Some_Alias_Name] 'นิพจน์


2

มีอีกหนึ่งข้อแม้ ถ้าฟิลด์ CustomerId เป็นโมฆะ (โดยทั่วไปจะอยู่ในแบบสอบถามที่มีการรวมด้านซ้าย) Dapper จะสร้าง ProductItem ด้วย Customer = null ในตัวอย่างด้านบน:

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

และอีกหนึ่งข้อแม้ / กับดัก หากคุณไม่ได้แมปฟิลด์ที่ระบุใน 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");  

มีวิธีแก้ไขตัวอย่างที่สองนอกเหนือจากการเพิ่ม Customerid ลงในคลาสหรือไม่ ฉันมีปัญหาที่ฉันต้องการวัตถุว่าง แต่มันทำให้ฉันเป็นวัตถุว่างเปล่า ( stackoverflow.com/questions/27231637/… )
jmzagorski

1

ฉันทำสิ่งนี้โดยทั่วไปใน repo ของฉันใช้ได้ดีกับกรณีการใช้งานของฉัน ฉันคิดว่าฉันจะแบ่งปัน อาจจะมีคนขยายเรื่องนี้ออกไป

ข้อเสียบางประการคือ:

  • สิ่งนี้ถือว่าคุณสมบัติคีย์นอกของคุณคือชื่อของวัตถุลูก + "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;
    }

0

หากคุณต้องการแมปเอนทิตีขนาดใหญ่ให้เขียนแต่ละฟิลด์ต้องเป็นงานหนัก

ฉันลองใช้คำตอบ @BlackjacketMack แต่หนึ่งในตารางของฉันมีคอลัมน์ Id อันอื่นไม่ใช่ (ฉันรู้ว่ามันเป็นปัญหาการออกแบบ DB แต่ ...

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

ไม่ได้ผลสำหรับฉัน จากนั้นฉันก็จบลงด้วยการเปลี่ยนแปลงเล็กน้อยเพียงแค่ใส่จุดแยกที่มีชื่อที่ไม่ตรงกับฟิลด์ใด ๆ บนตารางในกรณีอาจมีการเปลี่ยนแปลงas Idโดยas _SplitPoint_สคริปต์ sql สุดท้ายจะมีลักษณะดังนี้:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

จากนั้นใน dapper ให้เพิ่มเพียงหนึ่ง SplitOn ตามนี้

cmd =
    "SELECT Materials.*, " +
    "   Product.ItemtId as _SplitPoint_," +
    "   Product.*, " +
    "   MeasureUnit.IntIdUM as _SplitPoint_, " +
    "   MeasureUnit.* " +
    "FROM   Materials INNER JOIN " +
    "   Product ON Materials.ItemtId = Product.ItemtId INNER JOIN " +
    "   MeasureUnit ON Materials.IntIdUM = MeasureUnit.IntIdUM " +
List < Materials> fTecnica3 = (await dpCx.QueryAsync<Materials>(
        cmd,
        new[] { typeof(Materials), typeof(Product), typeof(MeasureUnit) },
        (objects) =>
        {
            Materials mat = (Materials)objects[0];
            mat.Product = (Product)objects[1];
            mat.MeasureUnit = (MeasureUnit)objects[2];
            return mat;
        },
        splitOn: "_SplitPoint_"
    )).ToList();
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.