| « Gartner ne conseille pas de migrer à Office 2007 | DataBinding dans ASP.Net 2.0 et NHibernate, pas de panique » |
DataBinding dans ASP.Net 2.0 et NHibernate, pas de panique (suite)
Le problème vient d’Hibernate, qui a été conçu dans un certain état d’esprit si l’on peut dire, pour certains types d’applications et de façon de programmer.
Il semblerait que ses concepteurs viennent de l’école N-tiers des applications connectés comme ce que l’on peut trouver avec des clients riches Java ou Windows.
Nous faisons ici du Web (1.0 ou 2.0 peu importe). Et donc les objets que nous manipulons n’ont d’espérance raisonnable de vie que le temps d’un traitement coté serveur et un aller/retour d’information entre le navigateur client (ce serait valable aussi pour du smart client) et les instances de traitement coté serveur (services/métier/domaines). Et en plus nous utilisons un moyen de binding entre les objets graphiques Web (un DetailView de ASP.Net 2.0) et les objets NHibernate.
Du coup, la méthode Update disponible dans NHibernate, ET DANS LA MESURE OU NOUS MAINTENONS LA SESSION NHIBERNATE EN MEMOIRE (sur le tas, via un Singleton qui renvoie une instance statique, ou dans l’espace mémoire de Session ASP.NET [mais je n’ai pas testé ce cas] ), nous renvoie une exception qui dit : « vous ne pouvez pas faire d’update sur cet objet car il est différent de celui qui existe déjà dans votre session NHib ».
Que faut-il comprendre ?
Nhib raisonne dans sa logique de session, et si vous prenez (Get ou Load) un objet de données, c’est pour le manipuler lui dans la même session, le modifier, le supprimer. Cela a du sens, en particulier pour assurer une vrai bonne transaction et pour éventuellement poser des verrous (méthode Lock sur l’objet Session de NHib).
Voir la documentation complète à ce sujet http://www.hibernate.org/hib_docs/nhibernate/html/transactions.html
Et en français http://www.hibernate.org/hib_docs/v3/reference/fr/html/objectstate.html#objectstate-detached
Lock permet de réassocier un objet qui est resté en mémoire lors d’une nouvelles session NHibernate.
Mais nous avons le problème inverse. Nous avons gardé la même session (voir plus haut) mais pas le même objet. EN effet, si on laisse faire le Two-Way Databinding, à chaque exécution de la page qui contient le composant webform DetailView , l’objet de données est régénéré. Il suffit de poser un point d’arrêt sur DetailsView1_ItemCreated pour s’en rendre compte. Cet événement est même déclenché 2 fois lorsqu’on est en mode « Edit » du DetailView et que l’utilisateur vient de valider sa saisie.
Nous le savons bien maintenant : on n’a jamais assez le contrôle sur un widget tiers ou sur du code généré par un wizard et tout ce qui peut s’apparenter à un automatisme.
Il faut donc mettre les mains dans le cambouis, et pallier soi-même à ce genre de défaut.
Principe : il faudrait lors du mécanisme d’update, assumer le fait que l’objet de données qui nous est passé n’est pas le même que celui qui a été utilisé pour faire la lecture (Load renvoie un objet A). Donc dans la méthode update, il nous faudra relire depuis la base un nouvel objet de données (appelons le C), lui passer les informations obtenues depuis l’objet qui contient les donnée modifiées (objet B qui, lui, est généré automatiquement par le 2-way binding du DetailView) et sauver l’objet C ce qui satisfait au fonctionnement NHib.

