2012-06-22 14 views
6

Le seguenti classi rappresentano, in modo assolutamente minimo, il mio scenario reale con un database legacy. Posso aggiungere nuove colonne, ma questo è tutto ciò che posso fare, dal momento che il database di oltre 300 tabelle è utilizzato da molte altre applicazioni legacy che non verranno trasferite su NHibernate (quindi la migrazione dalle chiavi composite non è un'opzione) :NHibernate che emette dichiarazioni di aggiornamento estranee a prescindere dalle corrette impostazioni Inverse (fluente nibernetico) sulle relazioni

public class Parent 
{ 
    public virtual long Id { get; protected set; } 
    ICollection<Child> children = new HashSet<Child>(); 
    public virtual IEnumerable<Child> Children { get { return children; } } 
    public virtual void AddChildren(params Child[] children) 
    { 
     foreach (var child in children) AddChild(child); 
    } 
    public virtual Child AddChild(Child child) 
    { 
     child.Parent = this; 
     children.Add(child); 
     return child; 
    } 
} 
public class Child 
{ 
    public virtual Parent Parent { get; set; } 
    public virtual int ChildId { get; set; } 
    ICollection<Item> items = new HashSet<Item>(); 
    public virtual ICollection<Item> Items { get { return items; } } 
    long version; 
    public override int GetHashCode() 
    { 
     return ChildId.GetHashCode()^(Parent != null ? Parent.Id.GetHashCode() : 0.GetHashCode()); 
    } 
    public override bool Equals(object obj) 
    { 
     var c = obj as Child; 
     if (ReferenceEquals(c, null)) 
      return false; 
     return ChildId == c.ChildId && Parent.Id == c.Parent.Id; 
    } 
} 
public class Item 
{ 
    public virtual long ItemId { get; set; } 
    long version; 
} 

Questo è il modo che ho mappato questi al database "esistente":

public class MapeamentoParent : ClassMap<Parent> 
{ 
    public MapeamentoParent() 
    { 
     Id(_ => _.Id, "PARENT_ID").GeneratedBy.Identity(); 
     HasMany(_ => _.Children) 
      .Inverse() 
      .AsSet() 
      .Cascade.All() 
      .KeyColumn("PARENT_ID"); 
    } 
} 
public class MapeamentoChild : ClassMap<Child> 
{ 
    public MapeamentoChild() 
    { 
     CompositeId() 
      .KeyReference(_ => _.Parent, "PARENT_ID") 
      .KeyProperty(_ => _.ChildId, "CHILD_ID"); 
     HasMany(_ => _.Items) 
      .AsSet() 
      .Cascade.All() 
      .KeyColumns.Add("PARENT_ID") 
      .KeyColumns.Add("CHILD_ID"); 
     Version(Reveal.Member<Child>("version")); 
    } 
} 
public class MapeamentoItem : ClassMap<Item> 
{ 
    public MapeamentoItem() 
    { 
     Id(_ => _.ItemId).GeneratedBy.Assigned(); 
     Version(Reveal.Member<Item>("version")); 
    } 
} 

Questo è il codice che sto usando per inserire un genitore con tre figli e una per bambini con un articolo:

 using (var tx = session.BeginTransaction()) 
     { 
      var parent = new Parent(); 
      var child = new Child() { ChildId = 1, }; 
      parent.AddChildren(
       child, 
       new Child() { ChildId = 2, }, 
       new Child() { ChildId = 3 }); 
      child.Items.Add(new Item() { ItemId = 1 }); 
      session.Save(parent); 
      tx.Commit(); 
     } 

Queste sono le istruzioni SQL generate per il codice precedente:

-- statement #1 
INSERT INTO [Parent] 
DEFAULT VALUES; 

select SCOPE_IDENTITY() 

-- statement #2 
INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_0 */, 
      1 /* @p1_0 */, 
      1 /* @p2_0 */) 

INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_1 */, 
      1 /* @p1_1 */, 
      2 /* @p2_1 */) 

INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_2 */, 
      1 /* @p1_2 */, 
      3 /* @p2_2 */) 


-- statement #3 
INSERT INTO [Item] 
      (version, 
      ItemId) 
VALUES  (1 /* @p0_0 */, 
      1 /* @p1_0 */) 

