Memetakan nama kolom secara manual dengan properti kelas

173

Saya baru mengenal ORM mikro Dapper. Sejauh ini saya dapat menggunakannya untuk hal-hal sederhana yang berhubungan dengan ORM, tetapi saya tidak dapat memetakan nama kolom basis data dengan properti kelas.

Sebagai contoh, saya memiliki tabel database berikut:

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

dan saya memiliki kelas yang disebut Person:

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

Harap perhatikan bahwa nama kolom saya di tabel berbeda dari nama properti kelas yang saya coba petakan data yang saya dapatkan dari hasil kueri.

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

Kode di atas tidak akan berfungsi karena nama kolom tidak cocok dengan properti (Orang) objek. Dalam skenario ini, apakah ada yang bisa saya lakukan di Dapper untuk memetakan secara manual (misalnya person_id => PersonId) nama kolom dengan properti objek?

pengguna1154985
sumber
kemungkinan duplikat Dapper. Peta ke SQL Column dengan spasi dalam nama kolom
David McEleney

Jawaban:

80

Ini berfungsi dengan baik:

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 tidak memiliki fasilitas yang memungkinkan Anda menentukan Atribut Kolom , saya tidak menentang menambahkan dukungan untuk itu, asalkan kami tidak menarik ketergantungan.

Sam Saffron
sumber
@Sam Saffron apakah ada cara saya dapat menentukan alias tabel. Saya memiliki kelas bernama Country tetapi di db tabel memiliki nama yang sangat berbelit-belit karena konvensi penamaan yang kuno.
TheVillageIdiot
64
Attribue Kolom akan berguna untuk memetakan hasil prosedur yang tersimpan.
Ronnie Overby
2
Atribut kolom juga akan berguna untuk lebih memudahkan memfasilitasi penggabungan fisik dan / atau semantik yang ketat antara domain Anda dan detail implementasi alat yang Anda gunakan untuk mewujudkan entitas Anda. Karena itu, jangan menambahkan dukungan untuk ini !!!! :)
Derek Greer
Saya tidak mengerti mengapa tidak ada kolomattribe ketika tableattribute. Bagaimana contoh ini bekerja dengan sisipan, pembaruan, dan SP? Saya ingin melihat columnattribe, ini sederhana dan akan membuat hidup sangat mudah bermigrasi dari solusi lain yang menerapkan sesuatu yang mirip seperti LINQ-sql yang sekarang sudah tidak berfungsi.
Vman
197

Dapper sekarang mendukung kolom khusus ke pemetaan properti. Ia melakukannya melalui antarmuka ITypeMap . Kelas CustomPropertyTypeMap disediakan oleh Dapper yang dapat melakukan sebagian besar pekerjaan ini. Sebagai contoh:

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

Dan modelnya:

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

Penting untuk dicatat bahwa implementasi CustomPropertyTypeMap mensyaratkan bahwa atribut ada dan cocok dengan salah satu nama kolom atau properti tidak akan dipetakan. Kelas DefaultTypeMap menyediakan fungsionalitas standar dan dapat dimanfaatkan untuk mengubah perilaku ini:

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

Dan dengan itu, mudah untuk membuat mapper tipe khusus yang secara otomatis akan menggunakan atribut jika mereka ada tetapi sebaliknya akan kembali ke perilaku standar:

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

Itu berarti kita sekarang dapat dengan mudah mendukung tipe yang memerlukan peta menggunakan atribut:

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

Berikut adalah Intisari ke kode sumber lengkap .

Kaleb Pederson
sumber
Saya telah berjuang dengan masalah yang sama ini ... dan ini sepertinya rute yang harus saya tuju ... Saya cukup bingung ke mana kode ini akan dipanggil "Dapper.SqlMapper.SetTypeMap (typeof (MyModel), ColumnAttributeTypeMapper baru <MyModel> ()); " stackoverflow.com/questions/14814972/…
Rohan Büchner
Anda akan ingin menyebutnya sekali sebelum Anda membuat pertanyaan. Anda dapat melakukannya di konstruktor statis, misalnya, karena hanya perlu dipanggil sekali.
Kaleb Pederson
7
Rekomendasikan agar ini menjadi jawaban resmi - fitur Dapper ini sangat berguna.
killthrush
3
Solusi pemetaan yang diposting oleh @Oliver ( stackoverflow.com/a/34856158/364568 ) berfungsi dan membutuhkan lebih sedikit kode.
Riga
4
Saya suka bagaimana kata "mudah" dilemparkan dengan begitu mudah: P
Jonathan B.
80

