手动将列名与类属性进行映射

227

我是Dapper微ORM的新手。到目前为止,我能够将其用于简单的ORM相关内容,但我无法将数据库列名与类属性相对应。

例如,我有以下数据库表:

Table Name: Person
person_id  int
first_name varchar(50)
last_name  varchar(50)

我有一个名为Person的类:

public class Person 
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
请注意,我的表中的列名与我尝试映射数据的类的属性名称不同,这些数据来自查询结果。
var sql = @"select top 1 PersonId,FirstName,LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(sql).ToList();
    return person;
}

由于列名与对象 (Person) 的属性不匹配,因此上述代码将无法工作。在这种情况下,我是否可以在 Dapper 中做一些手动映射 (例如 person_id => PersonId) 以将列名与对象属性匹配?


可能是Dapper. Map to SQL Column with spaces in column names的重复问题。 - David McEleney
17个回答

233
Dapper现在支持自定义列到属性映射。它通过ITypeMap接口实现。Dapper提供了一个CustomPropertyTypeMap类,可以完成大部分工作。例如:
Dapper.SqlMapper.SetTypeMap(
    typeof(TModel),
    new CustomPropertyTypeMap(
        typeof(TModel),
        (type, columnName) =>
            type.GetProperties().FirstOrDefault(prop =>
                prop.GetCustomAttributes(false)
                    .OfType<ColumnAttribute>()
                    .Any(attr => attr.Name == columnName))));

而且模型:

public class TModel {
    [Column(Name="my_property")]
    public int MyProperty { get; set; }
}

需要注意的是,实现CustomPropertyTypeMap需要确保属性存在且与列名之一匹配,否则属性将无法映射。DefaultTypeMap类提供了标准功能,并可用于更改此行为:
public class FallbackTypeMapper : SqlMapper.ITypeMap
{
    private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;

    public FallbackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
    {
        _mappers = mappers;
    }

    public SqlMapper.IMemberMap GetMember(string columnName)
    {
        foreach (var mapper in _mappers)
        {
            try
            {
                var result = mapper.GetMember(columnName);
                if (result != null)
                {
                    return result;
                }
            }
            catch (NotImplementedException nix)
            {
            // the CustomPropertyTypeMap only supports a no-args
            // constructor and throws a not implemented exception.
            // to work around that, catch and ignore.
            }
        }
        return null;
    }
    // implement other interface methods similarly

    // required sometime after version 1.13 of dapper
    public ConstructorInfo FindExplicitConstructor()
    {
        return _mappers
            .Select(mapper => mapper.FindExplicitConstructor())
            .FirstOrDefault(result => result != null);
    }
}

有了这个,就可以很容易地创建一个自定义类型映射程序,如果属性存在,则自动使用这些属性,否则返回标准行为:

public class ColumnAttributeTypeMapper<T> : FallbackTypeMapper
{
    public ColumnAttributeTypeMapper()
        : base(new SqlMapper.ITypeMap[]
            {
                new CustomPropertyTypeMap(
                   typeof(T),
                   (type, columnName) =>
                       type.GetProperties().FirstOrDefault(prop =>
                           prop.GetCustomAttributes(false)
                               .OfType<ColumnAttribute>()
                               .Any(attr => attr.Name == columnName)
                           )
                   ),
                new DefaultTypeMap(typeof(T))
            })
    {
    }
}

那意味着我们现在可以轻松支持需要使用属性映射的类型:
Dapper.SqlMapper.SetTypeMap(
    typeof(MyModel),
    new ColumnAttributeTypeMapper<MyModel>());

这是一个关于编程的完整源代码的Gist

我一直在苦苦挣扎这个问题...这似乎是我应该走的路线...我很困惑这段代码会在哪里被调用 "Dapper.SqlMapper.SetTypeMap(typeof(MyModel), new ColumnAttributeTypeMapper<MyModel>());" - Rohan Büchner
8
建议将此作为官方答案——Dapper的这个特性非常有用。 - killthrush
我很好奇为什么没有包括后备支持。我的猜测是担心会降低速度。 - Karl Kieninger
4
@Oliver 发布的映射解决方案(https://dev59.com/Ymox5IYBdhLWcg3w5IVv#34856158)有效,且需要的代码较少。 - Ricardo stands with Ukraine
34
我喜欢“容易”这个词是多么轻松地被随意使用。 :P - Jonathan B.
显示剩余2条评论

128

这个很好用:

var sql = @"select top 1 person_id PersonId, first_name FirstName, last_name LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(sql).ToList();
    return person;
}

