手动映射具有类属性的列名


173

我是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)带有对象属性的列名?


Answers:


80

这工作正常:

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没有允许您指定Column Attribute的功能,只要我们不引入依赖项,我就不反对添加对该属性的支持。


@Sam Saffron有什么方法可以指定表别名。我有一个名为Country的类,但是在数据库中,由于过时的命名约定,该表的名称非常复杂。
TheVillageIdiot 2012年

64
列属性对于映射存储过程结果非常有用。
罗尼·奥弗比

2
列属性对于更轻松地促进您的域与您用于实现实体的工具实现详细信息之间的紧密物理和/或语义耦合也很有用。因此,请勿添加对此的支持!!!!:)
Derek Greer 2014年

我不明白为什么tableattribute时columnattribe不存在。此示例如何与插入,更新和SP一起使用?我希望看到columnattribe,它的死法很简单,并且可以很轻松地从实现类似于现在已经停产的linq-sql的其他解决方案中迁移。
Vman

197

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>());

这是完整源代码要点


我一直在为这个问题而苦苦挣扎...这似乎是我应该走的路线...对于将代码命名为“ Dapper.SqlMapper.SetTypeMap(typeof(MyModel), new ColumnAttributeTypeMapper <MyModel>());“ stackoverflow.com/questions/14814972/...
罗汉布氏

进行任何查询之前,您需要先调用一次。例如,您可以在静态构造函数中执行此操作,因为只需调用一次即可。
Kaleb Pederson

7
建议将此作为正式答案-Dapper的此功能非常有用。
killthrush

3
@Oliver(stackoverflow.com/a/34856158/364568)发布的映射解决方案有效且所需的代码更少。
里加

4
我喜欢如何轻松地将“轻松”一词扔掉:P
乔纳森·B

80

在一段时间内,以下应该起作用:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

6
尽管这并不是“ 使用类属性手动映射列名称”问题的真正答案,但对我而言,这比必须手动映射要好得多(不幸的是,在PostgreSQL中,最好在列名称中使用下划线)。请不要在下一版本中删除MatchNamesWithUnderscores选项!谢谢!!!
victorvartan

5
@victorvartan没有删除该MatchNamesWithUnderscores选项的计划。最好的情况是,如果我们重构配置API,我将把该MatchNamesWithUnderscores成员保留在原处(理想情况下仍然可以正常工作),并添加一个[Obsolete]标记以将人们指向新的API。
Marc Gravell

4
@MarcGravell答案开头的“一段时间”一词使我担心您可能会在将来的版本中将其删除,感谢您的澄清!非常感谢Dapper,这是一个很棒的微型ORM,我刚刚开始将它与ASP.NET Core上的Npgsql一起用于一个小型项目!
victorvartan

2
这很容易是最好的答案。我发现了无数的工作环境,但最终偶然发现了这一点。最好的,但广告最少的答案。
teaMonkeyFruit

29

这是一个简单的解决方案,不需要属性,可让您将基础结构代码保留在POCO中。

这是一个处理映射的类。如果您映射了所有列,则字典将起作用,但是此类允许您仅指定差异。此外,它包括反向映射,因此您可以从列中获取字段,并从字段中获取列,这在执行诸如生成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])));

当您的POCO中的属性基本上与数据库从存储过程返回的内容不匹配时,这是一个很好的解决方案。
暗恋

1
我有点喜欢使用属性所提供的简洁性,但是从概念上讲,此方法更加简洁-它不会将POCO与数据库详细信息耦合。
布鲁诺·布兰特

如果我正确理解Dapper,它没有特定的Insert()方法,只有Execute()...这种映射方法可用于插入吗?还是更新?谢谢
UuDdLrLrSs

29

我使用动态和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;
    }

12

一种简单的方法是在查询中的列上使用别名。如果您的数据库列为,PERSON_ID而对象的属性为ID,则只需select PERSON_ID as Id ...在查询中执行操作,Dapper就会按预期进行处理。


12

取自Dapper 1.42当前提供的Dapper测试

// custom mapping
var map = new CustomPropertyTypeMap(typeof(TypeWithMapping), 
                                    (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));
Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);

帮手类,以获取Description属性的名称(我个人使用了@kalebs这样的Column示例)

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; }
}

1
为了使它即使在未定义描述的属性中也能正常工作,我更改了GetDescriptionFromAttributeto 的返回值return (attrib?.Description ?? member.Name).ToLower();并添加.ToLower()columnName了map中,它不应区分大小写。
山姆·怀特

11

贴图是进入真实ORM领域的边界。与其争斗并使Dapper保持其真正的简单(快速)形式,不如修改SQL如下:

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

8

在打开与数据库的连接之前,请为您的每个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();
}

1
它需要所有属性都具有Column属性。万一没有映射器,有什么方法可以映射属性?
sandeep.gosavi

5

如果您使用的是.NET 4.5.1或更高版本,请使用Dapper.FluentColumnMapping映射LINQ样式。它使您可以将数据库映射与模型完全分开(无需注释)


