Sử dụng đúng Đa ánh xạ trong Dapper


111

Tôi đang cố gắng sử dụng tính năng Đa ánh xạ của dapper để trả về danh sách các Hạng mục sản phẩm và Khách hàng được liên kết.

[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; }
}

Mã dapper của tôi như sau

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

Điều này hoạt động tốt nhưng tôi dường như phải thêm danh sách cột hoàn chỉnh vào tham số splitOn để trả về tất cả các thuộc tính khách hàng. Nếu tôi không thêm "CustomerName", nó sẽ trả về null. Có phải tôi không hiểu chức năng cốt lõi của tính năng đa ánh xạ. Tôi không muốn phải thêm danh sách tên cột đầy đủ mỗi lần.


làm thế nào để bạn thực sự hiển thị cả hai bảng trong datagridview sau đó? một ví dụ nhỏ sẽ được nhiều đánh giá cao.
Ankur Soni

Câu trả lời:


184

Tôi vừa chạy một bài kiểm tra hoạt động tốt:

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

Tham số splitOn cần được chỉ định làm điểm phân tách, nó mặc định là Id. Nếu có nhiều điểm phân tách, bạn sẽ cần thêm chúng vào danh sách được phân tách bằng dấu phẩy.

Giả sử bộ ghi của bạn trông giống như sau:

ProductID | ProductName | Đã mở tài khoản | CustomerId | Tên khách hàng
--------------------------------------------- --------------

Dapper cần biết cách tách các cột theo thứ tự này thành 2 đối tượng. Nhìn lướt qua cho thấy rằng Khách hàng bắt đầu ở cột CustomerId, do đó splitOn: CustomerId.

Có một cảnh báo lớn ở đây, nếu thứ tự cột trong bảng bên dưới bị lật vì một số lý do:

ProductID | ProductName | Đã mở tài khoản | Tên khách hàng | ID khách hàng  
--------------------------------------------- --------------

splitOn: CustomerId sẽ dẫn đến tên khách hàng rỗng.

Nếu bạn chỉ định CustomerId,CustomerNamelà các điểm phân tách, dapper giả sử bạn đang cố gắng chia tập hợp kết quả thành 3 đối tượng. Đầu tiên bắt đầu lúc bắt đầu, thứ hai bắt đầu lúc CustomerId, thứ ba lúc CustomerName.


2
Cảm ơn Sam. Vâng, quyền của bạn, đó là thứ tự trả lại của các cột là vấn đề với CustomerName | CustomerId được trả lại CustomerName đã trở lại null.
Richard Forrest,

18
Một điều cần nhớ là bạn không thể có khoảng trắng trong spliton, tức là CustomerId,CustomerNamekhông CustomerId, CustomerName, vì Dapper không phải Trimlà kết quả của việc phân chia chuỗi. Nó sẽ chỉ tạo ra lỗi spliton chung. Một ngày kia khiến tôi phát điên.
jes

2
@vaheeds, bạn LUÔN LUÔN nên sử dụng tên cột và không bao giờ sử dụng dấu sao, nó giúp cho sql ít việc phải làm hơn và bạn không gặp phải tình huống thứ tự cột bị sai, như trong trường hợp này.
Harag

3
@vaheeds - liên quan đến id, Id, ID nhìn vào mã dapper nó không phân biệt chữ hoa chữ thường và nó cũng cắt bớt văn bản cho splitOn - đây là v1.50.2.0 của dapper.
Harag

2
Đối với bất kỳ ai thắc mắc, trong trường hợp bạn phải chia truy vấn thành 3 đối tượng: trên một cột có tên "Id" và trên một cột có tên "somethingId", hãy đảm bảo bao gồm "Id" đầu tiên trong mệnh đề tách. Mặc dù Dapper tách theo mặc định trên "Id", trong trường hợp này, nó phải được đặt rõ ràng.
Sbu

27

Bảng của chúng tôi được đặt tên tương tự như bảng của bạn, trong đó một cái gì đó như "CustomerID" có thể được trả về hai lần bằng thao tác 'select *'. Do đó, Dapper đang thực hiện công việc của mình nhưng chỉ tách ra quá sớm (có thể), bởi vì các cột sẽ là:

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

Điều này làm cho tham số spliton: không quá hữu ích, đặc biệt là khi bạn không chắc chắn về thứ tự các cột được trả về. Tất nhiên bạn có thể chỉ định các cột theo cách thủ công ... nhưng đó là năm 2017 và chúng tôi hiếm khi làm điều đó nữa đối với đối tượng cơ bản.

Những gì chúng tôi làm và nó hoạt động hiệu quả với hàng nghìn truy vấn trong nhiều năm, chỉ đơn giản là sử dụng bí danh cho Id và không bao giờ chỉ định spliton (sử dụng 'Id' mặc định của Dapper).

select 
p.*,

