[Linq to SQL] Detacher des entités

J’ai un petit problème, certainement de compréhension, avec Linq to SQL.

Je suis en train de faire une application qui a une liste de produits, que l’utilisateur va pouvoir modifier. Au démarrage de l’application, il a une liste de ces produits dans laquelle il peut faire une recherche et qu’il peut ouvrir en double-cliquant.

Les changements ne sont pas directement sauvés dans la base de données, il doit cliquer sur un bouton sauvegarder pour qu’ils le soient et il a la possibilité de fermer l’application ou la form qui permet de modifier le produit sans sauvegarder à la base de données.

C’est le premier projet où j’utilise L2SQL et en voyant le datacontext et la méthode SubmitChanges() que ce serait un moyen simple de faire ce genre de choses.

J’ai donc créé une application avec différentes couches (chacune de ces couches en un projet distinct):
[ul]
[li]Tout en bas mon “data layer” qui contient le fichier dbml créé à partir de mon schéma de base de données.[/li][li]Au milieu, un “business layer” qui contient l’objet datacontext et qui va offire des méthodes au GUI (par exemple lister les produits)[/li][li]Tout en haut, mon GUI avec un contrôleur de GUI qui contient l’objet datacontext et qui va jouer le chef d’orchestre pour les différentes fenêtres et contrôles[/li][/ul]

Pour la communication entre ce petit monde, j’ai utilisé les objets créés pour chaque tables. Par exemple, pour ma table produits une classe Produit a été générée par le dbml et ma méthode listeProduits de mon business layer renvoie une collection de ces produits. Ca me semblait être la solution la plus simple.

Ensuite, par exemple, des datagrids dans mon UI peuvent se databinder à ces objets.

Tout ça fonctionne plutôt bien, le seul problème que j’ai c’est que quand je charge un produit dans la Form qui permet de modifier un produit, j’ai databindé les propriétés de l’objet Produit à mes contrôles dans la Form. Du coup tous les changements sont renvoyés au datacontext, ce qui ne me pose pas de problème vu qu’ils ne sont pas envoyés à la base de données tant que j’ai pas appelé SubmitChanges().

Le seul soucis c’est que je ne trouve aucun moyen d’annuler les changements qui ont été faits. Lorsque l’utilisateur ferme sans sauvegarder je me suis dit qu’il pourrait simplement annuler les changement, mais on dirait que l’option n’existe pas.

Est-ce que je fais complètement fausse route ou j’ai juste raté un moyen? Qqn a une meilleure idée?

Merci d’avance!

Tout d’abord, quelques petits trucs importants:

  • Exposer le DataContext dans la couche UI, c’est mal, très mal, ca autorise la couche UI à générer du code SQL (ce qui est vraiment super super pas bon).
  • Le DataContext est un objet relativement léger, qui a été designé pour avoir un lifetime relativement court. La solution la plus simple pour ton problème est donc de détruire ton DataContext, et de le recréer à la demande.
  • Enfin, attention aux conventions de nommage !

[quote=“girafologue, post:2, topic: 46760”]Tout d’abord, quelques petits trucs importants:

  • Exposer le DataContext dans la couche UI, c’est mal, très mal, ca autorise la couche UI à générer du code SQL (ce qui est vraiment super super pas bon).[/quote]

Euh, je ne crois pas exposer le datacontext dans ma couche UI, mais soit j’ai mal compris un truc, soit je me suis mal exprimé.
L’architecture de mon application est comme ça :
[ul]
[li]Un projet DAL qui ne contient que le fichier dbml, donc la classe datacontext et les classes qui représentent mes tables.[/li][li]Un projet BAL qui contient une classe que j’ai appelé BusinessClass et qui crée l’objet datacontext et qui offre des méthodes qui seront utilisées par mon UI. Par exemple :[/li] /// <summary> /// Returns the list of the active versions of the product /// </summary> public IQueryable<ProductVersion> ActiveProducts { get { return from p in dispoContext.ProductVersions where p.Status.Name != "Canceled" select p; } }
[li]Un projet GUI qui va contenir une classe UIController qui va contenir un objet du type BusinessClass et qui va gérer le placement des contrôles et des Forms et leur offrir des méthodes pour afficher les informations. Par exemple :[/li]public IQueryable<ProductVersion> ActiveProducts { get { return bal.ActiveProducts; } }
qui va permettre à une liste dans un de mes contrôles d’afficher la liste de tous les produits actifs.
[/ul]
Donc il ne me semble pas que j’expose le datacontext dans la couche UI, l’unique objet datacontext étant bien au chaud dans ma couche BAL…

En fait comme l’applic est constituée principalement d’un côté d’une liste de tous les produits disponibles et d’un autre côté d’une contrôle permettant de modifier un produit en particulier, je me suis dit que je pourrais avoir 2 datacontext. Un en read-only pour la liste des produits et un en lecture écriture que je crée lorsque j’ouvre mon contrôle de modification et que je peux simplement détruire si les modifications ne doivent pas être enregistrées dans la base de données.

