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

Ричард Форрест
источник
как тогда вы действительно показываете обе таблицы в datagridview? будет очень признателен небольшой пример.
Анкур Сони

Ответы:

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

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

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

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

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

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

Сэм Шафран
источник
2
Спасибо, Сэм. Да, вы правы, проблема с CustomerName | заключалась в порядке возврата столбцов | Возвращаемый CustomerId. CustomerName возвращал значение null.
Ричард Форрест,
18
Одна вещь , чтобы помнить , что вы не можете иметь пробелы в spliton, то есть CustomerId,CustomerNameне CustomerId, CustomerName, так как Щеголеватый не Trimрезультаты строки раскола. Он просто выдаст общую ошибку spliton. Однажды свела меня с ума.
jes
2
@vaheeds вы должны ВСЕГДА использовать имена столбцов и никогда не использовать звездочку, это дает sql меньше работы, и вы не получаете ситуаций, когда порядок столбцов неправильный, как в этом случае.
Harag
3
@vaheeds - что касается идентификатора, идентификатора, идентификатора, смотрящего на код dapper, он не чувствителен к регистру, а также обрезает текст для splitOn - это v1.50.2.0 dapper.
Хараг,
2
Для тех, кто задается вопросом, если вам нужно разделить запрос на 3 объекта: в одном столбце с именем «Id» и в одном столбце с именем «somethingId», обязательно включите первый «Id» в предложение разделения. Несмотря на то, что Dapper по умолчанию разбивается по «Id», в этом случае он должен быть установлен явно.
Sbu 09
28

Наши таблицы названы так же, как и ваша, где что-то вроде «CustomerID» может быть возвращено дважды с помощью операции «select *». Следовательно, Dapper выполняет свою работу, но просто разделяет слишком рано (возможно), потому что столбцы будут:

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

Это делает параметр spliton: не таким полезным, особенно когда вы не уверены, в каком порядке возвращаются столбцы. Конечно, вы можете вручную указать столбцы ... но сейчас 2017 год, и мы просто редко делаем это для получения базовых объектов.

Что мы делаем, и он отлично работал для тысяч запросов в течение многих лет, - это просто использовать псевдоним для Id и никогда не указывать spliton (с использованием идентификатора Dapper по умолчанию).

select 
p.*,

c.CustomerID AS Id,
c.*

... вуаля! По умолчанию Dapper будет разделяться только по идентификатору, и этот идентификатор встречается перед всеми столбцами «Клиент». Конечно, он добавит дополнительный столбец к вашему набору результатов возврата, но это чрезвычайно минимальные накладные расходы для добавленной утилиты, которая точно знает, какие столбцы принадлежат какому объекту. И вы можете легко это расширить. Нужна информация об адресе и стране?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Лучше всего то, что вы четко показываете в минимальном количестве sql, какие столбцы связаны с каким объектом. Остальное сделает Dapper.

БлэкджекетМак
источник
Это краткий подход, если в таблице нет полей Id.
Бернард Вандер Бекен,
При таком подходе в таблице все еще может быть поле 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")

Итак, мы хотим, чтобы TFirst отображал col_1 col_2 col_3, для TSecond col_n col_m ...

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

Начните отображение всех столбцов в TFrist, пока не найдете столбец с именем или псевдонимом «col_3», а также включите «col_3» в результат сопоставления.

Затем начните отображение в TSecond всех столбцов, начиная с col_n, и продолжайте отображение до тех пор, пока не будет найден новый разделитель, которым в данном случае является col_A, который отмечает начало отображения TThird и так далее.

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

борис
источник
2

Есть еще один нюанс. Если поле 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 создает и заполняет связанный объект (в данном случае 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");  
Франтишек Бачан
источник
есть ли решение для второго примера, кроме добавления Customerid в класс? У меня проблема, когда мне нужен нулевой объект, но он дает мне пустой объект. ( stackoverflow.com/questions/27231637/… )
jmzagorski 01
1

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

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

  • Это предполагает, что ваши свойства внешнего ключа - это имя вашего дочернего объекта + «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, других нет (я знаю, что это проблема с дизайном БД, но ...), тогда эта вставка дополнительного разделения на 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();
Хуан Пабло Гомес
источник