Si nous passions au code ?
La métohde de notre objet de domaine utilisée comme méthode d’update dans la déclaration du tag DataSourceObject aura cette tête (ce corps plutot):
[DataObjectMethod(DataObjectMethodType.Update)]
public bool Modify2(X.DataEntityObjects.TRestaurants modifiedData)
{
ITransaction tx = null;
try
{
tx = nHibSession.BeginTransaction();
TRestaurants originalData = (TRestaurants)nHibSession.Get(typeof(TRestaurants), modifiedData.Id);
if (originalData == null)
{
tx.Rollback();
return false; //on ne peut pas faire la transaction
}
nHibSession.Lock( originalData , LockMode.Upgrade);
originalData.Adresse = modifiedData.Adresse;
originalData.CodePostal = modifiedData.CodePostal;
originalData.Gerant = modifiedData.Gerant;
originalData.Nom = modifiedData.Nom;
originalData.Pays = modifiedData.Pays;
originalData.Ville = modifiedData.Ville;
nHibSession.Update(originalData);
tx.Commit();
}
catch (HibernateException ex)
{
tx.Rollback();
throw ex;
}
return true;
}
On voit donc qu’on est obligé de ré- obtenir un objet de données correspondant à la même clef primaire (modifiedData.Id) que l’objet modifié :
TRestaurants originalData = (TRestaurants)nHibSession.Get(typeof(TRestaurants), modifiedData.Id);
Puis transférer par valeur sur chaque propriété qui nous semble intéressante (cela va dépendre de votre contexte)
originalData.Adresse = modifiedData.Adresse;
ensuite on procède à la mise à jour de l’objet de données :
nHibSession.Update(originalData);
tx.Commit();
On remarque que on ne passe pas à Hibernate l’objet modifiedData, on ne fait juste qu’exploiter les informations qu’il contient.
11 commentaires
Tom
Pour éviter ce pb, il suffit de faire un session.reconnect(). J'avoue ne pas trop avoir compris après le sens de la copie d'objet. Quid des associations? dans l'exemple de l'article tous les types sont primitifs, que faire en cas de types complexes liés, il va me falloir récursivement copier les associations ?
Sami
Exception Details: NHibernate.HibernateException: session already connected
par contre oui, ceci n'est pas fait pour les objets portant des associations. Dans mon cas, il s'agit d'editer avec un DetailView un objet possédant des données scalaires. Il est prudent de s'en tenir là. Et de gérer les associations à part (avec un autre composant graphique par exemple).
Il faudrait aller plus loin, et voir comment se comporte le 2-way data binding avec les éléments associés. A mon avis, ca ne doit pas être superbe et il doit falloir encore du "hand-coding". Ce sera la suite de mon étude.
Ceci dit, merci pour vos commentaires. Sachant que je vous livre ici non pas LA solution, mais une solution possible.
La solution pourrait être de s'appuyer sur un framework de présentation Web 2.0 qui rapatrie la structure de données complexe côté client et en modifie le contenu directement en mémoire avant de la re-transférer sur le serveur pour mise à jour. J'ai mis en oeuvre une petite appli qui ressemble à ça basée sur les EJB 3 et DWR il y a quelques temps, c'était parfait en termes de productivité. Certains frameworks .NET doivent offrir ce genre de possibilités (pas creusé pour le moment), bien plus intéressantes que le binding "bidi" côté serveur en ASP.NET 2.0 classique.
Tom
- Tu veux faire une sorte de Master/Detail avec le même objet
- Or, (et c'est logique) ASP.NET recréé non seulement l'arbre des controls mais les ObjectDataSource associés
- Ta session est du type "Session Longue", du coup le cache de premier niveau est tout le temps actif (et tu n'evict() pas les objets)
Du coup, c'est tout à fait normal d'avoir un tel comportement. Qui plus est, Nhibernate n'en est pas la cause, tu aurais utilisé n'importe quel outil de mapping, le résultat aurait été à mon avis le même. Le mode Web fait que les graphes d'objets sont reconstruits à chaque requête. Et comme une même session Hibernate ne peut posséder deux références différentes pour une même clef, l'outil lève une exception.
Par contre ta solution me semble assez inadaptée. Pourquoi faire un lock et copier des objets, alors qu'un simple lock sur modifiedData aurait suffit ou même mieux SaveOrUpdateCopy() ?
1)
[DataObjectMethod(DataObjectMethodType.Update)]
public bool Modify2(X.DataEntityObjects.TRestaurants modifiedData)
{
nhibsession.lock(modifiedData, LockMode...)
}
2) session.SaveOrUpdateCopy(modifiedData) (il copie l'objet passé en paramètre avec celui attaché en session mais n'attache jamais le paramètre)...
Et puis, si on veut encore une autre solution (celle que je préfère) : Il vaut mieux ne jamais utiliser le mode Long lived Session sur du Web et faire de l'optimistic locking.
Sami
ps: guillaume,pourrais tu mettre un lien sur les sources si c'est possible?
Oui en effet, SaveOrUpdateCopy que j'ai tenté d'utiliser semblait le plus approprié;
mais il ne marche pas dans mon cas car mon objet de données TRestaurant possède des relations avec d'autres objets.
Du coup une exception est levée: object references an unsaved transient instance - save the transient instance before flushing: NomDeLObjetLieParTelation .
Sachant que dans mon detailview, je ne veux que travailler sur les données scalaires, par les données résultantes des objets liés; il y aura d'autres IHM pour ca. Sinon on se tape la liaison à l'infini de toutes les entités dépendantes. Et on a une IHM hyper-contrainte par la couche de persistance et ce n'est pas le but non plus.
Ok, le problème peut venir aussi du schema de mapping et la facon dont sont exploitées les relations. Activer le lazy sur le mode "extra" serait une solution? Debrancher les objets entre eux par le mapping? (Je n'utilise pas le mode cascade).
Il me parait peu sensé de devoir adapter un schema O/R aux nécessités des opérations de domaine. Cela devrait pouvoir se regler autrement.
Mon idée etait de contrôler le databinding donné par défaut, d'une manière acceptable pour lui (le couple NHib/Webforms) faire faire exactement ce que l'on souhaite et qu'il n'en fasse pas trop.
D'où l'idée de se ré-approprier la partie mise à jour de données en faisant soi même le choix des données à faire updater par NHib ( et pas le laisser tout faire tout seul puisque ca se passe mal ).
PS: les captures d'écran, sources et le détail du projet viennent avec l'article en préparation.
Je viens de tomber sur ce problème sur un prototype ObjectDataSource + Castle ActiveRecord + Nhibernate. Très ennuyeux. L'origine en est que pour un Update l'ObjectDataSource appel le constructeur par défaut et ne nous laisse jamais la main pour substituer notre propre constructeur ou appeler une fonction sur une éventuelle Factory.
J'ai cru un moment que l'event OnObjectCreating pourrait nous sauver, mais il ne permet que de spécifier la Facade de l'accès aux objects quand les fonctions d'accès (SelectMethod...) sont non statiques, et non les objets eux-mêmes.
Je ne vois pas de solution satisfaisante mais je n'ai pas tout essayé (les paramètres du style UpdateParameters ?). Je suis preneur de tout workaround...
Mais comme les colonnes de publication dans les blogs doivent rester courtes, je ne vous ai pas exposé l'architecture totale de ma solution. Ceci sera expliqué dans l'article complet sur NHibernate que je prépare.
Pour le moment, je montre directement le code des opérations de la DAL, car elle travaille tout de même, en dessous de toute couche métier, donc il faut bien s'en occuper et c'est à son niveau des exceptions sont levées et empechait ma solution "naive" de fonctionner.