-- statement #4 
UPDATE [Child] 
SET version = 2 /* @p0 */ 
WHERE PARENT_ID = 1 /* @p1 */ 
     AND CHILD_ID = 1 /* @p2 */ 
     AND version = 1 /* @p3 */ 

-- statement #5 
UPDATE [Child] 
SET version = 2 /* @p0 */ 
WHERE PARENT_ID = 1 /* @p1 */ 
     AND CHILD_ID = 2 /* @p2 */ 
     AND version = 1 /* @p3 */ 

-- statement #6 
UPDATE [Child] 
SET version = 2 /* @p0 */ 
WHERE PARENT_ID = 1 /* @p1 */ 
     AND CHILD_ID = 3 /* @p2 */ 
     AND version = 1 /* @p3 */ 

-- statement #7 
UPDATE [Item] 
SET PARENT_ID = 1 /* @p0_0 */, 
     CHILD_ID = 1 /* @p1_0 */ 
WHERE ItemId = 1 /* @p2_0 */ 

dichiarazioni 4, 5 e 6 sono estranee/superflua dal momento che tutte le informazioni siano state già inviate al database in inserti lotti nella dichiarazione 2.

Questo sarebbe il comportamento previsto se la mappatura padre non avesse impostato la proprietà Inverse sulla relazione HasMany (uno-a-molti).

In realtà, diventa ancora più strano quando ci liberiamo della relazione uno-a-molti da bambino alla voce in questo modo:

rimuovere la collezione da bambino e aggiungere un alloggio Bambino in oggetto:

public class Child 
    { 
     public virtual Parent Parent { get; set; } 
     public virtual int ChildId { get; set; } 
     long version; 
     public override int GetHashCode() 
     { 
      return ChildId.GetHashCode()^(Parent != null ? Parent.Id.GetHashCode() : 0.GetHashCode()); 
     } 
     public override bool Equals(object obj) 
     { 
      var c = obj as Child; 
      if (ReferenceEquals(c, null)) 
       return false; 
      return ChildId == c.ChildId && Parent.Id == c.Parent.Id; 
     } 
    } 

    public class Item 
    { 
     public virtual Child Child { get; set; } 
     public virtual long ItemId { get; set; } 
     long version; 
    } 

cambiare la mappatura del bambino e voce per rimuovere il hasMany dalla voce e aggiungere una Riferimenti sul tasto composito articolo di nuovo bambino:

public class MapeamentoChild : ClassMap<Child> 
{ 
    public MapeamentoChild() 
    { 
     CompositeId() 
      .KeyReference(_ => _.Parent, "PARENT_ID") 
      .KeyProperty(_ => _.ChildId, "CHILD_ID"); 
     Version(Reveal.Member<Child>("version")); 
    } 
} 
public class MapeamentoItem : ClassMap<Item> 
{ 
    public MapeamentoItem() 
    { 
     Id(_ => _.ItemId).GeneratedBy.Assigned(); 
     References(_ => _.Child).Columns("PARENT_ID", "CHILD_ID"); 
     Version(Reveal.Member<Item>("version")); 
    } 
} 

Modificare la c ode al seguente (si noti che ora abbiamo bisogno di chiamare salvare Articolo esplicitamente):

 using (var tx = session.BeginTransaction()) 
     { 
      var parent = new Parent(); 
      var child = new Child() { ChildId = 1, }; 
      parent.AddChildren(
       child, 
       new Child() { ChildId = 2, }, 
       new Child() { ChildId = 3 }); 
      var item = new Item() { ItemId = 1, Child = child }; 
      session.Save(parent); 
      session.Save(item); 
      tx.Commit(); 
     } 

le istruzioni SQL risultanti sono:

-- statement #1 
INSERT INTO [Parent] 
DEFAULT VALUES; 

select SCOPE_IDENTITY() 

-- statement #2 
INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_0 */, 
      1 /* @p1_0 */, 
      1 /* @p2_0 */) 

INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_1 */, 
      1 /* @p1_1 */, 
      2 /* @p2_1 */) 

INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_2 */, 
      1 /* @p1_2 */, 
      3 /* @p2_2 */) 

-- statement #3 
INSERT INTO [Item] 
      (version, 
      PARENT_ID, 
      CHILD_ID, 
      ItemId) 
