web-dev-qa-db-ja.com

Dapperのカスタムマッピング

DapperとマルチマッピングでCTEを使用して、ページングされた結果を取得しようとしています。列が重複しているため、不便です。 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; }
}

何らかの理由で、このシナリオを処理する名前付け規則が存在することを頭の中に持っていますが、ドキュメントで言及することはできません。

10
Ant Swift

複数の問題があるので、それらを一つずつ説明しましょう。

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の マルチマッピング 機能を使用すると、単一行を複数のsingleオブジェクト(LINQ Joinなど)に分割できます。 )Sitelist(LINQ Locationと同様)で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();

ここで、cnSqlConnectionが開かれ、sqlは上記のクエリを保持するstringです。

18
Ivan Stoev

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を手動で1回設定します。

_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);
        }
    }
_
5
Sen Jacob

以下のコードは、関連する場所のサイトのリストをロードするためにうまく機能するはずです

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(); 
}
0
ahankendi