Ánh xạ tên cột theo cách thủ công với các thuộc tính lớp


173

Tôi chưa quen với ORM micro Dapper. Cho đến nay tôi có thể sử dụng nó cho các công cụ liên quan đến ORM đơn giản nhưng tôi không thể ánh xạ các tên cột cơ sở dữ liệu với các thuộc tính lớp.

Ví dụ: tôi có bảng cơ sở dữ liệu sau:

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

và tôi có một lớp gọi là Person:

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

Xin lưu ý rằng tên cột của tôi trong bảng khác với tên thuộc tính của lớp mà tôi đang cố gắng ánh xạ dữ liệu mà tôi nhận được từ kết quả truy vấn.

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

Đoạn mã trên sẽ không hoạt động vì tên cột không khớp với thuộc tính (Person) của đối tượng. Trong kịch bản này, có bất cứ điều gì tôi có thể làm trong Dapper để ánh xạ thủ công (ví dụ person_id => PersonId) tên cột với thuộc tính đối tượng không?


bản sao có thể của Dapper. Ánh
xạ

Câu trả lời:


80

Điều này hoạt động tốt:

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 không có cơ sở nào cho phép bạn chỉ định Thuộc tính Cột , tôi không chống lại việc thêm hỗ trợ cho nó, miễn là chúng tôi không phụ thuộc vào phụ thuộc.


@Sam Saffron có bất kỳ cách nào tôi có thể chỉ định bí danh bảng. Tôi có một lớp có tên Country nhưng trong db, bảng có tên rất phức tạp do các quy ước đặt tên lưu trữ.
TheVillageIdiot

64
Cột Attribue sẽ thuận tiện cho việc ánh xạ kết quả thủ tục được lưu trữ.
Ronnie Overby

2
Các thuộc tính cột cũng sẽ hữu ích để dễ dàng tạo điều kiện kết nối chặt chẽ hơn về mặt vật lý và / hoặc ngữ nghĩa giữa tên miền của bạn và chi tiết triển khai công cụ bạn đang sử dụng để cụ thể hóa các thực thể của mình. Vì vậy, đừng thêm hỗ trợ cho việc này !!!! :)
Derek Greer

Tôi không hiểu tại sao cột lại không có ở đó khi đóng góp. Làm thế nào ví dụ này hoạt động với chèn, cập nhật và SP? Tôi muốn xem cột, đơn giản và sẽ làm cho cuộc sống rất dễ dàng di chuyển từ các giải pháp khác thực hiện một cái gì đó tương tự như linq-sql hiện không còn tồn tại.
Vman

197

Dapper hiện hỗ trợ cột tùy chỉnh cho người lập bản đồ tài sản. Nó làm như vậy thông qua giao diện ITypeMap . Một lớp CustomPropertyTypeMap được cung cấp bởi Dapper có thể thực hiện hầu hết công việc này. Ví dụ:

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

Và mô hình:

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

Điều quan trọng cần lưu ý là việc triển khai CustomPropertyTypeMap yêu cầu thuộc tính tồn tại và khớp với một trong các tên cột hoặc thuộc tính sẽ không được ánh xạ. Lớp DefaultTypeMap cung cấp chức năng tiêu chuẩn và có thể được tận dụng để thay đổi hành vi này:

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

Và với điều đó, việc tạo một trình ánh xạ loại tùy chỉnh sẽ tự động sử dụng các thuộc tính nếu chúng có mặt nhưng sẽ trở lại hành vi tiêu chuẩn trở nên dễ dàng hơn:

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

Điều đó có nghĩa là bây giờ chúng ta có thể dễ dàng hỗ trợ các loại yêu cầu bản đồ bằng các thuộc tính:

Dapper.SqlMapper.SetTypeMap(
    typeof(MyModel),
    new ColumnAttributeTypeMapper<MyModel>());

Đây là một ý chính cho mã nguồn đầy đủ .


Tôi đã phải vật lộn với vấn đề tương tự này ... và có vẻ như con đường tôi nên đi ... Tôi khá bối rối về việc mã này sẽ được gọi là "Dapper.SqlMapper.SetTypeMap (typeof (MyModel), cột mớiAttributionTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/ hy
Rohan Büchner

Bạn sẽ muốn gọi nó một lần trước khi bạn thực hiện bất kỳ truy vấn. Bạn có thể làm điều đó trong một hàm tạo tĩnh, ví dụ, vì nó chỉ cần được gọi một lần.
Kaleb Pederson

7
Đề nghị biến đây thành câu trả lời chính thức - tính năng này của Dapper cực kỳ hữu ích.
killthrush

3
Giải pháp lập bản đồ được đăng bởi @Oliver ( stackoverflow.com/a/34856158/364568 ) hoạt động và yêu cầu ít mã hơn.
Riga

4
Tôi thích cách từ "dễ dàng" được ném xung quanh một cách dễ dàng như vậy: P
Jonathan B.

80

Trong một thời gian, những điều sau đây nên hoạt động:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

