Пользовательское сопоставление в Dapper
Я пытаюсь использовать CTE с Dapper и multi-mapping для получения результатов выгрузки. Я сталкиваюсь с неудобством с повторяющимися столбцами; CTE мешает мне, например, называть столбцы.
Я хотел бы отобразить следующий запрос на следующие объекты, а не несоответствие между именами столбцов и свойствами.
запрос:
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; }
}
по какой-то причине у меня в голове, что именование существует соглашение, которое будет обрабатывать этот сценарий для меня, но я не могу найти упоминания об этом в документах.
3 ответов
есть несколько вопросов, пусть охватывают их один за другим.
CTE дублировать имена столбцов:
CTE не позволяет дублировать имена столбцов, поэтому вы должны разрешить их с помощью псевдонимов, предпочтительно используя некоторое соглашение об именах, как в попытке запроса.
по какой-то причине у меня в голове есть соглашение об именах, которое будет обрабатывать этот сценарий для меня, но я не могу найти упоминания об этом в доктора.
вы, вероятно, имели в виду установку DefaultTypeMap.MatchNamesWithUnderscores
свойство true
, но в качестве документации кода свойства говорится:
должны ли имена столбцов, такие как User_Id, соответствовать свойствам/полям, таким как UserId?
по-видимому, это не решение. Но проблема может быть легко решена путем введения пользовательского соглашения об именах, например "{prefix}{propertyName}"
(где по умолчанию префикс "{className}_"
) и реализация его через 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
делегат и unitilize в указать LocationID
как разделенный столбец. Однако этого недостаточно. 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
удержание вышеуказанного запроса.
вы можете сопоставить имя столбца с другим атрибутом, используя ColumnAttributeTypeMapper.
см. Мой первый комментарий по существу, для получения дополнительной информации.
вы можете сделать отображение, как
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);
}
}
приведенный ниже код должен отлично работать для загрузки списка сайтов со связанными местоположениями
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();
}