【问题标题】:Manually map column names with class properties使用类属性手动映射列名
【发布时间】:2012-02-12 17:34:05
【问题描述】:

我是 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 =&gt; PersonId)具有对象属性的列名吗?

【问题讨论】:

标签: dapper


【解决方案1】:

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 to the full source code

【讨论】:

  • 我一直在努力解决同样的问题......这似乎是我应该走的路线......我很困惑这段代码在哪里被称为“Dapper.SqlMapper .SetTypeMap(typeof(MyModel), new ColumnAttributeTypeMapper());" stackoverflow.com/questions/14814972/…
  • 您需要在进行任何查询之前调用它一次。例如,您可以在静态构造函数中执行此操作,因为它只需要调用一次。
  • 建议将此作为官方答案 - Dapper 的此功能非常有用。
  • @Oliver (stackoverflow.com/a/34856158/364568) 发布的映射解决方案有效且需要更少的代码。
  • 我喜欢“轻松”这个词如此轻松地被抛出:P
【解决方案2】:

这很好用:

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 的类,但在数据库中,由于古老的命名约定,该表的名称非常复杂。
  • 列属性可以方便地映射存储过程结果。
  • 列属性也有助于更轻松地促进您的域与您用于实现实体的工具实现细节之间的紧密物理和/或语义耦合。因此,不要添加对此的支持!!!! :)
  • 我不明白为什么 tableattribute 时 columnattribe 不存在。此示例如何处理插入、更新和 SP?我希望看到 columnattribe,它非常简单,并且可以轻松地从其他实现类似现在已失效的 linq-sql 的解决方案迁移。
【解决方案3】:

一段时间后,以下应该可以工作:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

【讨论】:

  • 虽然这不是“手动使用类属性映射列名”问题的真正答案,但对我来说,它比手动映射要好得多(不幸的是在 PostgreSQL最好在列名中使用下划线)。请不要在下一个版本中删除 MatchNamesWithUnderscores 选项!谢谢!!!
  • @victorvartan 没有删除MatchNamesWithUnderscores 选项的计划。 充其量,如果我们重构配置 API,我会保留 MatchNamesWithUnderscores 成员(理想情况下仍然有效)并添加一个 [Obsolete] 标记以将人们指向新的 API。
  • @MarcGravell 答案开头的“一段时间”让我担心您可能会在未来的版本中将其删除,感谢您的澄清!非常感谢 Dapper,这是我刚开始在一个小型项目中使用的出色的微型 ORM,以及 ASP.NET Core 上的 Npgsql!
  • 这无疑是最好的答案。我找到了成堆的解决方法,但最终偶然发现了这一点。很容易成为最好但广告最少的答案。
【解决方案4】:

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

【讨论】:

  • 最好的解决方案
  • 其实这是一个很好的答案。谢谢。
【解决方案5】:

这是一个不需要属性的简单解决方案,可让您将基础架构代码排除在 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 中的属性基本上与您的数据库从存储过程返回的内容不匹配时,这是一个很好的解决方案。
  • 我有点喜欢使用属性提供的简洁性,但从概念上讲,这种方法更简洁——它不会将您的 POCO 与数据库详细信息耦合。
  • 如果我正确理解 Dapper,它没有特定的 Insert() 方法,只有一个 Execute()... 这种映射方法是否适用于插入?还是更新?谢谢
【解决方案6】:

取自 Dapper Tests,目前在 Dapper 1.42 上。

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

帮助类从描述属性中获取名称(我个人使用过类似@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; }
}

【讨论】:

  • 为了让它即使对于没有定义描述的属性也能工作,我将GetDescriptionFromAttribute的返回更改为return (attrib?.Description ?? member.Name).ToLower();,并将.ToLower()添加到columnName在地图中它应该'不区分大小写。
  • 谢谢。有没有办法设置每个 SQL 调用而不是全局的映射?我只需要它来使用我一半的通话。
【解决方案7】:

实现此目的的一种简单方法是在查询中的列上使用别名。

如果你的数据库列是PERSON_ID,而你的对象的属性是ID,你可以这样做

select PERSON_ID as Id ...

在您的查询中,Dapper 会按预期提取。

【讨论】:

    【解决方案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();
    }
    

    【讨论】:

    • 它需要所有属性都有Column属性。如果映射器不可用,有什么方法可以映射属性?
    【解决方案9】:

    打乱映射是进入真正 ORM 领域的边界。与其与之抗争并使 Dapper 保持其真正简单(快速)的形式,不如像这样稍微修改一下您的 SQL:

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

    【讨论】:

      【解决方案10】:

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

      【讨论】:

      • 我是 Dapper.FluentColumnMapping 的作者。将映射与模型分离是主要设计目标之一。我想将核心数据访问(即存储库接口、模型对象等)与特定于数据库的具体实现隔离开来,以便彻底分离关注点。感谢您的提及,我很高兴您发现它很有用! :-)
      • github.com/henkmollema/Dapper-FluentMap 类似。但是您不再需要 3rd 方包。 Dapper 添加了 Dapper.SqlMapper。如果您有兴趣,请参阅我的答案以获取更多详细信息。
      【解决方案11】:

      这是对其他答案的背叛。这只是我管理查询字符串的一个想法。

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

      【讨论】:

        【解决方案12】:

        Kaleb 试图解决的问题的简单解决方案是在列属性不存在的情况下接受属性名称:

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

        【讨论】:

          【解决方案13】:

          更简单的方法(与@Matt M 的答案相同,但已更正并添加了对默认地图的回退)

          // override TypeMapProvider to return custom map for every requested type
          Dapper.SqlMapper.TypeMapProvider = type =>
             {
                 // create fallback default type map
                 var fallback = new DefaultTypeMap(type);
                 return new CustomPropertyTypeMap(type, (t, column) =>
                 {
                     var property = t.GetProperties().FirstOrDefault(prop =>
                         prop.GetCustomAttributes(typeof(ColumnAttribute))
                             .Cast<ColumnAttribute>()
                             .Any(attr => attr.Name == column));
          
                     // if no property matched - fall back to default type map
                     if (property == null)
                     {
                         property = fallback.GetMember(column)?.Property;
                     }
          
                     return property;
                 });
             };
          

          【讨论】:

            【解决方案14】:

            对于所有使用 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 进行更改。
            • 但是你必须为你的每一个模型类型调用映射函数,不是吗?我对通用解决方案感兴趣,这样我的所有类型都可以使用该属性,而不必为每种类型调用映射。
            • 我希望看到 DefaultTypeMap 使用一种策略模式来实现,以便可以将其替换为@UriAbramson 提到的原因。见code.google.com/p/dapper-dot-net/issues/detail?id=140
            【解决方案15】:

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

            我希望属性映射能够在全球范围内工作。要么匹配属性名称(也称为默认值),要么匹配类属性上的列属性。我也不想为我映射到的每个类都设置这个。因此,我创建了一个在应用启动时调用的 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 是什么样的?另外,你是如何在启动时调用它的?
            • @GlenF。关键是 CreateChatRequestResponse 长什么样并不重要。它可以是任何 POCO。这会在您的启动中调用。您可以在 StartUp.cs 或 Global.asax 中的应用启动时调用它。
            • 也许我完全错了,但除非CreateChatRequestResponseT 替换,否则这将如何遍历所有实体对象。如果我错了,请纠正我。
            【解决方案16】:

            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))
                        })
                {
                    //
                }
            }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多