NHibernate 多對多映射的數據更新

最近在用 NHibernate 作多對多更新時忽然發現 NHibernate 更新的策略不好, 對多對多關係的更新竟然是先所有刪除再插入所有數據, 感受很是奇怪, 如今還原以下: html

原來的實體類關係以下: 數據庫

public class User { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual ICollection<Role> Roles { get; set; } public User() {
        Roles = new HashSet<Role>();
    }
} public class Role { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual ICollection<User> Users { get; set; } public Role() {
        Users = new HashSet<User>();
    }

}

即一個用戶能夠有多個角色, 一個角色也能夠有多我的, 典型的多對多關係, 對應的映射代碼以下: session

public class UserMapping : ClassMapping<User> { public UserMapping() {
        Table("[User]");

        Id(m => m.Id, map => { map.Column("[Id]"); map.Type(NHibernateUtil.Int32); map.Generator(Generators.Identity);
        });

        Property(m => m.Name, map => { map.Column("[Name]"); map.Type(NHibernateUtil.String);
        });

        Bag(
            m => m.Roles, map => { map.Table("[User_Role]"); map.Key(k => { k.Column("[UserId]"); });
            },
            rel => {
                rel.ManyToMany(map => { map.Class(typeof(Role)); map.Column("[RoleId]");
                });
            }
        );
    }
} public class RoleMapping : ClassMapping<Role> { public RoleMapping() {
        Table("[Role]");

        Id(m => m.Id, map => { map.Column("[Id]"); map.Type(NHibernateUtil.Int32); map.Generator(Generators.Identity);
        });

        Property(m => m.Name, map => { map.Column("[Name]"); map.Type(NHibernateUtil.String);
        });


        Bag(
            m => m.Users, map => { map.Table("[User_Role]"); map.Key(k => { k.Column("[RoleId]"); }); map.Inverse(true);
            },
            rel => {
                rel.ManyToMany(map => { map.Class(typeof(User)); map.Column("[UserId]");
                });
            }
        );

    }
}

數據庫關係圖以下: app

當向用戶添加或刪除角色是, 發現更新的效率特別低, 代碼以下: 性能

using (var session = sessionFactory.OpenSession()) { var user = session.Query<User>().First(); var firstRole = user.Roles.First();
    user.Roles.Remove(firstRole);
    session.Update(user); var roleCount = session.Query<Role>().Count(); var role = new Role { Name = "Role " + (roleCount + 1) };
    session.Save(role);

    user.Roles.Add(role);
    session.Update(user);

    session.Update(user);
    session.Flush();
}

上面的代碼是將用戶的第一個角色刪除, 再添加一個新的角色, NHibernate 生成的 SQL 語句以下(僅包含對關係表User_Role的操做): 測試

DELETE FROM [User_Role] WHERE [UserId] = @p0;@p0 = 1 [Type: Int32 (0)] INSERT INTO [User_Role] ([UserId], [RoleId]) VALUES (@p0, @p1);@p0 = 1 [Type: Int32 (0)], @p1 = 2 [Type: Int32 (0)] INSERT INTO [User_Role] ([UserId], [RoleId]) VALUES (@p0, @p1);@p0 = 1 [Type: Int32 (0)], @p1 = 7 [Type: Int32 (0)] INSERT INTO [User_Role] ([UserId], [RoleId]) VALUES (@p0, @p1);@p0 = 1 [Type: Int32 (0)], @p1 = 6 [Type: Int32 (0)] INSERT INTO [User_Role] ([UserId], [RoleId]) VALUES (@p0, @p1);@p0 = 1 [Type: Int32 (0)], @p1 = 10 [Type: Int32 (0)]

竟然是先將屬於該用戶的所有角色刪除, 再添加一份新的進來, 徹底沒法接受, 反過來思考以爲確定是本身的問題, 通過一番搜索 (Google), 發現 StackOverflow 上也有人問相似的問題, 而且最終在 NHibernate Tip: Use set for many-to-many associations 發現瞭解決方案, 將多對多的映射的bag改成用set, 問題終於獲得瞭解決, 改事後的映射以下: fetch

Set(
    m => m.Roles, map => { map.Table("[User_Role]"); map.Key(k => { k.Column("[UserId]"); });
    },
    rel => {
        rel.ManyToMany(map => { map.Class(typeof(Role)); map.Column("[RoleId]");
        });
    }
);

將UserMapping和RoleMapping中多對多映射所有改成Set以後, 上面的測試代碼生成的 SQL 以下: ui

DELETE FROM [User_Role] WHERE [UserId] = @p0 AND [RoleId] = @p1;@p0 = 1 [Type: Int32 (0)], @p1 = 8 [Type: Int32 (0)] INSERT INTO [User_Role] ([UserId], [RoleId]) VALUES (@p0, @p1);@p0 = 1 [Type: Int32 (0)], @p1 = 9 [Type: Int32 (0)]

在 NHibernate 參考文檔的 19.5. Understanding Collection performance 中這樣描述: this

Bags are the worst case. Since a bag permits duplicate element values and has no index column, no primary key may be defined. NHibernate has no way of distinguishing between duplicate rows. NHibernate resolves this problem by completely removing (in a single DELETE) and recreating the collection whenever it changes. This might be very inefficient. spa

不僅是多對多, 若是你的集合須要更新, NHibernate 推薦的是:

19.5.2. Lists, maps, idbags and sets are the most efficient collections to update

然而 bags 也不是一無可取:

19.5.3. Bags and lists are the most efficient inverse collections

Just before you ditch bags forever, there is a particular case in which bags (and also lists) are much more performant than sets. For a collection with inverse="true" (the standard bidirectional one-to-many relationship idiom, for example) we can add elements to a bag or list without needing to initialize (fetch) the bag elements! This is because IList.Add() must always succeed for a bag or IList (unlike an ISet). This can make the following common code much faster.

Parent p = sess.Load(id); Child c = new Child(); c.Parent = p;
p.Children.Add(c); //no need to fetch the collection! sess.Flush();

因而可知,bag在多對多映射更新時性能較差, 若是不須要更新,則能夠放心使用, 在須要更新時則set是更好的選擇。

相關文章
相關標籤/搜索