c.CustomerID AS Id,
c.*

... thì đấy! Dapper sẽ chỉ tách trên Id theo mặc định và Id đó xuất hiện trước tất cả các cột Khách hàng. Tất nhiên nó sẽ thêm một cột bổ sung vào tập kết quả trả về của bạn, nhưng đó là chi phí cực kỳ tối thiểu để tiện ích bổ sung biết chính xác cột nào thuộc đối tượng nào. Và bạn có thể dễ dàng mở rộng điều này. Cần thông tin địa chỉ và quốc gia?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Hơn hết, bạn đang hiển thị rõ ràng trong một lượng sql tối thiểu cột nào được liên kết với đối tượng nào. Dapper làm phần còn lại.


Đây là một cách tiếp cận ngắn gọn miễn là không có bảng nào có trường Id.
Bernard Vander Beken

Với cách tiếp cận này, một bảng vẫn có thể có trường Id ... nhưng nó phải là PK. Bạn sẽ không phải tạo bí danh vì vậy nó thực sự ít công việc hơn một chút. (Tôi sẽ nghĩ rằng rất bất thường (hình thức xấu?) Nếu có một cột có tên 'Id' không phải là PK.)
BlackjacketMack

5

Giả sử cấu trúc sau đây, trong đó '|' là điểm phân tách và Ts là các thực thể mà ánh xạ nên được áp dụng.

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

Sau đây là truy vấn dapper mà bạn sẽ phải viết.

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

Vì vậy, chúng tôi muốn TFirst ánh xạ col_1 col_2 col_3, đối với TSecond là col_n col_m ...

Cuộc thám hiểm splitOn dịch thành:

Bắt đầu ánh xạ tất cả các cột vào TFrist cho đến khi bạn tìm thấy một cột có tên hoặc bí danh là 'col_3', đồng thời bao gồm 'col_3' vào kết quả ánh xạ.

Sau đó, bắt đầu ánh xạ vào TSecond tất cả các cột bắt đầu từ 'col_n' và tiếp tục ánh xạ cho đến khi tìm thấy dấu phân tách mới, trong trường hợp này là 'col_A' và đánh dấu thời điểm bắt đầu ánh xạ TThird và như vậy.

Các cột của truy vấn sql và đạo cụ của đối tượng ánh xạ nằm trong quan hệ 1: 1 (có nghĩa là chúng phải được đặt tên giống nhau), nếu tên các cột do truy vấn sql khác nhau, bạn có thể đặt bí danh cho chúng bằng cách sử dụng 'AS [ Biểu thức Some_Alias_Name] '.


2

Có một cảnh báo nữa. Nếu trường CustomerId là rỗng (thường trong các truy vấn có phép nối bên trái) Dapper tạo ProductItem với Customer = null. Trong ví dụ trên:

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

Và thậm chí một cảnh báo / bẫy nữa. Nếu bạn không ánh xạ trường được chỉ định trong splitOn và trường đó chứa null Dapper sẽ tạo và điền vào đối tượng liên quan (Trong trường hợp này là Khách hàng). Để chứng minh, hãy sử dụng lớp này với sql trước đó:

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

có giải pháp nào cho ví dụ thứ hai ngoài việc thêm Customerid vào lớp không? Tôi đang gặp sự cố trong đó tôi cần một đối tượng null, nhưng nó đang cung cấp cho tôi một đối tượng trống. ( stackoverflow.com/questions/27231637/… )
jmzagorski

1

Tôi làm điều này nói chung trong repo của tôi, hoạt động tốt cho trường hợp sử dụng của tôi. Tôi nghĩ tôi sẽ chia sẻ. Có thể ai đó sẽ mở rộng điều này hơn nữa.

Một số hạn chế là:

  • Điều này giả định các thuộc tính khóa ngoại của bạn là tên của đối tượng con + "Id", ví dụ: UnitId.
  • Tôi có nó chỉ ánh xạ 1 đối tượng con với đối tượng chính.

Mật mã:

    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

Nếu bạn cần lập bản đồ một thực thể lớn, viết mỗi trường phải là một nhiệm vụ khó khăn.

Tôi đã thử câu trả lời @BlackjacketMack, nhưng một trong các bảng của tôi có Cột Id những bảng khác thì không (tôi biết đó là vấn đề thiết kế DB, nhưng ...) sau đó điều này chèn thêm một phần tách trên dapper, đó là lý do

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Không hiệu quả với tôi. Sau đó, tôi đã kết thúc với một chút thay đổi đối với điều này, chỉ cần chèn một điểm phân tách có tên không khớp với bất kỳ trường nào trên bảng, Trong trường hợp có thể được thay đổi as Idbởi as _SplitPoint_, tập lệnh sql cuối cùng trông như thế này:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

Sau đó, trong dapper chỉ thêm một splitOn như thế này

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();
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.