Oops, en effet c’est quelque chose sur laquelle je suis assez pointilleux, mais là je ne vois pas bien où il y a un soucis. Est-ce que tu peux être un peu plus précis?

En général, merci infiniment d’avoir pris un peu de temps pour me filer un coup de main, c’est pas encore super clair ces problèmes architecturaux pour moi.

Dans le post de base, tu parles de classe “Produit” => c’est mal, le francais dans du code
et pire tu parles d’une méthode “listeProduits” => en Francais, et sans la majuscule qui va bien.

Autre petit point, fais très attention, quand tu exposes un IQueryable qui vient de Linq to SQL, l’UI peut le transformer et donc modifier le SQL généré. (le SQL est généré qu’au moment ou on itère dessus… et tu peux donc faire des choses comme modifier les filtres, créer des jointures qui font bien mal et tout, dans la couche UI).

une solution est de faire un truc dans ce genre là au niveau de ta BL:

public IEnumerable GetProductsOfCategory(int catID)
{
var q = from p in m_dataContext.GetTable()
where p.CategoryID == catID
select p;
return from p in q.AsEnumerable()
select p;
}

Et juste pour te faire mariner, essaie de trouver l’utilité du “return from p in q.AsEnumerable() select p;” par toi même ^^.

Si l’intérêt à la base était de tester un peu d’architecture “en couche”, j’aurai tendance à dire que là ce n’est pas encore tout à fait ça.
Les trois couches (projet) sont apparemment très fortement couplés, ce qui réduit l’intérêt à pratiquement néant. De plus le “separation of concern” est également pas vraiment respecté.

La business layer n’en est apparemment pas vraiment une, vu qu’elle te sert à requêter, et le requêtage n’est pas le métier, mais le boulot de la couche data.

On peut imaginer dans dans businness une classe, même simple, Order, et côté data on peut imaginer une classe OrderRepository qui expose un GetProductOfCategory retournant une IList.

Data contient donc une ref vers Businness, vers Linq, mais Businness ne contient pas de ref vers ni data ni linq, et est donc ignorant de comment persister tout en enlevant une interdépendance (et l’interdépendance, c’est le mal).

Mmh, je suis de plus en plus perdu en fait.

Si je regarde ce tutorial, l’archi qui y est présentée est bien relativement identique à celle que j’ai. La différence est qu’ici il a un BusinessLayer pour chacun de ses objets dans son application, ce qui en effet est quand même vachement plus clean que mon approche avec une classe “quifaittout”, je vais certainement changer ça.

Dans son exemple il a un DataSet où moi j’ai mon Linq to SQL (le dbml).

Où lui envoie des objets DataTable moi j’envoie les objets définis par le dbml.

J’ai un poil de peine à voir la différence fondamentale.

Ha ça, ca depend où ton produit est utilisé et dans quelle condition :slight_smile:

J’ai finalement trouvé un moyen de résoudre le problème que j’avais.

En fait, pour faire un roll-back des modifications, il suffit simplement de détruire le datacontext et de repartir avec un neuf.

Du coup, j’ai créé deux datacontext, un en lecture seule pour la liste de mes produits et un autre pour lorsque je modifie un produit en particulier, que je détruit à volonté quand je n’en ai plus besoin.

D’après ce que j’ai pu lire à droite à gauche, ces objets datacontext sont très légers et on été prévus pour être recréés à volonter lorsqu’il y en a besoin.

Je sais qu’une solution plus élégante aurait été d’avoir de ne pas utiliser les objets Linq to SQL pour la communication entre les layers, mais malheureusement vu le délai je n’aurais pas le temps de faire la solution la plus élégante qui soit et la solution que j’ai actuellement même loin d’être parfaite fonctionne plutôt bien.

En tout cas merci à tous pour votre aide.

Sinon, Girafologue, j’ai tenté de trouver l’explication du « return from p in q.AsEnumerable() select p; » mais je ne suis pas certain. Pour moi cette commande Linq est du Linq to Object (puisque tu crées un IEnumerable avec AsEnumerable()) donc je me suis dit que ça devait forcer le lancement de la commande SQL.

J’ai bon? Sinon, tu peux m’expliquer?

Merci :slight_smile:

Juste pour dire qu’il y a des webcasts tout chaud sur Linq (et Linq to SQL). C’est par ici.

Alors tu n’es pas loin du compte.
En fait, ca ne force pas l’éxécution de la requète, mais ca l’englobe dans un itérateur, ce qui empèche le code client de récupérer un IQueryable (même en castant l’objet vers IQueryable).

En effet, si tu retournes directement le IQueryable, même si ta méthode déclare retourner un IEnumerable, tu peux toujours faire "var myQueryableOfGrosHackerDeLaMort = myBusiness.GetProducts() as IQueryable();

Et là, dans ton UI, tu peux refaire des trucs à la con, genre des tris de la mort sur des colones non indexées, des produits cartésiens, des trucs du genre quoi ^^.