Untuk beberapa waktu, berikut ini akan berfungsi:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Marc Gravell
sumber
6
Meskipun ini bukan jawaban untuk pertanyaan " Memetakan nama kolom secara manual dengan properti kelas", bagi saya itu jauh lebih baik daripada harus memetakan secara manual (sayangnya di PostgreSQL lebih baik menggunakan garis bawah pada nama kolom). Tolong jangan hapus opsi MatchNamesWithUnderscores di versi selanjutnya! Terima kasih!!!
victorvartan
5
@victorvartan tidak ada rencana untuk menghapus MatchNamesWithUnderscoresopsi. Paling-paling , jika kami refactored API konfigurasi, saya akan meninggalkan MatchNamesWithUnderscoresanggota di tempatnya (yang masih berfungsi, idealnya) dan menambahkan [Obsolete]penanda untuk mengarahkan orang ke API baru.
Marc Gravell
4
@MarcGravell kata-kata "Untuk beberapa waktu" di awal jawaban Anda membuat saya khawatir bahwa Anda mungkin menghapusnya di versi mendatang, terima kasih telah menjelaskan! Dan terima kasih banyak untuk Dapper, ORM mikro yang luar biasa yang baru saja saya gunakan untuk proyek kecil bersama dengan Npgsql di ASP.NET Core!
victorvartan
2
Ini dengan mudah jawaban terbaik. Saya telah menemukan tumpukan dan tumpukan pekerjaan di sekitar, tetapi akhirnya menemukan ini. Mudah jawaban terbaik tapi paling tidak diiklankan.
teaMonkeyFruit
29

Berikut ini adalah solusi sederhana yang tidak memerlukan atribut yang memungkinkan Anda untuk menjaga kode infrastruktur dari POCO Anda.

Ini adalah kelas untuk berurusan dengan pemetaan. Kamus akan berfungsi jika Anda memetakan semua kolom, tetapi kelas ini memungkinkan Anda menentukan hanya perbedaannya. Selain itu, ini termasuk peta terbalik sehingga Anda bisa mendapatkan bidang dari kolom dan kolom dari bidang, yang dapat berguna saat melakukan hal-hal seperti menghasilkan pernyataan 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;
        }
    }
}

Setup objek ColumnMap dan beri tahu Dapper untuk menggunakan pemetaan.

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])));
Randall Sutton
sumber
Ini adalah solusi yang baik ketika Anda pada dasarnya memiliki ketidakcocokan properti di POCO Anda dengan apa yang kembali dari database Anda, misalnya, prosedur tersimpan.
naksir
1
Saya agak suka keringkasan yang diberikan oleh atribut, tetapi secara konseptual metode ini lebih bersih - ini tidak memasangkan POCO Anda dengan detail basis data.
Bruno Brant
Jika saya memahami Dapper dengan benar, ia tidak memiliki metode Sisipkan () khusus, hanya Eksekusi () ... akankah pendekatan pemetaan ini berfungsi untuk penyisipan? Atau pembaruan? Terima kasih
UuDdLrLrSs
29

Saya melakukan hal berikut ini menggunakan dynamic dan 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;
    }
liorafar
sumber
12

Cara mudah untuk mencapai ini adalah dengan hanya menggunakan alias pada kolom dalam permintaan Anda. Jika kolom basis data Anda PERSON_IDdan proprty objek Anda, IDAnda dapat melakukannya select PERSON_ID as Id ...di kueri dan Dapper akan mengambilnya seperti yang diharapkan.

Brad Westness
sumber
12

Diambil dari Tes Dapper yang saat ini ada di 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);

Kelas pembantu untuk mendapatkan nama dari atribut Deskripsi (Saya pribadi telah menggunakan Kolom seperti contoh @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;
}

Kelas

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

   [Description("A")]
   public string B { get; set; }
}
Oliver
sumber
2
Agar berfungsi bahkan untuk properti di mana tidak ada deskripsi yang didefinisikan, saya mengubah kembali GetDescriptionFromAttributeke return (attrib?.Description ?? member.Name).ToLower();dan ditambahkan .ToLower()ke columnNamedalam peta yang seharusnya tidak peka huruf besar-kecil.
Sam White
11

Messing with mapping adalah pemindahan batas ke tanah ORM nyata. Alih-alih berkelahi dengannya dan menjaga Dapper dalam bentuknya yang sederhana (cepat), modifikasi SQL Anda sedikit seperti ini:

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

Sebelum Anda membuka koneksi ke database Anda, jalankan kode ini untuk setiap kelas poco Anda:

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

Kemudian tambahkan anotasi data ke kelas poco Anda seperti ini:

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