VALUES  (1 /* @p0_0 */, 
      1 /* @p1_0 */, 
      1 /* @p2_0 */, 
      1 /* @p3_0 */) 

Come potete vedere non ci sono/istruzioni UPDATE superflui estranei, ma il modello a oggetti non è modellato naturalmente in quanto non voglio che Item abbia un link a Child e HO BISOGNO di una raccolta di elementi in Child.

Non riesco a trovare alcun modo per impedire quelle istruzioni UPDATE indesiderate/non necessarie ad eccezione di rimuovere qualsiasi relazione HasMany da Child. Sembra che dal momento che Child sia già "molti" da una relazione "invertita" uno-a-molti (è responsabile del salvataggio di se stesso), non rispetta l'impostazione Inversa quando è la parte "uno" di un'altra -a-molti relazione invertita ...

Questo mi sta facendo impazzire. Non posso accettare quelle dichiarazioni UPDATE extra senza alcun tipo di spiegazione ben ponderata :-) Qualcuno sa cosa sta succedendo qui intorno?

risposta

8

Dopo aver lottato con questo per tutta la notte e nessuna speranza in vista per una risposta anche qui in Stack Overflow :-) Ho trovato la soluzione ... Ho iniziato a pensare che forse era un cambiamento nel Oggetti secondari che venivano considerati come una modifica nella raccolta del genitore e quindi apportati una modifica alla versione dell'entità.La mia ipotesi ha iniziato a solidificarsi dopo aver letto questo:

(13) ottimisti-lock (opzionale - il default è true): Le specie che modifiche allo stato dei risultati di raccolta in incremento del possedere la versione di entità. (Per uno di molte associazioni, è spesso ragionevole disattivare questa impostazione.) (trovate qui: http://nhibernate.info/doc/nh/en/index.html#collections)

Allora ho ingenuamente cambiato la mappatura sul genitore non utilizzare blocco ottimistico come segue:

public MapeamentoParent() 
    { 
     Id(_ => _.Id, "PARENT_ID").GeneratedBy.Identity(); 
     HasMany<Child>(_ => _.Children) 
      .Inverse() 
      .AsSet() 
      .Cascade.All() 
      .Not.OptimisticLock() 
      .KeyColumn("PARENT_ID"); 
    } 

Questo non ha funzionato. Ma poi ho notato qualcosa di interessante notare che negli aggiornamenti estranei:

-- statement #1 
UPDATE [Child] 
SET version = 2 /* @p0 */ 
WHERE PARENT_ID = 1 /* @p1 */ 
     AND CHILD_ID = 1 /* @p2 */ 
     AND version = 1 /* @p3 */ 

-- statement #2 
UPDATE [Child] 
SET version = 2 /* @p0 */ 
WHERE PARENT_ID = 1 /* @p1 */ 
     AND CHILD_ID = 2 /* @p2 */ 
     AND version = 1 /* @p3 */ 

-- statement #3 
UPDATE [Child] 
SET version = 2 /* @p0 */ 
WHERE PARENT_ID = 1 /* @p1 */ 
     AND CHILD_ID = 3 /* @p2 */ 
     AND version = 1 /* @p3 */ 

ho avuto la fortuna di notare che la versione è stata aggiornata per 2! (Digressione: stavo usando un campo di versione DateTime, ma dal momento che non ha una precisione infinita, l'ho intenzionalmente cambiato in una versione integrale quando ho iniziato a pensare che si trattasse di un problema di versione, in modo che potessi vedere ogni singolo incremento nella versione e non perdere gli incrementi che si verificano in meno di millisecondi, che non sono tracciabili dalle versioni DateTime a causa della sua precisione o mancanza). Quindi, prima di disperare ancora una volta, ho cambiato nuovamente l'hasMany del genitore con ciò che era prima (per cercare di isolare ogni possibile soluzione) e ho aggiunto il Not.OptimisticLock() alla mappa del bambino (dopo tutte le entità che sembravano hanno le loro versioni sono stati aggiornati i bambini):!

public class MapeamentoChild : ClassMap<Child> 
    { 
     public MapeamentoChild() 
     { 
      CompositeId() 
       .KeyReference(_ => _.Parent, "PARENT_ID") 
       .KeyProperty(_ => _.ChildId, "CHILD_ID"); 
      HasMany(_ => _.Items) 
       .AsSet() 
       .Cascade.All() 
       .Not.OptimisticLock() 
       .KeyColumns.Add("PARENT_ID") 
       .KeyColumns.Add("CHILD_ID"); 
      Version(Reveal.Member<Child>("version")); 
     } 
    } 

e ha funzionato perfettamente emissione le seguenti istruzioni SQL:

-- statement #1 
INSERT INTO [Parent] 
DEFAULT VALUES; 

select SCOPE_IDENTITY() 

-- statement #2 
INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_0 */, 
      1 /* @p1_0 */, 
      1 /* @p2_0 */) 

INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_1 */, 
      1 /* @p1_1 */, 
      2 /* @p2_1 */) 