Avec le petit snippet du dessus, il y’a un surcout, car à chaque fois que tu fetch une ligne, tu travers le code d’un itérateur de plus, mais au bout du compte, ca garantie une meilleur isolation (c’est particulièrement utile quand tu fournis une plateforme de services genre BluePortal pour ne pas le citer) auxquel tu peux aussi accèder “in-process” par simple appels de méthodes (car là, tu n’as absoluement aucun contrôle sur le code client).

Ezeckiel → Exact, d’ailleurs, la partie Visual Linq, c’est moi qui parle :slight_smile:

Bonjour la zone,

Mon projet avance bien, malheureusement je me retrouve devant un nouveau soucis du coup je fais remonter le thread.

Le problème vient d’une relation one-to-one que j’ai dans ma base de donnée.

Les deux tables en question ressemblent à ceci :
Table Parent:
ParentId, int IDENTITY Primary Key
Name, VARCHAR(100) NULLABLE
CreationDate, DATETIME
Table Child:
ChildId, int Primary Key, Foreign Key (references ParentId).
Name, VARCHAR(100) NULLABLE

J’ai ajouté les deux tables à mon fichier dbml et tenté le code suivant :

[code]Parent parent = new Parent() {
CreationDate = DateTime.Now,
Child = new Child()
};

myContext.Parents.InsertOnSubmit(parent);

SubmitChanges();[/code]

Le soucis c’est que je me ramasse une SqlException lorsque je fais le SubmitChanges() car il y aurait un conflit de clé étrangère.

Sur les conseils d’un type sur un forum, j’ai essayé de mettre ChildId en IDENTITY aussi, mais cela ne marche pas non plus car les deux incréments ne sont pas forcément synchronisés.

Est-ce que quelqu’un a une idée?

Je ne connais pas Linq to SQL mais pour moi ton problème est plus un problème de bases de données, n’est-ce pas ?

Et donc ton conflit de clé étrangère vient du fait que tu essayes d’insérer un nouvel élement avant que sa réference ne soit inserées auparavant.

Il faut donc connaître la clé de la table de réference. Ce n’est pas en mettant IDENTITY , qui est un compteur, sur les 2 tables que tu vas la connaitre. Le compteur ne doit être qu’à un seul endroit et tu transmets le code à la procédure d’insertion de la table fille.

[quote=“phili_b, post:13, topic: 46760”]Je ne connais pas Linq to SQL mais pour moi ton problème est plus un problème de bases de données, n’est-ce pas ?
Et donc ton conflit de clé étrangère vient du fait que tu essayes d’insérer un nouvel élement avant que sa réference ne soit inserées auparavant.[/quote]

Mmh oui et non, en fait le problème vient quand même, pour moi, de Linq to SQL. Le truc c’est qu’avec Linq to SQL normalement tu n’as pas à gérer les problèmes d’index et de clés, c’est fait automatiquement. Normalement ce qu’il devrait faire c’est insérer la ligne de la table Parent (et donc recevoir l’index avec le numéro auto-incrémenté) puis insérer la ligne dans la table Child en reprenant le même ID (vu que c’est aussi la clé étrangère).

Le truc c’est qu’il ne semble pas faire ça dans cet ordre et que je me ramasse une exception du coup. L’autre truc c’est que tant que je n’ai pas fait de SubmitChanges() l’id de mon objet Parent n’est pas valide, Linq to SQL le met à jour lors de l’insertion. Donc je ne peux pas en avance récupérer l’ID de la table Parent pour la coller à l’ID de la table Child.

Le truc qui foirouille, c’est que ton entité “Child” n’est pas attachée à ton context… du coup il ne l’insère pas.

Pour faire ca bien il faut faire:
using(var scope = new TransactionScope())
{
Child newChild = new Child();
Parent parent = new Parent() {
CreationDate = DateTime.Now,
Child = newChild
};
myContext.Children.InsertOnSubmit(newChild);
myContext.Parents.InsertOnSubmit(parent);

myContext.SubmitChanges();
scope.Complete(); // love System.Transactions
}

Super!

Merci ça a effectivement marché, et ça me semble logique en effet.

Par contre le truc que je comprends pas, c’est qu’ailleurs je fais :

[code]Product prod = new Product()
{
CreationDate = DateTime.Now,
};

ProductVersion prodVer = new ProductVersion()
{
Product = prod,
CreationDate = DateTime.Now,
LastUsed = DateTime.Now,
Name = name,
Number = 1,
StatusID = 1,
VersionComment = “First Version”
};

myContext.ProductVersions.InsertOnSubmit(prodVer);
myContext.SubmitChanges();[/code]

et que ça marche très bien… par contre ici on a une relation One-to-many entre Product et ProductVersion (un produit a plusieurs versions).

Enfin, du coup pour plus de sécurité j’ai ajouté myContext.Products.InsertOnSubmit(prod); avant l’insertion de prodVer, mais ça reste quand même étrange pour moi.

Ca vient peut etre du type de tes Primary Keys
Si c’est un guid et que Linq to SQL voit l’équivalent de Guid.Empty, il sait qu’il doit faire un insert. Quand c’est un int, la valeur par défaut est tout a fait valable donc il considère par défaut que l’entité existe en base
(c’est une supposition)