8

以下类以最低限度的方式代表了我使用遗留数据库的真实场景。我可以向它添加新列,但这就是我所能做的,因为 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 已经是“倒置”一对多关系中的“多”(它负责保存自己),因此当它是另一个“一个”部分时,它不尊重 Inverse 设置对多倒置关系...

这让我发疯了。如果没有任何深思熟虑的解释,我不能接受那些额外的 UPDATE 语句 :-) 有人知道这里发生了什么吗?

4

1 回答 1

11

经过一夜的努力,即使在堆栈溢出中也没有希望得到答案 :-) 我想出了解决方案......我开始认为这可能是 Child 对象的变化被认为是父集合的更改,然后导致实体版本的更改。读完这篇文章后,我的猜测开始巩固:

(13) 乐观锁(可选 - 默认为 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 与 Item... 有 HasMany 关系这一事实就发生了(无需向任何 Child 添加任何项目以“获取”那些额外的更新)。在我看来,NHibernate 在这里错误地解决了问题,但由于我对 NHibernate 完全缺乏更深入的了解,我不能肯定地说,也不能准确指出问题出在哪里,甚至不能断然肯定它确实是一个问题这很可能是我完全缺乏 NHibernate 探索能力的真正罪魁祸首!:-)

我希望有更开明的人来解释发生了什么/正在发生什么,但是按照文档的建议在一对多关系上将乐观锁设置为 false 解决了这个问题。

于 2012-06-22T15:34:41.873 回答