Michel Perfetti
Mapping de données par attributs: comment éviter les pertes de performance grâce à la génération de MSIL à l'exécution
Cet article présente une classe qui permet le mapping par attributs sur un IDataReader en générant à l'exécution du code MSIL spécifique à la classe à
Par Michel Perfetti publié le 27/09/2005 à 23:03, lu 5773 fois, 5 pages
 2 | La classe Mapper
Téléchargez le code source - 11 Kb
La classe Mapper<T>
Présentation
La classe Mapper<T> est la classe qui est fournie avec l'article. Elle permet de mapper des classes sur un IDataReader, grâce à l'attribut MappingMemberAttribute. Cet attribut gère les propriétés et les champs d'une classe. Les types Nullables<> sont gérés et pour les autres types, seulement, ceux qui possèdent une fonction GetXXX dans l'interface IDataRecord. Pour ceux-là, si la base de données peut renvoyer des données nulles, la propriété IgnoreIfNull de l'attribut doit être à true. La classe possède 3 méthodes publiques :
  1. public List<T> ExtractListFromIDataReader(IDataReader reader)
  2. public BindingList<T> ExtractBindingListFromIDataReader(IDataReader reader)
  3. public void FillICollectionFromIDataReader(IDataReader reader, ICollection<T> collection)
Sa déclaration est simple et se présente sous cette forme :
public class Client
{
    public int? nullableAge;

    [MappingMemberAttribute("age")]
    public int? NullableAge
    {
        get { return nullableAge; }
        set { nullableAge = value; }
    }

    [MappingMemberAttribute("nom")]
    public string nom;

    [MappingMemberAttribute("prenom", IgnoreIfNull = true)]
    public string prenom;
}
Pour son utilisation cela est encore plus simple :
Mapper<Client> ClientMapper = new Mapper<Client>();
...

IDataReader reader = cmd.ExecuteReader();

List<Client> result = ClientMapper.ExtractListFromIDataReader(reader);
Concepts utilisés
La classe Mapper<T> utilise la généricité pour générer le code de mapping pour chaque type spécifiquement. La génération de code est réalisé avec les outils fournis par l'espace de nom System.Reflection.Emit : cet espace de nom contient les API pour générer un assemblage, un module, une classe, des méthodes, etc... Cet article de Mathieu Kempé présente un bon aperçu des fonctionnalités de génération d'assemblage. Nous allons ici nous concentrer sur ce qui fait l'intérêt de la classe : la génération du code de mapping.

Mapper va utiliser intensivement la classe ILGenerator pour écrire le code MSIL nécessaire au mapping. Ecrire du MSIL n'est pas très compliqué au départ, sont fonctionnement par pile simplifie l'écriture.
Préparation au mapping
Pour la classe Mapper, nous n'allons pas générer un assemblage, mais seulement une méthode: System.Reflection.Emit contient la classe DynamicMethod qui permet de générer une méthode pour un module donné. Pour la classe Mapper<>, ce module sera celui de T. Cela nous évite la création d'un assemblage, d'un module et d'une classe !

dynamicMethodCreateCollectionFromDataReader = new DynamicMethod("CreateCollectionFromDataReader",
                                                                typeof(void), 
                                                                new Type[] { 
                                                                    typeof(ICollection<T>), 
                                                                    typeof(IDataReader), 
                                                                    typeof(int[]) }, 
                                                                typeof(T));
Au premier chargement de Mapper<T>, pour chaque type T, le constructeur statique initialise la classe en analysant T. Pour chaque propriété et/ou champs à mapper, les informations suivantes sont extraites:
  • son nom pour le mapping,
  • son type
  • Si oui ou non, les données valeurs de retour « Null » de la base de donnée sont à ignorer
  • Un objet FieldInfo ou PropertyInfo utilisé pour la génération du mapping
static Mapper()
{
    List<MappedMember> entryList = new List<MappedMember>();

    ExtractMappedFields(entryList);

    ExtractMappedProperties(entryList);

    classEntryToMapArray = entryList.ToArray();

    GenerateCompiledMapper();
}


private static void ExtractMappedProperties(List<MappedMember> entryList)
{
    PropertyInfo[] properties = typeof(T).GetProperties();
    foreach (PropertyInfo property in properties)
    {
        if (!property.CanWrite)
            continue;

        MappingMemberAttribute[] mappers = (MappingMemberAttribute[])
            property.GetCustomAttributes(typeof(MappingMemberAttribute), false);
        if (mappers.Length == 0)
        {
            continue;
        }

        string mappingName = mappers[0].FieldName;

        MappedMember ce = new MappedMember(property.PropertyType, property, mappingName);
        ce.IgnoreIfNull = mappers[0].IgnoreIfNull;
        entryList.Add(ce);
    }
}
La fonction de mapping générée prend 3 paramètres :
  • Une collection ICollection<T> qui est la liste à remplir
  • Un IDataReader
  • In tableau de « int », qui contient les indexes de chaque membre dans l'objet IDataReader
Donc, lors de l'appel de la fonction de mapping de l'objet IDataReader, un prétraitement est effectué avant l'appel au code généré. Ce traitement permet d'identifier pour chaque membre à mapper l'index dans l'objet IDataReader pour récupérer les informations :

private void MapIDataReader(IDataReader reader,ICollection<T> result)
{
    int[] ordinalArray = new int[classEntryToMapArray.Length];

    for (int i = 0; i < classEntryToMapArray.Length; ++i)
    {
        MappedMember classEntry = classEntryToMapArray[i];
        int ordinal = reader.GetOrdinal(classEntry.MappingName);
        if (ordinal < 0)
            throw new Exception("Can't map " + classEntry.MappingName);
        ordinalArray[i] = ordinal;
    }
    
    createCollectionFromDataReaderDelegate(result, reader, ordinalArray);
}
 
» Démarrer une discussion
 
Discussion démarée par MickyMax le 18/11/2007 à 14:44, 1 commentaire(s).