5
我是Dapper.FluentColumnMapping的作者。将映射与模型分离是主要设计目标之一。我希望将核心数据访问(即存储库接口,模型对象等)与特定于数据库的具体实现隔离开来,以实现关注点的清晰分离。感谢您的提及,很高兴您发现它有用!:-)
亚历山大

github.com/henkmollema/Dapper-FluentMap是相似的。但是您不再需要第三者套餐。Dapper添加了Dapper.SqlMapper。如果您有兴趣,请参阅我的答案以获取更多详细信息。
塔德吉

4

这是其他答案无法解决的问题。这只是我管理查询字符串的想法。

人.cs

public class Person 
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public static string Select() 
    {
        return $"select top 1 person_id {nameof(PersonId)}, first_name {nameof(FirstName)}, last_name {nameof(LastName)}from Person";
    }
}

API方法

using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(Person.Select()).ToList();
    return person;
}

1

对于所有使用Dapper 1.12的人,这是完成此工作所需要做的:

  • 添加一个新的列属性类:

      [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property]
    
      public class ColumnAttribute : Attribute
      {
    
        public string Name { get; set; }
    
        public ColumnAttribute(string name)
        {
          this.Name = name;
        }
      }

  • 搜索此行:

    map = new DefaultTypeMap(type);

    并注释掉。

  • 改写这个:

            map = new CustomPropertyTypeMap(type, (t, columnName) =>
            {
              PropertyInfo pi = t.GetProperties().FirstOrDefault(prop =>
                                prop.GetCustomAttributes(false)
                                    .OfType<ColumnAttribute>()
                                    .Any(attr => attr.Name == columnName));
    
              return pi != null ? pi : t.GetProperties().FirstOrDefault(prop => prop.Name == columnName);
            });


  • 我不确定我是否理解-您是否建议用户更改Dapper以使按列进行属性映射成为可能?如果是这样,可以使用我上面发布的代码而无需更改Dapper。
    Kaleb Pederson

    1
    但是,那么您将不得不为每个模型类型调用映射函数,不是吗?我对通用解决方案感兴趣,因此我所有的类型都可以使用该属性,而不必为每种类型调用映射。
    Uri Abramson

    2
    我希望看到使用策略模式实现DefaultTypeMap,以便可以使用@UriAbramson提及的原因来替换它。请参阅code.google.com/p/dapper-dot-net/issues/detail?id=140
    理查德·科莱特

    1

    Kaleb Pederson的解决方案为我工作。我更新了ColumnAttributeTypeMapper以允许自定义属性(在同一域对象上需要两个不同的映射),并更新了属性以允许在需要派生字段且类型不同的情况下使用私有设置方法。

    public class ColumnAttributeTypeMapper<T,A> : FallbackTypeMapper where A : ColumnAttribute
    {
        public ColumnAttributeTypeMapper()
            : base(new SqlMapper.ITypeMap[]
                {
                    new CustomPropertyTypeMap(
                       typeof(T),
                       (type, columnName) =>
                           type.GetProperties( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(prop =>
                               prop.GetCustomAttributes(true)
                                   .OfType<A>()
                                   .Any(attr => attr.Name == columnName)
                               )
                       ),
                    new DefaultTypeMap(typeof(T))
                })
        {
            //
        }
    }

    1

    我知道这是一个相对较旧的线程,但是我想我会把所做的事情扔掉。

    我希望属性映射能够在全球范围内工作。您可以匹配属性名称(也称为默认值),也可以匹配类属性上的列属性。我也不想为我要映射的每个类都进行设置。这样,我创建了一个DapperStart类,该类在应用启动时调用:

    public static class DapperStart
    {
        public static void Bootstrap()
        {
            Dapper.SqlMapper.TypeMapProvider = type =>
            {
                return new CustomPropertyTypeMap(typeof(CreateChatRequestResponse),
                    (t, columnName) => t.GetProperties().FirstOrDefault(prop =>
                        {
                            return prop.Name == columnName || prop.GetCustomAttributes(false).OfType<ColumnAttribute>()
                                       .Any(attr => attr.Name == columnName);
                        }
                    ));
            };
        }
    }

    很简单 不知道我刚刚写这篇文章时会遇到什么问题,但是它可以工作。


    CreateChatRequestResponse是什么样的?另外,您如何在启动时调用它?
    Glen F.

    1
    @GlenF。关键是,CreateChatRequestResponse的外观无关紧要。它可以是任何POCO。这在您的启动中被调用。您可以仅在StartUp.cs或Global.asax中的应用程序启动时调用它。
    Matt M

    也许我完全错了,但是除非CreateChatRequestResponse被替换,否则T它将如何遍历所有Entity对象。如果我错了,请纠正我。
    Fwd079

    0

    Kaleb试图解决的问题的简单解决方案是,如果column属性不存在,则接受属性名称:

    Dapper.SqlMapper.SetTypeMap(
        typeof(T),
        new Dapper.CustomPropertyTypeMap(
            typeof(T),
            (type, columnName) =>
                type.GetProperties().FirstOrDefault(prop =>
                    prop.GetCustomAttributes(false)
                        .OfType<ColumnAttribute>()
                        .Any(attr => attr.Name == columnName) || prop.Name == columnName)));
    
    By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
    Licensed under cc by-sa 3.0 with attribution required.