Mapper manuellement les noms de colonnes avec les propriétés de classe

173

Je suis nouveau sur le micro ORM Dapper. Jusqu'à présent, je suis capable de l'utiliser pour des trucs simples liés à l'ORM, mais je ne suis pas en mesure de mapper les noms de colonne de la base de données avec les propriétés de classe.

Par exemple, j'ai la table de base de données suivante:

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

et j'ai une classe appelée Person:

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

Veuillez noter que les noms de mes colonnes dans la table sont différents du nom de propriété de la classe à laquelle j'essaie de mapper les données que j'ai obtenues à partir du résultat de la requête.

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

Le code ci-dessus ne fonctionnera pas car les noms de colonne ne correspondent pas aux propriétés (Person) de l'objet. Dans ce scénario, y a-t-il quelque chose que je puisse faire dans Dapper pour mapper manuellement (par exemple person_id => PersonId) les noms de colonnes avec les propriétés d'objet?

user1154985
la source

Réponses:

80

Cela fonctionne bien:

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 n'a aucune fonctionnalité qui vous permet de spécifier un attribut de colonne , je ne suis pas contre l'ajout de la prise en charge de celui-ci, à condition que nous ne tirions pas dans la dépendance.

Sam Safran
la source
@Sam Saffron est-il possible de spécifier l'alias de table. J'ai une classe nommée Country mais dans la base de données, la table a un nom très alambiqué en raison de conventions de dénomination archiques.
TheVillageIdiot
64
L'attribut de colonne serait pratique pour mapper les résultats des procédures stockées.
Ronnie Overby
2
Les attributs de colonne seraient également utiles pour faciliter plus facilement un couplage physique et / ou sémantique étroit entre votre domaine et les détails d'implémentation de l'outil que vous utilisez pour matérialiser vos entités. Par conséquent, n'ajoutez pas de support pour cela !!!! :)
Derek Greer
Je ne comprends pas pourquoi columnattribe n'est pas là quand tableattribute. Comment cet exemple fonctionnerait-il avec les insertions, les mises à jour et les SP? Je voudrais voir columnattribe, son mort simple et rendrait la vie très facile à migrer à partir d'autres solutions qui implémentent quelque chose de similaire comme le désormais défunt linq-sql.
Vman
197

Dapper prend désormais en charge la colonne personnalisée vers les mappeurs de propriétés. Il le fait via l' interface ITypeMap . Une classe CustomPropertyTypeMap est fournie par Dapper qui peut effectuer la plupart de ce travail. Par exemple:

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

Et le modèle:

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

Il est important de noter que l'implémentation de CustomPropertyTypeMap nécessite que l'attribut existe et corresponde à l'un des noms de colonne ou la propriété ne sera pas mappée. La classe DefaultTypeMap fournit la fonctionnalité standard et peut être exploitée pour modifier ce comportement:

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

Et avec cela en place, il devient facile de créer un mappeur de type personnalisé qui utilisera automatiquement les attributs s'ils sont présents, mais qui reviendra autrement au comportement standard:

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

Cela signifie que nous pouvons désormais facilement prendre en charge les types qui nécessitent une carte à l'aide d'attributs:

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

Voici un résumé du code source complet .

Kaleb Pederson
la source
J'ai eu du mal avec ce même problème ... et cela semble être la route que je devrais emprunter ... Je ne sais pas trop où ce code s'appellerait "Dapper.SqlMapper.SetTypeMap (typeof (MyModel), new ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Rohan Büchner
Vous voudrez l'appeler une fois avant de faire des requêtes. Vous pouvez le faire dans un constructeur statique, par exemple, car il ne doit être appelé qu'une seule fois.
Kaleb Pederson le
7
Recommandez d'en faire la réponse officielle - cette fonctionnalité de Dapper est extrêmement utile.
killthrush
3
La solution de cartographie publiée par @Oliver ( stackoverflow.com/a/34856158/364568 ) fonctionne et nécessite moins de code.
Riga
4
J'aime la façon dont le mot «facilement» est lancé sans effort: P
Jonathan B.
80