Dapper没有提供可以指定列属性的功能。我不反对添加支持,只要我们不引入依赖项即可。


@Sam Saffron 有没有办法我可以指定表的别名。我有一个名为Country的类,但是由于架构命名约定,数据库中的表名非常复杂。 - TheVillageIdiot
85
列属性对于映射存储过程结果会很有用。 - Ronnie Overby
2
列属性对于更容易地促进领域和工具实现细节之间的紧密物理和/或语义耦合也非常有用。因此,请不要添加对此的支持!!! :) - Derek Greer
2
我不明白为什么在tableattribute中没有columnattribe。这个例子在插入、更新和SPs方面如何工作? 我想看到columnattribe,它非常简单,可以使从实现类似于现在已停用的linq-sql的其他解决方案迁移变得非常容易。 - Vman

120

有一段时间,以下内容应该是有效的:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

9
虽然这并不是对“手动将列名与类属性映射”的真正答案,但对我而言比手动映射要好得多(遗憾的是,在PostgreSQL中最好使用下划线作为列名)。请在以后的版本中不要删除MatchNamesWithUnderscores选项!谢谢! - victorvartan
5
@victorvartan目前没有计划取消“MatchNamesWithUnderscores”选项。最多,如果我们重构配置API,我会保留“MatchNamesWithUnderscores”成员(仍然有效),并添加一个[Obsolete]标记来引导人们使用新的API。 - Marc Gravell
4
@MarcGravell 在你回答开头使用的词语“有一段时间”让我有些担忧,害怕你在未来的版本中会将其删除,谢谢你澄清!同时也非常感谢 Dapper,这是一个很棒的微型 ORM 工具,我最近在 ASP.NET Core 中的小项目中开始使用它和 Npgsql! - victorvartan
2
这是最好的答案。我找到了一堆解决方法,但最终偶然发现了这个。这是最好的答案,但却是最少被宣传的。 - teaMonkeyFruit
当列名为500时,不匹配名为_500的属性。 - PandaWood
1
如果我不得不为多年来你帮助过我的所有答案付费,我会成为一个穷人。 - WBuck

66
我使用动态编程和LINQ进行以下操作:
    var sql = @"select top 1 person_id, first_name, last_name from Person";
    using (var conn = ConnectionFactory.GetConnection())
    {
        List<Person> person = conn.Query<dynamic>(sql)
                                  .Select(item => new Person()
                                  {
                                      PersonId = item.person_id,
                                      FirstName = item.first_name,
                                      LastName = item.last_name
                                  }
                                  .ToList();

        return person;
    }

3
最佳解决方案。 - MiBol
1
实际上那是一个非常好的答案。谢谢。 - Arash.Zandi
完美解决方案。 - daveD

32

这里有一个简单的解决方案,不需要属性,可以让你将基础设施代码从你的POCOs中排除。

这是一个处理映射的类。如果您映射了所有列,则字典可以正常工作,但此类允许您仅指定差异。此外,它还包括反向映射,因此您可以从字段获取列和从列获取字段,这在执行诸如生成SQL语句之类的操作时非常有用。

public class ColumnMap
{
    private readonly Dictionary<string, string> forward = new Dictionary<string, string>();
    private readonly Dictionary<string, string> reverse = new Dictionary<string, string>();

    public void Add(string t1, string t2)
    {
        forward.Add(t1, t2);
        reverse.Add(t2, t1);
    }

    public string this[string index]
    {
        get
        {
            // Check for a custom column map.
            if (forward.ContainsKey(index))
                return forward[index];
            if (reverse.ContainsKey(index))
                return reverse[index];

            // If no custom mapping exists, return the value passed in.
            return index;
        }
    }
}

设置ColumnMap对象并告诉Dapper使用该映射。

var columnMap = new ColumnMap();
columnMap.Add("Field1", "Column1");
columnMap.Add("Field2", "Column2");
columnMap.Add("Field3", "Column3");

SqlMapper.SetTypeMap(typeof (MyClass), new CustomPropertyTypeMap(typeof (MyClass), (type, columnName) => type.GetProperty(columnMap[columnName])));

1
当您的POCO中的属性与数据库返回的内容(例如存储过程)不匹配时,这是一个很好的解决方案。 - crush
1
我有点喜欢使用属性的简洁性,但从概念上讲,这种方法更加清晰 - 它不会将您的POCO与数据库细节耦合在一起。 - Bruno Brant
1
如果我正确理解Dapper,它没有特定的Insert()方法,只有一个Execute()...这种映射方法对插入操作有效吗?或者更新操作呢?谢谢。 - StayOnTarget

23
从当前的Dapper 1.42版本中提取自Dapper Tests
// custom mapping
var map = new CustomPropertyTypeMap(
                 typeof(TypeWithMapping), 
                 (type, columnName) => 
                        type.GetProperties().FirstOrDefault(prop => 
                                GetDescriptionFromAttribute(prop) == columnName));
Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);

