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?
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
@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
Sono assolutamente d'accordo. Ho aggiunto un FluentNHibernate 'IHasManyConvention' con' .Not.OptimisticLock() 'alle nostre convenzioni per assicurarci che questo sia il comportamento predefinito. – BatteryBackupUnit