6
Mặc dù đây không phải là thực sự là câu trả lời cho các câu hỏi để " thủ tên cột Map với thuộc tính của lớp", đối với tôi nó tốt hơn nhiều so với việc tự lập bản đồ (tiếc là trong PostgreSQL nó tốt hơn để dấu gạch sử dụng trong tên cột). Vui lòng không xóa tùy chọn MatchNamesWithUnderscores trong các phiên bản tiếp theo! Cảm ơn bạn!!!
victorvartan

5
@victorvartan không có kế hoạch loại bỏ MatchNamesWithUnderscorestùy chọn này. Tốt nhất , nếu chúng tôi tái cấu trúc API cấu hình, tôi sẽ giữ nguyên MatchNamesWithUnderscoresthành viên (điều đó vẫn hoạt động, lý tưởng) và thêm một [Obsolete]điểm đánh dấu để hướng mọi người đến API mới.
Marc Gravell

4
@MarcGravell các từ "Trong một thời gian" ở đầu câu trả lời của bạn khiến tôi lo lắng rằng bạn có thể xóa nó trong phiên bản tương lai, cảm ơn vì đã làm rõ! Và một lời cảm ơn lớn cho Dapper, một ORM vi mô tuyệt vời mà tôi mới bắt đầu sử dụng cho một dự án nhỏ cùng với Npgsql trên ASP.NET Core!
victorvartan

2
Đây dễ dàng là câu trả lời tốt nhất. Tôi đã tìm thấy hàng đống công việc xung quanh, nhưng cuối cùng cũng vấp phải điều này. Dễ dàng là câu trả lời tốt nhất nhưng ít quảng cáo nhất.
teaMonkeyFnut

29

Đây là một giải pháp đơn giản không yêu cầu các thuộc tính cho phép bạn giữ mã cơ sở hạ tầng khỏi POCO của bạn.

Đây là một lớp để đối phó với các ánh xạ. Một từ điển sẽ hoạt động nếu bạn ánh xạ tất cả các cột, nhưng lớp này cho phép bạn chỉ định sự khác biệt. Ngoài ra, nó bao gồm các bản đồ đảo ngược để bạn có thể lấy trường từ cột và cột từ trường, có thể hữu ích khi thực hiện những việc như tạo câu lệnh 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;
        }
    }
}

Thiết lập đối tượng ColumnMap và báo cho Dapper sử dụng ánh xạ.

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

Đây là một giải pháp tốt khi về cơ bản bạn có sự không phù hợp của các thuộc tính trong POCO của bạn với cơ sở dữ liệu của bạn đang trở về, ví dụ, một thủ tục được lưu trữ.
nghiền nát

1
Tôi giống như sự đồng nhất mà việc sử dụng một thuộc tính mang lại, nhưng về mặt khái niệm phương pháp này sạch hơn - nó không kết hợp POCO của bạn với các chi tiết cơ sở dữ liệu.
Bruno Brant

Nếu tôi hiểu Dapper một cách chính xác, nó không có một phương thức Chèn () cụ thể, chỉ là Thực thi () ... phương pháp ánh xạ này có hiệu quả đối với các phần chèn thêm không? Hoặc cập nhật? Cảm ơn
UuDdLrLrSs

29

Tôi làm như sau bằng cách sử dụng động và 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

Một cách dễ dàng để đạt được điều này là chỉ sử dụng bí danh trên các cột trong truy vấn của bạn. Nếu cột cơ sở dữ liệu của bạn là PERSON_IDvà proprty của đối tượng của IDbạn là bạn có thể thực hiện select PERSON_ID as Id ...trong truy vấn của mình và Dapper sẽ chọn nó như mong đợi.


12

Lấy từ các thử nghiệm Dapper hiện đang có trên 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);

Lớp trình trợ giúp để lấy tên khỏi thuộc tính Mô tả (Cá nhân tôi đã sử dụng Cột như ví dụ @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;
}

Lớp học

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

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

1
Để cho nó để làm việc ngay cả đối với thuộc tính mà không có mô tả được xác định, tôi đã thay đổi sự trở lại của GetDescriptionFromAttributeđể return (attrib?.Description ?? member.Name).ToLower();và thêm .ToLower()vào columnNametrong bản đồ nó không phải là trường hợp nhạy cảm.
Sam White

11

Lộn xộn với ánh xạ là đường biên giới di chuyển vào vùng đất ORM thực sự. Thay vì chiến đấu với nó và giữ Dapper ở dạng đơn giản (nhanh) thực sự của nó, chỉ cần sửa đổi SQL của bạn một chút như vậy:

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

8

Trước khi bạn mở kết nối tới cơ sở dữ liệu của mình, hãy thực thi đoạn mã này cho từng lớp poco của bạn:

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

Sau đó thêm các chú thích dữ liệu vào các lớp poco của bạn như thế này:

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

Sau đó, bạn đã hoàn tất. Chỉ cần thực hiện một cuộc gọi truy vấn, đại loại như:

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
Nó cần tất cả các thuộc tính để có thuộc tính Cột. Có cách nào để lập bản đồ với tài sản trong trường hợp mapper không có sẵn không?
sandeep.gosavi