Setelah itu, Anda siap. Buat saja panggilan permintaan, sesuatu seperti:

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();
}
Tadej
sumber
1
Perlu semua properti memiliki atribut Kolom. Apakah ada cara untuk memetakan dengan properti jika mapper tidak tersedia?
sandeep.gosavi
5

Jika Anda menggunakan .NET 4.5.1 atau checkout Dapper.FluentColumnMapping yang lebih tinggi untuk memetakan gaya LINQ. Ini memungkinkan Anda sepenuhnya memisahkan pemetaan db dari model Anda (tidak perlu penjelasan)

mamuesstack
sumber
5
Saya penulis Dapper.FluentColumnMapping. Memisahkan pemetaan dari model adalah salah satu tujuan desain utama. Saya ingin mengisolasi akses data inti (yaitu antarmuka repositori, objek model, dll ...) dari implementasi konkret khusus database untuk pemisahan masalah yang bersih. Terima kasih untuk menyebutkannya dan saya senang Anda menemukannya berguna! :-)
Alexander
github.com/henkmollema/Dapper-FluentMap serupa. Tetapi Anda tidak membutuhkan paket pihak ke-3 lagi. Dapper menambahkan Dapper.SqlMapper. Lihat jawaban saya untuk lebih jelasnya jika Anda tertarik.
Tadej
4

Ini celengan mundur dari jawaban lain. Itu hanya pemikiran yang saya miliki untuk mengelola string kueri.

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

Metode API

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

untuk Anda semua yang menggunakan Dapper 1.12, Inilah yang perlu Anda lakukan untuk menyelesaikan ini:

  • Tambahkan kelas atribut kolom baru:

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

  • Cari baris ini:

    map = new DefaultTypeMap(type);

    dan berkomentar.

  • Tulis ini sebagai gantinya:

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

  • Uri Abramson
    sumber
    Saya tidak yakin saya mengerti - apakah Anda menyarankan agar pengguna mengubah Dapper untuk memungkinkan pemetaan atribut menurut kolom? Jika demikian, mungkin menggunakan kode yang saya posting di atas tanpa membuat perubahan pada Dapper.
    Kaleb Pederson
    1
    Tetapi kemudian Anda harus memanggil fungsi pemetaan untuk masing-masing dan setiap Jenis Model Anda bukan? Saya tertarik dengan solusi umum sehingga semua tipe saya dapat menggunakan atribut tanpa harus memanggil pemetaan untuk setiap tipe.
    Uri Abramson
    2
    Saya ingin melihat DefaultTypeMap diimplementasikan menggunakan pola strategi sehingga dapat diganti dengan alasan @UriAbramson menyebutkan. Lihat code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette
    1

    Solusi Kaleb Pederson bekerja untuk saya. Saya memperbarui ColumnAttributeTypeMapper untuk memungkinkan atribut khusus (memiliki persyaratan untuk dua pemetaan berbeda pada objek domain yang sama) dan memperbarui properti untuk memungkinkan setter pribadi dalam kasus di mana bidang perlu diturunkan dan jenisnya berbeda.

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

    Saya tahu ini adalah utas yang relatif lama, tetapi saya pikir saya akan membuang apa yang saya lakukan di luar sana.

    Saya ingin pemetaan atribut berfungsi secara global. Entah Anda cocok dengan nama properti (alias default) atau Anda cocok dengan atribut kolom pada properti kelas. Saya juga tidak mau harus mengatur ini untuk setiap kelas yang saya pemetaan. Karena itu, saya membuat kelas DapperStart yang saya gunakan saat memulai aplikasi:

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

    Cukup mudah. Tidak yakin masalah apa yang akan saya temui karena saya baru saja menulis ini, tetapi berhasil.

    Matt M
    sumber
    Seperti apa bentuk CreateChatRequestResponse? Juga, bagaimana Anda menjalankannya di startup?
    Glen F.
    1
    @GlenF. intinya adalah bahwa tidak masalah seperti apa bentuk CreateChatRequestResponse. bisa POCO apa saja. ini dipanggil di startup Anda. Anda bisa menjalankannya di aplikasi Anda mulai di StartUp.cs Anda atau Global.asax Anda.
    Matt M
    Mungkin saya benar-benar salah, tetapi kecuali CreateChatRequestResponsedigantikan oleh Tbagaimana ini akan beralih melalui semua objek Entity. Tolong koreksi saya jika saya salah.
    Fwd079
    0

    Solusi sederhana untuk masalah yang Kaleb coba selesaikan adalah hanya menerima nama properti jika atribut kolom tidak ada:

    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)));
    
    Stewart Cunningham
    sumber