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