Pendant un certain temps, les éléments suivants devraient fonctionner:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Marc Gravell
la source
6
Bien que ce ne soit pas vraiment la réponse à la question « Mapper manuellement les noms de colonnes avec les propriétés de classe», pour moi c'est bien mieux que d'avoir à mapper manuellement (malheureusement dans PostgreSQL, il est préférable d'utiliser des traits de soulignement dans les noms de colonnes). Veuillez ne pas supprimer l'option MatchNamesWithUnderscores dans les prochaines versions! Je vous remercie!!!
victorvartan
5
@victorvartan il n'est pas prévu de supprimer l' MatchNamesWithUnderscoresoption. Au mieux , si nous refactorions l'API de configuration, je laisserais le MatchNamesWithUnderscoresmembre en place (cela fonctionne toujours, idéalement) et ajouterais un [Obsolete]marqueur pour diriger les gens vers la nouvelle API.
Marc Gravell
4
@MarcGravell les mots "Depuis un certain temps" au début de votre réponse m'ont fait craindre que vous ne le supprimiez dans une version future, merci de clarifier! Et un grand merci pour Dapper, un merveilleux micro ORM que je viens de commencer à utiliser pour un petit projet avec Npgsql sur ASP.NET Core!
victorvartan
2
C'est facilement la meilleure réponse. J'ai trouvé des tas et des tas de travail autour, mais finalement je suis tombé dessus. Facilement la meilleure réponse mais la moins annoncée.
teaMonkeyFruit
29

Voici une solution simple qui ne nécessite pas d'attributs vous permettant de garder le code d'infrastructure hors de vos POCO.

C'est une classe pour gérer les mappages. Un dictionnaire fonctionnerait si vous mappiez toutes les colonnes, mais cette classe vous permet de spécifier uniquement les différences. En outre, il inclut des cartes inversées afin que vous puissiez obtenir le champ de la colonne et la colonne du champ, ce qui peut être utile lors de tâches telles que la génération d'instructions 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;
        }
    }
}

Configurez l'objet ColumnMap et indiquez à Dapper d'utiliser le mappage.

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
la source
C'est une bonne solution lorsque les propriétés de votre POCO ne correspondent pas à ce que votre base de données renvoie, par exemple une procédure stockée.
écraser le
1
J'aime un peu la concision que donne l'utilisation d'un attribut, mais conceptuellement, cette méthode est plus propre - elle ne couple pas votre POCO aux détails de la base de données.
Bruno Brant
Si je comprends bien Dapper, il n'a pas de méthode Insert () spécifique, juste un Execute () ... cette approche de mappage fonctionnerait-elle pour les insertions? Ou des mises à jour? Merci
UuDdLrLrSs
29

Je fais ce qui suit en utilisant dynamic et 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
la source
12

Un moyen simple d'y parvenir est d'utiliser simplement des alias sur les colonnes de votre requête. Si votre colonne de base de données est PERSON_IDet que la propriété de votre objet est, IDvous pouvez simplement le faire select PERSON_ID as Id ...dans votre requête et Dapper la récupérera comme prévu.

Brad Westness
la source
12

Tiré des tests Dapper qui sont actuellement sur 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);

Classe d'assistance pour obtenir le nom de l'attribut Description (j'ai personnellement utilisé Column comme l'exemple @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;
}

Classe

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

   [Description("A")]
   public string B { get; set; }
}
Oliver
la source
2
Pour que cela fonctionne même pour les propriétés pour lesquelles aucune description n'est définie, j'ai modifié le retour de GetDescriptionFromAttributeà return (attrib?.Description ?? member.Name).ToLower();et ajouté .ToLower()à columnNamela carte, il ne devrait pas être sensible à la casse.
Sam White
11

Jouer avec la cartographie est à la limite du déplacement vers de véritables terres ORM. Au lieu de se battre avec lui et de garder Dapper dans sa vraie forme simple (rapide), modifiez simplement votre SQL légèrement comme ceci:

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

Avant d'ouvrir la connexion à votre base de données, exécutez ce morceau de code pour chacune de vos classes 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))));

Ajoutez ensuite les annotations de données à vos classes poco comme ceci:

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

Après cela, vous êtes prêt. Faites simplement un appel de requête, quelque chose comme:

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
la source
1
Il faut que toutes les propriétés aient un attribut Column. Existe-t-il un moyen de mapper avec la propriété au cas où le mappeur ne serait pas disponible?
sandeep.gosavi
5

