Правильное использование Multimapping в Dapper

Я пытаюсь использовать функцию 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", он возвращает null. Я упускаю из виду основную функциональность функции мультиплексирования. Я не хочу каждый раз добавлять полный список имен столбцов.

Ответы

Ответ 1

Я просто проверил тест, который отлично работает:

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 | AccountOpened | CustomerId | CustomerName 
---------------------------------------   -------------------------

Dapper должен знать, как разбить столбцы в этом порядке на 2 объекта. Беглый взгляд показывает, что Клиент начинается с столбца CustomerId, следовательно splitOn: CustomerId.

Здесь существует оговорка большая, если по какой-то причине упорядочен порядок столбцов в базовой таблице:

ProductID | ProductName | AccountOpened | CustomerName | CustomerId  
---------------------------------------   -------------------------

splitOn: CustomerId приведет к нулевому имени клиента.

Если вы указываете CustomerId,CustomerName как точки разделения, dapper предполагает, что вы пытаетесь разбить результирующий набор на 3 объекта. Сначала начинается в начале, второе начинается с CustomerId, третье - при CustomerName.

Ответ 2

Наши таблицы названы так же, как ваши, где что-то вроде "CustomerID" может быть возвращено дважды с помощью операции "select *". Таким образом, 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, какие столбцы связаны с каким объектом. Даппер делает все остальное.

Ответ 3

Есть еще одна оговорка. Если поле CustomerId равно null (обычно в запросах с левым соединением), 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");  

Ответ 4

Я делаю это в целом в своем репо, хорошо работает для моего прецедента. Я думал, что поделюсь. Может быть, кто-то продолжит это дальше.

Некоторые недостатки:

  • Предполагается, что ваши свойства внешнего ключа - это имя вашего дочернего объекта + "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;
    }

Ответ 5

Предполагая следующую структуру запроса SQL (представление имен столбцов, значения не имеют значения)

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

Таким образом, в dapper вы будете использовать следующее определение Query (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 2nd и так далее.

Выражение splitOn переводится в:

Сопоставьте все столбцы с TFrist, пока не найдете столбец с именем или псевдонимом 'col_3', включите этот столбец также в отображение.

Затем сопоставьте TSecond, начиная с col_n до конца или найдя новый разделитель (также включите его в отображение col_n)

Затем сопоставьте с TThird, начиная с col_A до конца или обнаружив новый разделитель (также включите его в отображение col_A)

Затем сопоставьте TFourth, начиная с col_9 до конца или до обнаружения нового разделителя (также включите его в отображение col_9)

Столбцы запроса SQL и реквизиты объекта сопоставления находятся в отношении 1:1 (это означает, что они должны называться одинаково), если имена столбцов, полученные в результате запроса SQL, отличаются, вы будете использовать псевдонимы AS [Some_Alias_Name]