Un bug in LINQ

Am dat de un mic bug in LINQ to SQL, ce m-a facut sa-mi regandesc o abordare dintr-un anumit proiect. Eroarea apare in interiorul DataContext si anume la nivelul asocierii dintre clasele mapate pe tabele in scopul de a evidentia relatia 1:n.

Sa explic insa cu un exemplu.

Se da o baza de date cu trei tabele, o tabela cu categorii, o tabela cu produse si o tabela History, care contine istoria operatiilor pe celelalte doua. Pentru o anumita inregistrare din Products sau Categories vor exista una sau mai multe intrari(sau niciuna) in tabela History. Stiind un anumit HistoryGUID din Products sau Categories se pot afla inregistrarile din History, adica istoria operatiilor facute pentru o anumita inregistrare din oricare tabele. Cum HistoryGUID este generat automat de SQL Server cu newid(), la inserarea unei noi inregistrari intr-o tabela din cele doua, atunci e asigurata unicitatea lui la nivel de baza de date, conditie necesara pentru a identifica corect cumulul de inregistrari in History asociate lui. Natural este sa se construiasca anumite constrangeri la nivel de logica a structurii. Fiecare HistoryGUID din Categories sau Products este unic, deci se pot adauga chei unice la nivel de tabele formate din acest HistoryGUID. Am preferat sa las cheia primara un int identity(ID), pentru ca aceasta parte cu istoria este doar o particularitate a bazei de date. Pornind de la aceste chei unice se pot adauga ,la fel de natural, chei straine intre tabela Categories si tabela History si intre tabela Products si tabela History.



Translatand in model OOP, se obtin afirmatiile:



- o categorie contine o lista de istorii
- un produs contine o lista de istorii
- o istorie are un produs
- o istorie are o categorie.
Aceste relatii sunt generate si de LINQ to DBML

Ultimile doua afirmatii sunt contradictorii, adica o inregistrare din History nu poate avea corespondent atat in Categories cat si in Products. Ori se inregistreaza o actiune pentru o anumita categorie, ori se inregistreaza o anumita actiune pentru un anumit produs. Lucru sanctionat si de SQL server, prin faptul ca nu esti lasat sa adaugi o inregistrare in History, decat daca exista acel uniqueidentifier atat in Categories cat si in Products la nivelul coloanei HistoryGUID. Daca ar fi acelasi GUID atat in Categories cat si in Products, atunci nu s-ar mai putea identifica cumulul inregistrarilor in History pentru acel GUID, ceea ce contrazice ideea de a pastra istoria operatiilor pentru o anumita inregistrare dintr-o anumita tabela. Asadar aceste chei straine nu ajuta si nu vor mai exista. Insa la nivel de ORM, stergerea lor inseamna si eliminarea primelor doua afirmatii (cel putin la generarea automata de Visual Studio 2008 DataContext-ului). Pentru a pastra insa aceasta modelare, le adaug manual. Observ ca si de data aceasta nu am scapat de problema faptului ca un obiect de tip History are atat un produs cat si o categorie. Sa zicem DBMLu se afla intr-o librarie separata de cea in care va fi folosit si modific accesorul proprietilor Product si Category din public in internal.
Acum, toate bune si frumoase pana a venit timpul si sa folosesc acest model. Pentru un anumit Product vreau sa adaug un History.



private static void InsertProductHistory(int productID)
{
LinqBugDataContext dc = new LinqBugDataContext();
Product product = dc.Products.FirstOrDefault(p => p.ID == productID);
History newHistory = new History();
product.Histories.Add(newHistory);
dc.SubmitChanges();
}

Si obtin urmatorul mesaj de eroare



Unhandled Exception: System.InvalidCastException: Specified cast is not valid.
at System.Data.Linq.IdentityManager.StandardIdentityManager.SingleKeyManager`
2.TryCreateKeyFromValues(Object[] values, V& v)
at System.Data.Linq.IdentityManager.StandardIdentityManager.IdentityCache`2.F
ind(Object[] keyValues)
at System.Data.Linq.IdentityManager.StandardIdentityManager.Find(MetaType typ
e, Object[] keyValues)
at System.Data.Linq.CommonDataServices.GetCachedObject(MetaType type, Object[
] keyValues)
at System.Data.Linq.ChangeProcessor.GetOtherItem(MetaAssociation assoc, Objec
t instance)
at System.Data.Linq.ChangeProcessor.BuildEdgeMaps()
at System.Data.Linq.ChangeProcessor.SubmitChanges(ConflictMode failureMode)
at System.Data.Linq.DataContext.SubmitChanges(ConflictMode failureMode)
at System.Data.Linq.DataContext.SubmitChanges()
at LinqBug.Program.InsertProductHistory(Int32 productID) in Z:\blogger\LinqBu
g\LinqBug\Program.cs:line 32
at LinqBug.Program.Main(String[] args)

Apare la:

internal override bool TryCreateKeyFromValues(object[] values, out V v)
{
object obj2 = values[this.offset];
if ((obj2 == null) && !this.isKeyNullAssignable)
{
v = default(V);
return false;
}
v = (V) obj2;
return true;
}

Asa ca presupun ca atunci cand LINQ isi formeaza cele necesare cand isi creaza tipurile, relatiile si toate cele, a cam dat de pamant in acest scenariu. Nu stiu cum as putea rezolva asta, dar important e ca stiu ca nu se poate si pe viitor evit ideea pana MS revine cu o rezolvare.

Comments

  1. A.
    Mi se pare mie sau ai cam dat-o in bara cu relatiile din diagrama SQL Server. E oarecum invers ca in schema DBML.

    B.
    Un GUID nu e neaparat sa fie creat prin newid() din Transact-SQL. Il poti crea tu din .NET (C#) si-l poti trimite la SQL Server; whatever.GUID = Guid.NewGuid();

    Fa un refactory la articol!

    10x

    ReplyDelete
  2. A.

    Nu sunt invers

    Coloana HistoryGUID, in fiecare tabela, e Unique, deci coloana parinte, care contine cheia

    Tabela History are si ea o coloana GUID, care contine valori fie din tabela Categories, fie din tabela Products

    E ca si cum tabela Products ar avea o coloana CategoryID care va contine valori existente in coloana ID (cheie primara, valori unice pe tabela)

    B.

    yep

    dar daca as avea un client al bazei de date scris in .. nush.. php

    exista o logica la nivel de baza de date, exista o alta logica la nivel de clienti..

    Ideea e ca numai cu modelul asta am obtinut aceasta sitatutie

    ReplyDelete

Post a Comment

Popular posts from this blog

IIS 7.5, HTTPS Bindings and ERR_CONNECTION_RESET

Verify ILogger calls with Moq.ILogger

Table Per Hierarchy Inheritance with Column Discriminator and Associations used in Derived Entity Types