Si vous utilisez .NET 4.5.1 ou une version ultérieure, vérifiez Dapper.FluentColumnMapping pour mapper le style LINQ. Il vous permet de séparer complètement le mappage db de votre modèle (pas besoin d'annotations)

mamuesstack
la source
5
Je suis l'auteur de Dapper.FluentColumnMapping. La séparation des mappages des modèles était l'un des principaux objectifs de conception. Je voulais isoler les accès aux données de base (c'est-à-dire les interfaces de référentiel, les objets modèles, etc.) des implémentations concrètes spécifiques à la base de données pour une séparation nette des préoccupations. Merci pour la mention et je suis content que vous l'ayez trouvée utile! :-)
Alexander
github.com/henkmollema/Dapper-FluentMap est similaire. Mais vous n'avez plus besoin d'un package tiers. Dapper a ajouté Dapper.SqlMapper. Voir ma réponse pour plus de détails si vous êtes intéressé.
Tadej
4

C'est le soutien d'autres réponses. C'est juste une pensée que j'avais pour gérer les chaînes de requête.

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

Méthode API

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

pour tous ceux qui utilisent Dapper 1.12, voici ce que vous devez faire pour y parvenir:

  • Ajoutez une nouvelle classe d'attributs de colonne:

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

  • Recherchez cette ligne:

    map = new DefaultTypeMap(type);

    et commentez-le.

  • Écrivez ceci à la place:

            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
    la source
    Je ne suis pas sûr de comprendre - recommandez-vous aux utilisateurs de modifier Dapper pour rendre possible le mappage d'attributs par colonnes? Si tel est le cas, il est possible d'utiliser le code que j'ai publié ci-dessus sans apporter de modifications à Dapper.
    Kaleb Pederson
    1
    Mais alors vous devrez appeler la fonction de mappage pour chacun de vos types de modèles, n'est-ce pas ?? Je suis intéressé par une solution générique afin que tous mes types puissent utiliser l'attribut sans avoir à appeler le mappage pour chaque type.
    Uri Abramson
    2
    Je voudrais voir DefaultTypeMap être implémenté en utilisant un modèle de stratégie tel qu'il puisse être remplacé pour la raison mentionnée par @UriAbramson. Voir code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette
    1

    La solution de Kaleb Pederson a fonctionné pour moi. J'ai mis à jour le ColumnAttributeTypeMapper pour autoriser un attribut personnalisé (exigeant deux mappages différents sur le même objet de domaine) et mis à jour les propriétés pour autoriser les setters privés dans les cas où un champ devait être dérivé et les types différaient.

    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
    la source
    1

    Je sais que c'est un fil de discussion relativement ancien, mais j'ai pensé jeter ce que j'ai fait là-bas.

    Je voulais que le mappage d'attributs fonctionne globalement. Soit vous faites correspondre le nom de la propriété (aka default), soit vous faites correspondre un attribut de colonne sur la propriété de classe. Je ne voulais pas non plus avoir à configurer cela pour chaque classe à laquelle je mappais. En tant que tel, j'ai créé une classe DapperStart que j'appelle au démarrage de l'application:

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

    Assez simple. Je ne sais pas quels problèmes je vais encore rencontrer car je viens d'écrire ceci, mais cela fonctionne.

    Matt M
    la source
    À quoi ressemble CreateChatRequestResponse? Aussi, comment l'invoquez-vous dans le démarrage?
    Glen F.
    1
    @GlenF. le fait est que peu importe à quoi ressemble CreateChatRequestResponse. il peut s'agir de n'importe quel POCO. cela est invoqué dans votre démarrage. Vous pouvez simplement l'appeler sur le démarrage de votre application dans votre StartUp.cs ou votre Global.asax.
    Matt M
    Peut-être que je me trompe complètement, mais à moins qu'il ne CreateChatRequestResponsesoit remplacé par Tcomment cela itérerait à travers tous les objets Entity. S'il vous plait corrigez moi si je me trompe.
    Fwd079
    0

    La solution simple au problème que Kaleb essaie de résoudre est simplement d'accepter le nom de la propriété si l'attribut de colonne n'existe pas:

    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
    la source