INSERT INTO [Child] 
      (version, 
      PARENT_ID, 
      CHILD_ID) 
VALUES  (1 /* @p0_2 */, 
      1 /* @p1_2 */, 
      3 /* @p2_2 */) 

-- statement #3 
INSERT INTO [Item] 
      (version, 
      ItemId) 
VALUES  (1 /* @p0_0 */, 
      1 /* @p1_0 */) 

-- statement #4 
UPDATE [Item] 
SET PARENT_ID = 1 /* @p0_0 */, 
     CHILD_ID = 1 /* @p1_0 */ 
WHERE ItemId = 1 /* @p2_0 */ 

UPDATE NO ESTRANEI A TUTTI !!! :-)

Il problema è che non riesco ancora a spiegare perché non ha funzionato prima. Per qualche motivo, quando Child ha una relazione uno a molti con un'altra entità, vengono eseguite le istruzioni SQL estranee. È necessario impostare il blocco ottimistico su false in queste raccolte one-to-many sull'oggetto Child. Non so perché TUTTI gli oggetti Child abbiano cambiato le loro versioni contemporaneamente, solo perché la classe Child aveva una relazione uno a molti con l'Articolo aggiunto ad essa. Non ha senso aumentare i numeri di versione di tutti gli oggetti Child quando viene modificato solo uno di essi!

Il mio più grande problema con questo è il motivo per cui tutti gli oggetti figlio sulla raccolta del genitore venivano aggiornati anche se non aggiungevo alcun oggetto a nessuno degli oggetti figlio. Stava accadendo solo per il fatto che Child ha una relazione HasMany con Item ... (non c'è bisogno di aggiungere alcun elemento a Child per "ottenere" quegli aggiornamenti extra). Mi sembra che NHibernate stia capendo male le cose qui, ma siccome mi manca completamente una comprensione più profonda di NHibernate non posso dire con certezza, né individuare esattamente dove sia il problema, nemmeno affermare categoricamente che è davvero un problema come potrebbe benissimo essere la mia completa mancanza di potere di NHibernate groking il vero colpevole! :-)

Spero che qualcuno più illuminato venga a spiegare cosa stava succedendo, ma impostare il blocco ottimistico su una relazione uno-a-molti come suggerito dalla documentazione ha risolto il problema.

+0

Ho trovato che c'è una differenza tra parent.AddChild (...) e child.Parent = parent; La versione successiva non _non_ risulta in un aggiornamento di versione estraneo (non aggiunge il figlio alla raccolta Parent.Children durante la sessione, ma quando il genitore viene caricato in una sessione successiva, è lì. – BatteryBackupUnit

+0

@BatteryBackupUnit questo è vero, ma come un ORM non dovrebbe "comportarsi male" in modo diverso quando lo si aggiunge alla raccolta o quando si imposta semplicemente il Parent direttamente. A volte è necessario averlo subito nella raccolta, prima di caricarlo dal database. In quei casi quelle query estranee sarebbero ancora lì. Ma è bello sapere che possiamo "prevenirli" se tutto ciò che vogliamo è "legare" i figli al genitore ... Basta usare child.Parent invece di aggiungere alla collezione del genitore. :-) – Loudenvier

+1

Sono assolutamente d'accordo. Ho aggiunto un FluentNHibernate 'IHasManyConvention' con' .Not.OptimisticLock() 'alle nostre convenzioni per assicurarci che questo sia il comportamento predefinito. – BatteryBackupUnit

Problemi correlati