Пользовательское отображение в Dapper
Я пытаюсь использовать CTE с Dapper и multi-mapping, чтобы получать постраничные результаты. Я столкнулся с неудобством с дублирующимися столбцами; CTE препятствует мне иметь имя столбцов, например.
Я хотел бы отобразить следующий запрос на следующие объекты, а не на несоответствие между именами столбцов и свойствами.
Query:
WITH TempSites AS(
SELECT
[S].[SiteID],
[S].[Name] AS [SiteName],
[S].[Description],
[L].[LocationID],
[L].[Name] AS [LocationName],
[L].[Description] AS [LocationDescription],
[L].[SiteID] AS [LocationSiteID],
[L].[ReportingID]
FROM (
SELECT * FROM [dbo].[Sites] [1_S]
WHERE [1_S].[StatusID] = 0
ORDER BY [1_S].[Name]
OFFSET 10 * (1 - 1) ROWS
FETCH NEXT 10 ROWS ONLY
) S
LEFT JOIN [dbo].[Locations] [L] ON [S].[SiteID] = [L].[SiteID]
),
MaxItems AS (SELECT COUNT(SiteID) AS MaxItems FROM Sites)
SELECT *
FROM TempSites, MaxItems
Объекты:
public class Site
{
public int SiteID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Location> Locations { get; internal set; }
}
public class Location
{
public int LocationID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public Guid ReportingID { get; set; }
public int SiteID { get; set; }
}
По какой-то причине у меня есть в голове, что существует соглашение об именах, которое будет обрабатывать этот сценарий для меня, но я не могу найти упоминание об этом в документах.
Ответы
Ответ 1
Есть несколько вопросов, пусть они охватывают их один за другим.
Названия дубликатов CTE:
CTE не позволяет дублировать имена столбцов, поэтому вы должны разрешать их с помощью псевдонимов, предпочтительно используя какое-либо соглашение об именах, например, в вашей попытке запроса.
По какой-то причине у меня есть в голове, что существует соглашение об именах, которое будет обрабатывать этот сценарий для меня, но я не могу найти упоминание об этом в документах.
Вероятно, вы имели в виду настройку свойства DefaultTypeMap.MatchNamesWithUnderscores
на true
, но в качестве документации кода состояния состояния:
Должны ли имена столбцов, такие как User_Id, соответствовать свойствам/полям, например UserId?
очевидно, это не решение. Но проблему можно легко решить, введя условное соглашение об именах, например "{prefix}{propertyName}"
(где по умолчанию используется префикс "{className}_"
) и реализуется через Dapper CustomPropertyTypeMap
. Вот вспомогательный метод, который делает это:
public static class CustomNameMap
{
public static void SetFor<T>(string prefix = null)
{
if (prefix == null) prefix = typeof(T).Name + "_";
var typeMap = new CustomPropertyTypeMap(typeof(T), (type, name) =>
{
if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
name = name.Substring(prefix.Length);
return type.GetProperty(name);
});
SqlMapper.SetTypeMap(typeof(T), typeMap);
}
}
Теперь вам нужно только позвонить (один раз):
CustomNameMap.SetFor<Location>();
примените соглашение об именовании к вашему запросу:
WITH TempSites AS(
SELECT
[S].[SiteID],
[S].[Name],
[S].[Description],
[L].[LocationID],
[L].[Name] AS [Location_Name],
[L].[Description] AS [Location_Description],
[L].[SiteID] AS [Location_SiteID],
[L].[ReportingID]
FROM (
SELECT * FROM [dbo].[Sites] [1_S]
WHERE [1_S].[StatusID] = 0
ORDER BY [1_S].[Name]
OFFSET 10 * (1 - 1) ROWS
FETCH NEXT 10 ROWS ONLY
) S
LEFT JOIN [dbo].[Locations] [L] ON [S].[SiteID] = [L].[SiteID]
),
MaxItems AS (SELECT COUNT(SiteID) AS MaxItems FROM Sites)
SELECT *
FROM TempSites, MaxItems
и вы закончите с этой частью. Конечно, вы можете использовать более короткий префикс типа "Loc_", если хотите.
Сопоставление результата запроса с предоставленными классами:
В этом конкретном случае вам нужно использовать перегрузку метода Query
, которая позволяет передать делегат Func<TFirst, TSecond, TReturn> map
и унифицировать параметр splitOn
, чтобы указать LocationID
как разделенный столбец. Однако этого недостаточно. Dapper Функция Multi Mapping позволяет разделить одну строку на несколько одиночных объектов (например, LINQ Join
), в то время как вам понадобится Site
с Location
списком (например, LINQ GroupJoin
).
Это может быть достигнуто с помощью метода Query
для проецирования во временный анонимный тип, а затем использовать обычный LINQ для создания нужного вывода следующим образом:
var sites = cn.Query(sql, (Site site, Location loc) => new { site, loc }, splitOn: "LocationID")
.GroupBy(e => e.site.SiteID)
.Select(g =>
{
var site = g.First().site;
site.Locations = g.Select(e => e.loc).Where(loc => loc != null).ToList();
return site;
})
.ToList();
где cn
открыт SqlConnection
, а sql
- это string
, содержащий вышеуказанный запрос.
Ответ 2
Вы можете сопоставить имя столбца с другим атрибутом, используя ColumnAttributeTypeMapper.
См. мой первый комментарий к Gist для более подробной информации.
Вы можете сделать сопоставление, например
public class Site
{
public int SiteID { get; set; }
[Column("SiteName")]
public string Name { get; set; }
public string Description { get; set; }
public List<Location> Locations { get; internal set; }
}
public class Location
{
public int LocationID { get; set; }
[Column("LocationName")]
public string Name { get; set; }
[Column("LocationDescription")]
public string Description { get; set; }
public Guid ReportingID { get; set; }
[Column("LocationSiteID")]
public int SiteID { get; set; }
}
Отображение может быть выполнено с использованием одного из следующих 3 методов
Метод 1
Вручную установите собственный TypeMapper для вашей модели один раз:
Dapper.SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
Dapper.SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
Метод 2
Для библиотек классов .NET Framework >= v4.0 вы можете использовать PreApplicationStartMethod для регистрации ваших классов для пользовательского сопоставления типов.
using System.Web;
using Dapper;
[assembly: PreApplicationStartMethod(typeof(YourNamespace.Initiator), "RegisterModels")]
namespace YourNamespace
{
public class Initiator
{
private static void RegisterModels()
{
SqlMapper.SetTypeMap(typeof(Site), new ColumnAttributeTypeMapper<Site>());
SqlMapper.SetTypeMap(typeof(Location), new ColumnAttributeTypeMapper<Location>());
// ...
}
}
}
Метод 3
Или вы можете найти классы, к которым применяется ColumnAttribute, путем отражения и сопоставления типов. Это может быть немного медленнее, но он автоматически отображает все ваши сборки в вашей сборке. Просто вызовите RegisterTypeMaps()
после загрузки сборки.
public static void RegisterTypeMaps()
{
var mappedTypes = Assembly.GetAssembly(typeof (Initiator)).GetTypes().Where(
f =>
f.GetProperties().Any(
p =>
p.GetCustomAttributes(false).Any(
a => a.GetType().Name == ColumnAttributeTypeMapper<dynamic>.ColumnAttributeName)));
var mapper = typeof(ColumnAttributeTypeMapper<>);
foreach (var mappedType in mappedTypes)
{
var genericType = mapper.MakeGenericType(new[] { mappedType });
SqlMapper.SetTypeMap(mappedType, Activator.CreateInstance(genericType) as SqlMapper.ITypeMap);
}
}
Ответ 3
Нижеприведенный код должен отлично работать для загрузки списка сайтов со связанными местоположениями
var conString="your database connection string here";
using (var conn = new SqlConnection(conString))
{
conn.Open();
string qry = "SELECT S.SiteId, S.Name, S.Description, L.LocationId, L.Name,L.Description,
L.ReportingId
from Site S INNER JOIN
Location L ON S.SiteId=L.SiteId";
var sites = conn.Query<Site, Location, Site>
(qry, (site, loc) => { site.Locations = loc; return site; });
var siteCount = sites.Count();
foreach (Site site in sites)
{
//do something
}
conn.Close();
}