Правильное использование Multimapping в 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; }
}

Мой код более правильный

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

7 ответов

Решение

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

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 | Имя покупателя 
--------------------------------------- ----------- --------------

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

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

ProductID | ProductName | AccountOpened | CustomerName | Пользовательский ИД  
--------------------------------------- ----------- --------------

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

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

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

Предполагая следующую структуру запроса 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]

Если вам нужно отобразить большой объект, напишите, что каждое поле должно быть сложной задачей.

Я попробовал ответить @BlackjacketMack, но одна из моих таблиц имеет столбец идентификатора, другие нет (я знаю, что это проблема с дизайном БД, но...), тогда эта вставка дополнительного разделения на dapper, поэтому

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

Есть еще одна оговорка. Если поле 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, и это поле содержит нулевое значение, Dapper создает и заполняет связанный объект (в данном случае Customer). Чтобы продемонстрировать использование этого класса с предыдущим 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");  

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

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

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

Хочу отметить очень важный момент. Именование свойства в Entity должно соответствовать выражению select. Другой аспект spliton - это то, как он ищет Id по умолчанию, поэтому вам не нужно указывать его, если ваше имя не похоже на CustomerId, а не на Id. Итак, давайте посмотрим на эти 2 подхода:

Подход 1: Юридический заказчик: Id Name

ваш выбор должен быть примерно таким: Выберите c.Id как имя {Customer.Id}, c.WeirdAssName как имя {Customer.Name}. Тогда ваше отображение понимает связь между сущностью и таблицей.

Подход 2:

Сущность Customer: CustomerId, FancyName Выберите c.Id как имя {Customer.CustomerId}, c.WeirdAssName как имя {Customer.FancyName}, и в конце сопоставления вы должны указать, что Id - это CustomerId, используя SplitOn. У меня возникла проблема с невозможностью получить свои значения, хотя сопоставление было правильным технически, но это было несоответствие с оператором sql.

Другие вопросы по тегам