获取描述属性名称的辅助类(我个人使用了Column,就像@kalebs的示例一样)

static string GetDescriptionFromAttribute(MemberInfo member)
{
   if (member == null) return null;

   var attrib = (DescriptionAttribute) Attribute.GetCustomAttribute(
                         member,
                         typeof(DescriptionAttribute), false);

   return attrib == null ? null : attrib.Description;
}

课程

public class TypeWithMapping
{
   [Description("B")]
   public string A { get; set; }

   [Description("A")]
   public string B { get; set; }
}

5
为了让它适用于没有定义描述的属性,我将GetDescriptionFromAttribute的返回值更改为return (attrib?.Description ?? member.Name).ToLower();并在映射中添加了.ToLower()来使columnName不区分大小写。 - Sam White
2
谢谢。有没有办法在每个 SQL 调用中设置映射,而不是全局设置?我只需要它在我的某些调用中使用。 - Lukas

23

实现这一点的简单方法是在查询中使用列别名。

如果您的数据库列为PERSON_ID,而对象的属性为ID,则只需执行以下操作:

select PERSON_ID as Id ...
在您的查询中添加此语句,Dapper 将按预期接收它。

18

在打开与数据库的连接之前,对于每个 poco 类执行以下代码:

// Section
SqlMapper.SetTypeMap(typeof(Section), new CustomPropertyTypeMap(
    typeof(Section), (type, columnName) => type.GetProperties().FirstOrDefault(prop =>
    prop.GetCustomAttributes(false).OfType<ColumnAttribute>().Any(attr => attr.Name == columnName))));

然后像这样将数据注释添加到您的 poco 类中:

public class Section
{
    [Column("db_column_name1")] // Side note: if you create aliases, then they would match this.
    public int Id { get; set; }
    [Column("db_column_name2")]
    public string Title { get; set; }
}

之后,一切都准备好了。只需进行查询调用,类似于:

using (var sqlConnection = new SqlConnection("your_connection_string"))
{
    var sqlStatement = "SELECT " +
                "db_column_name1, " +
                "db_column_name2 " +
                "FROM your_table";

    return sqlConnection.Query<Section>(sqlStatement).AsList();
}

3
需要给所有属性添加“Column”属性。如果映射器不可用,有没有办法与属性进行映射? - sandeep.gosavi
@sandeep.gosavi,请查看我在此处的答案 https://dev59.com/u2Uq5IYBdhLWcg3wEcXu#74291093,它不需要映射器。 - DonMiguelSanchez

15

操纵映射边际接近真正的ORM领域。与其与之斗争并保持Dapper真正的简单(快速)形式,不如稍微修改你的SQL语句,像这样:

var sql = @"select top 1 person_id as PersonId,FirstName,LastName from Person";

1
我看不出这个答案与Brad的回答有什么区别:https://dev59.com/Ymox5IYBdhLWcg3w5IVv#30200384 有什么不同吗? - surfmuggle

7
如果你使用的是.NET 4.5.1或更高版本,请查看Dapper.FluentColumnMapping,用于映射LINQ风格。它可以完全将数据库映射与模型分离(无需注释)。

6
我是Dapper.FluentColumnMapping的作者,将映射与模型分开是其中一个主要设计目标。我想要隔离核心数据访问(如存储库接口、模型对象等)与特定于数据库的具体实现,以实现关注点分离。感谢提及,我很高兴您觉得它有用! :-) - Alexander
https://github.com/henkmollema/Dapper-FluentMap 是类似的工具,但是您不再需要第三方包。Dapper已经添加了Dapper.SqlMapper。如果您感兴趣,可以查看我的回答获取更多详细信息。 - tedi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接