Fabien Bézagu

Visibilité interne et NHibernate

Récemment, nous avons fait une petite découverte intéressante concernant NHibernate. Oh, ce n'est qu'une petite chose sans grande importance mais je trouve regrettable que NHibernate ne détecte pas le problème "à la source".

Problème

Parfois, nous voulons réduire la portée d'un constructeur, d'une méthode ou d'une propriété au modèle du domaine en utilisant le mot clef C# internal. Lorsque nous souhaitons rendre persistante la classe contenant ce membre interne, NHibernate offre par défaut son mécanisme de proxy. Nous avons tous eu cette erreur de mapping à la création de la SessionFactory qui dit à peu près : Method should be virtual. Seulement, cet avertissement n'est levé uniquement lorsque la visibilité est publique. Dans le cas d'une méthode interne, NHibernate ne "voit" pas la méthode. Or, cette méthode peut être invoquée depuis l'extérieur de la classe. Deux cas de figures peuvent se présenter.

Réhydratation par NHibernate

NHibernate tente d'utiliser le constructeur interne : dans ce cas, une erreur de type Constructeur non trouvé est lancée. Sans avoir vérifié, je pense que s'il essaye d'accéder à une propriété interne, le même type d'erreur surviendra.

Invocation depuis le même assemblage

Ce cas est beaucoup plus vicieux à détecter et il s'agit de ma réelle motivation pour écrire ce billet. Lorsque la méthode interne est invoquée (donc nécessairement depuis le même assemblage), elle peut accéder à des champs privés. Or ces champs, si la classe est "proxifiée", ne contiennent pas les valeurs réelles. Ces valeurs sont dans le proxy qui intercepte normalement tous les appels par polymorphisme et accède à un tableau contenant tous les champs privés. La méthode interne n'aura donc jamais connaissance des bonnes valeurs des champs privés.

Solution

La solution, afin que NHibernate puisse voir les membre internes des classes qu'il proxifie est donc double :

  • Prendre l'initiative de marquer ces membres comme virtuels : ainsi, le polymorphisme pourra fonctionner,
  • Ajouter la visibilité protégée aux membres : de cette façon, les proxy, qui ne sont pas dans le même assemblage, pourront y accéder. (pour rappel, la visibilité internal protected rend les membres visibles depuis l'assemblage et en plus depuis les héritiers).

Mon réflexe désormais seront donc certainement d'éviter les membres internes purs et d'y adjoindre quasi systématiquement la visibilité protégée.

Fuyez le client Oracle .NET de Microsoft

Avec mon binôme, nous avons perdu une bonne demi-journée à résoudre un problème de performance tortueux. Le contexte est le suivant :

  • Base de données Oracle
  • Client .NET Microsoft (System.Data.Oracle.Client)
  • NHibernate

Nous avons constaté qu'une requête NHibernate prenait un peu trop son temps : environ 40 secondes !! Voici à peu près le code incriminé :

IList articles = session.CreateCriteria(typeof(Article))
        .Add( Expression.In("Code", listeDeCodes) ).List();

listeDeCodes est une liste de 1000 codes.

Je passe les détails de nos investigations mais nous avons fini par faire un programme de test sans NHibernate dont voici le pseudo-code :

requete1 = "select * from articles where code in ('code1', 'code2', ..., 'code100')";
command = new OracleCommand(requete1);
TesterCommande(command, "Liste directe de codes");
 
requete2 = "select * from articles where code in (:code1, :code2, ..., :code100)";
command = new OracleCommand(requete2);
AjouterParametre(command, ":code1", "code1");
AjouterParametre(command, ":code2", "code2");
...
AjouterParametre(command, ":code100", "code100");
TesterCommande(command, "Avec paramètres");
private TesterCommand(IDbCommand cmd, string nomDuTest)
{
  reader = cmd.ExecuteReader();
  chrono.RAZ();
  while (reader.Read())
  {
 
  }
  Console.WriteLine("{0} : {1}ms", nomDuTest, chrono.TotalMilliseconds)
}

Les résultats sont pour le moins surprenants :



Liste directe de codes : 43ms
Avec paramètres : 41000ms

Nous avons donc décidé de faire les mêmes tests avec le client fourni par Oracle : Oracle Data Provider .NET (ODP.NET). Voici les résultats :

Liste directe de codes : 43ms
Avec paramètres : 15ms

NHibernate utilise intensément les requêtes paramétrées (qui sont bien sûr sensées être plus efficaces, le deuxième test le prouve) et ses performances sont donc drastiquement dégradées par la gestion catastrophique des commandes du client Microsoft.