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

これは、アイテムを持つ 3 つの子と 1 つの子を持つ親を挿入するために使用しているコードです。

        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 (1 対多) 関係で Inverse プロパティを設定していない場合、これは予想される動作です。

実際、次のように Child から Item への 1 対多の関係を取り除くと、さらに奇妙になります。

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 はすでに「逆」の 1 対多の関係からの「多」であるため (それ自体を保存する責任があります)、別の「1」の部分である場合、逆の設定を尊重しません。 -対多逆関係...

これは私を夢中にさせています。よく考えられた説明がなければ、これらの追加の UPDATE ステートメントを受け入れることはできません :-) この辺りで何が起こっているか知っている人はいますか?

4

1 に答える 1

11

一晩中これに苦労し、ここスタックオーバーフローでも答えが見えなかった後:-)私は解決策を思いつきました...おそらくそれは子オブジェクトの変更だったと思い始めました親のコレクションの変更と見なされ、エンティティのバージョンが変更されました。これを読んだ後、私の推測は固まり始めました:

(13) optimistic-lock (オプション - デフォルトは true): コレクションの状態に変化する種は、所有エンティティのバージョンのインクリメントになります。(1 対多の関連付けの場合、多くの場合、この設定を無効にするのが合理的です。) (ここにあります: http://nhibernate.info/doc/nh/en/index.html#collections )

次に、親のマッピングを単純に変更して、楽観的ロックを使用しないようにしました。

    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 バージョンでは精度が高いか不足しているために追跡できません)。したがって、もう一度絶望する前に、親の HasMany を以前の状態に戻し (考えられる解決策を特定するために)、代わりに Not.OptimisticLock() を子のマップに追加しました (バージョンが更新されたのは子供でした!):

  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 が別のエンティティと 1 対多の関係を持つ場合、不要な SQL ステートメントが実行されます。Child オブジェクトのこれらの 1 対多のコレクションでは、楽観的ロックを false に設定する必要があります。Child クラスに Item との一対多の関係が追加されたという理由だけで、すべての Child オブジェクトのバージョンが同時に変更された理由もわかりません。Child オブジェクトの 1 つだけが変更された場合に、すべての Child オブジェクトのバージョン番号を増やしても意味がありません。

これに関する私の最大の問題は、子オブジェクトのいずれにも項目を追加していなくても、親のコレクションのすべての子オブジェクトが更新されていた理由です。Child が Item と HasMany 関係を持っているという事実だけで起こっていました... (これらの追加の更新を「取得」するために、任意の Child に項目を追加する必要はありません)。NHibernate はここで物事を間違って理解しているように思えますが、NHibernate の深い理解が完全に欠けているため、確実に言うことも、問題がどこにあるかを正確に特定することも、それが実際に問題であることを断言することさえできませんそれは、本当の犯人である NHibernate の処理能力が完全に欠如している可能性があります。:-)

何が起こっているのか、何が起こっているのかをより賢明な人が説明してくれることを願っていますが、ドキュメントで示唆されているように、一対多の関係で楽観的ロックを false に設定すると問題が解決しました。

于 2012-06-22T15:34:41.873 に答える