无论关系中是否存在正确的Inverse(流畅的nhibernate)设置,NHibernate都会发出无关的更新语句

时间:2012-06-22 04:38:38

标签: c# nhibernate fluent-nhibernate

以下类以极少的方式表示我的遗留数据库的真实场景。我可以添加新列,但这是我能做的全部,因为300多个表数据库被许多其他遗留应用程序使用,这些应用程序不会移植到NHibernate(因此从复合键迁移不是一个选项) :

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;
}

这就是我将这些映射到“现有”数据库的方式:

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"));
    }
}

这是我用来插入父有三个孩子和一个孩子的代码的代码:

        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();
        }

这些是为上一代码生成的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 [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 */

语句4,5和6是无关/多余的,因为所有信息都已在语句2中的批量插入中发送到数据库。

如果Parent映射未在HasMany(一对多)关系上设置Inverse属性,那么这将是预期的行为。

事实上,当我们摆脱从Child到Item的一对多关系时,它仍然变得陌生:

从Child中删除该集合,并将Child属性添加到Item:

   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;
    }

更改Child和Item的映射以从Item中删除HasMany,并将Item上的复合键上的References添加回Child:

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"));
    }
}

将代码更改为以下内容(请注意,现在我们需要显式调用save Item):

        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();
        }

生成的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,
             PARENT_ID,
             CHILD_ID,
             ItemId)
VALUES      (1 /* @p0_0 */,
             1 /* @p1_0 */,
             1 /* @p2_0 */,
             1 /* @p3_0 */)

正如您所看到的,没有无关/多余的UPDATE语句,但对象模型没有自然建模,因为我不希望Item有一个返回Child的链接,我需要Child中的Items集合。

除了从Child中删除任何HasMany关系之外,我找不到任何方法来阻止那些不需要/不需要的UPDATE语句。看来,由于Child已经是“倒”一对多关系中的“多个”(它本身就是保存),所以当它是另一个的“一个”部分时,它不尊重逆向设置。多对多关系...

这让我疯了。我不能接受那些额外的UPDATE语句而没有任何好的解释:-)是否有人知道这里发生了什么?

1 个答案:

答案 0 :(得分:10)

在彻夜难眠之后,即使在Stack Overflow中也没有希望得到答案:-)我已经想出了解决方案......我开始认为这可能是对孩子的改变了被视为父集合中的更改的对象,然后导致对实体版本的更改。读完之后我的猜测开始凝固了:

  

(13)optimistic-lock(可选 - 默认为true):物种   对集合状态的更改会导致集合的增量   拥有实体的版本。 (对于一对多的协会,它经常是   合理地禁用此设置。)   (见http://nhibernate.info/doc/nh/en/index.html#collections

然后我天真地改变了Parent上的映射,不使用乐观锁,如下所示:

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

这不起作用。但后来我在无关的更新中注意到了一些有趣的事情:

-- 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 */

我很幸运地注意到该版本已更新为2! (题外话:我使用的是DateTime版本字段,但由于它没有无限的精度,当我开始认为它是一个版本问题时,我故意将其改为完整版本,以便我可以看到版本中的每一个增量,并且不要错过在不到几毫秒内发生的增量,由于它的精度或缺乏,它们无法由DateTime版本追踪。所以,在再次绝望之前,我已经将Parent的HasMany改回原来的状态(尝试隔离任何可能的解决方案)并将Not.OptimisticLock()添加到Child的地图中(在所有似乎是将他们的版本更新为儿童!):

  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"));
        }
    }

它完美地发布了以下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 */

没有任何特别的更新声明! : - )

问题是我仍然无法解释为什么以前没有用。出于某种原因,当Child与另一个实体有一对多关系时,将执行无关的SQL语句。您必须在Child对象上的这些一对多集合上将乐观锁设置为false。我不知道为什么所有的Child对象都同时更改了它们的版本,只是因为Child类与添加到它的Item有一对多的关系。当只改变其中一个对象时,增加所有Child对象的版本号是没有意义的!

我最大的问题是,即使我没有向任何Child对象添加任何Item,为什么Parent的集合上的所有Child对象都被更新了。仅发生这样的事实,Child与Item具有HasMany关系...(无需向任何Child添加任何项目以“获取”这些额外更新)。在我看来,NHibernate在这里错误地搞清楚了事情,但由于我完全没有对NHibernate有任何更深入的了解,我无法肯定地说,也没有确切地指出问题的确切位置,甚至没有明确地确认它确实是一个问题。它可能是我完全缺乏NHibernate grokking力量的真正罪魁祸首! : - )

我希望有更开明的人来解释正在/正在发生的事情,但是如文档所建议的那样,在一对多关系中将乐观锁设置为假可以解决问题。