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