5

Nếu bạn đang sử dụng .NET 4.5.1 hoặc cao hơn, hãy kiểm tra Dapper.FluentColumnMapping để ánh xạ kiểu LINQ. Nó cho phép bạn tách biệt hoàn toàn ánh xạ db khỏi mô hình của bạn (không cần chú thích)


5
Tôi là tác giả của Dapper.FluentColumnMapping. Tách các ánh xạ khỏi các mô hình là một trong những mục tiêu thiết kế chính. Tôi muốn tách biệt quyền truy cập dữ liệu cốt lõi (ví dụ: giao diện kho lưu trữ, đối tượng mô hình, v.v.) khỏi các triển khai cụ thể của cơ sở dữ liệu để tách biệt các mối quan tâm. Cảm ơn đã đề cập và tôi rất vui vì bạn thấy nó hữu ích! :-)
Alexander

github.com/henkmollema/Dapper-FluentMap cũng tương tự. Nhưng bạn không cần gói bên thứ 3 nữa. Dapper đã thêm Dapper.SqlMapper. Xem câu trả lời của tôi để biết thêm chi tiết nếu bạn quan tâm.
Tadej

4

Đây là heo con ủng hộ các câu trả lời khác. Đó chỉ là suy nghĩ của tôi để quản lý các chuỗi truy vấn.

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

Phương pháp API

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

1

cho tất cả các bạn sử dụng Dapper 1.12, đây là những gì bạn cần làm để hoàn thành công việc này:

  • Thêm một lớp thuộc tính cột mới:

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

  • Tìm kiếm dòng này:

    map = new DefaultTypeMap(type);

    và nhận xét nó ra.

  • Viết cái này thay thế:

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


  • Tôi không chắc là tôi hiểu - bạn có khuyên người dùng thay đổi Dapper để tạo ánh xạ thuộc tính theo các cột không? Nếu vậy, có thể sử dụng mã tôi đã đăng ở trên mà không thực hiện thay đổi đối với Dapper.
    Kaleb Pederson

    1
    Nhưng sau đó, bạn sẽ phải gọi chức năng ánh xạ cho từng loại Mô hình của mình phải không ?? Tôi quan tâm đến một giải pháp chung để tất cả các loại của tôi có thể sử dụng thuộc tính mà không phải gọi ánh xạ cho từng loại.
    Uri Abramson

    2
    Tôi muốn thấy DefaultTypeMap được triển khai bằng cách sử dụng một mẫu chiến lược sao cho nó có thể được thay thế vì lý do @UriAbramson đề cập. Xem code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette

    1

    Giải pháp của Kaleb Pederson đã làm việc cho tôi. Tôi đã cập nhật CộtAttributionTypeMapper để cho phép một thuộc tính tùy chỉnh (có yêu cầu đối với hai ánh xạ khác nhau trên cùng một đối tượng miền) và các thuộc tính được cập nhật để cho phép các setters riêng trong trường hợp một trường cần xuất phát và các loại khác nhau.

    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

    Tôi biết đây là một chủ đề tương đối cũ, nhưng tôi nghĩ tôi sẽ ném những gì tôi đã làm ra khỏi đó.

    Tôi muốn ánh xạ thuộc tính để làm việc trên toàn cầu. Hoặc bạn khớp với tên thuộc tính (còn gọi là mặc định) hoặc bạn khớp với thuộc tính cột trên thuộc tính lớp. Tôi cũng không muốn phải thiết lập điều này cho mỗi lớp tôi đang ánh xạ. Như vậy, tôi đã tạo một lớp DapperStart mà tôi gọi khi bắt đầu ứng dụng:

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

    Khá đơn giản. Không chắc vấn đề gì tôi sẽ gặp phải khi tôi vừa viết bài này, nhưng nó hoạt động.


    CreatChatRequestResponse trông như thế nào? Ngoài ra, làm thế nào bạn gọi nó trong khởi động?
    Glen F.

    1
    @GlenF. vấn đề là không quan trọng CreatChatRequestResponse trông như thế nào. nó có thể là bất kỳ POCO nào. Điều này được viện dẫn trong khởi nghiệp của bạn. Bạn chỉ có thể gọi nó trên ứng dụng của mình bắt đầu trong StartUp.cs hoặc Global.asax của bạn.
    Matt M

    Có lẽ tôi hoàn toàn sai, nhưng trừ khi CreateChatRequestResponseđược thay thế bằng Tcách lặp lại thông qua tất cả các đối tượng Thực thể. Xin hãy sửa tôi nếu tôi sai.
    Fwd079

    0

    Giải pháp đơn giản cho vấn đề mà Kaleb đang cố gắng giải quyết chỉ là chấp nhận tên thuộc tính nếu thuộc tính cột không tồn tại:

    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)));
    
    Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
    Licensed under cc by-sa 3.0 with attribution required.