这个问题其实困扰很久了,前两天看到论坛上有人在问权限管理系统,就想起来我做权限管理时遇到的这么个问题。有以下实体:/// <summary>
/// 用户类
/// </summary>
public class User
{
private IList<Role> roles; /// <summary>
/// 用户所属的角色集合
/// </summary>
public IList<Role> Roles
{
get
{
return roles;
}
set 
{
roles = value;
}
}
}/// <summary>
/// 角色类
/// </summary>
public class Role
{
        private IList<Right> rights;
        private IList<User> users;        /// <summary>
        /// 该角色拥有的用户集合
        /// </summary>
        public IList<User> Users
        {
            get 
            {
                return users; 
            }
            set { users = value; }
        }
        /// <summary>
        /// 该角色拥有的权限集合
        /// </summary>
        public IList<Right> Rights
        {
            get 
            {
             
                return rights; 
            }
            set { rights = value; }
        }
}/// <summary>
/// 权限类
/// </summary>
public class Right
{
        private IList<Role> roles;        /// <summary>
        /// 拥有该权限的角色集合
        /// </summary>
        public IList<Role> Roles
        {
            get 
            {
              
                return roles; 
            }
            set { roles = value; }
        }
}
这样其实
User、Right、Role就是两两Many-Many的关系
访问时
可以通过这样
User.Roles[0].Rights的方式访问用户的第一个角色拥有的所有权限
也可以通过
foreach(Role role in user.Roles)
{
foreach(Right right in role.Rights)
{
yield return right;
}
}
这样的方式获取用户拥有的所有权限集合。
但是,问题来了
user.Roles[0].Rights[0].Roles[0].Users[0].....................
这样的结构也是允许的,就会导致数据访问层做ORM映射的时候,出现循环调用堆栈溢出的情况
获取user的Roles集合时,要实例化每一个Role,而实例化每一个Role的时候,要获取其Users集合,实例化其中每一个User,再获取User的Roles集合......在例如NHibernate等ORM框架中,延迟加载就没有这种问题
也就是说user的Roles集合并没有一开始就在在数据访问层获取,而是当业务逻辑层或是表示层有需要读取的时候,才去获取Roles集合
这个读取的机制应该是要做在实体层的最开始的时候,曾设想这样的解决方案
/// <summary>
/// 用户类
/// </summary>
public class User
{
private IList<Role> roles; /// <summary>
/// 用户所属的角色集合
/// </summary>
public IList<Role> Roles
{
get
{
if (roles==null)
//调用数据访问层获取Roles集合
return roles;
}
set 
{
roles = value;
}
}
}
但是这样做明显会出现数据访问层引用实体层,而实体层引用数据访问层这样的循环引用接着试图使用事件机制来通知数据访问层public IList<Role> Roles
{
get
{
if (roles == null)
                {
                    if (BeforeReadNullRoles != null)
                    {
                        BeforeReadNullRoles(this, EventArgs.Empty);
                    }
                }
                return roles;
            }
            set 
            {
                roles = value;
            }
        }
        /// <summary>
        /// 用于在获取Roles集合前,如果Roles集合为空则触发的事件
        /// </summary>
        public event EventHandler BeforeReadNullRoles;
}
数据访问层采用类似这样的方式来监听事件
if (reader.Read())
{
User user = new User();
user.BeforeReadNullRoles+=new EventHandler(user_BeforeReadNullRoles);
reader.Close();
}void user_BeforeReadNullRoles(object sender,EventArgs e)
{
User user=sender as User;
user.Roles=new RoleService().GetRolesByUserId(user.Id);//调用数据访问层方法获取用户的角色集合
}
好像能够完成目标了,但是:
当在Asp.Net页面绑定User或IList<User>数据之后,第一次加载页面是可以正常获取user的外键对象的,但是,一旦页面回传后,利用ObjectDataSource绑定的user对象就没有办法获取Roles等外键对象了,跟踪后发现user对象的事件是null,事件没了我想知道,这类问题该如何处理,使用事件监听机制正确吗?如何解决事件丢失的情况?
NHibernate等ORM是如何实现延迟加载的?
如果没有实现延迟加载如何避免user.Roles[0].User[0].Roles这类循环调用导致堆栈溢出的情况?
如果有可能,请尽量多给些示例代码,也请有过类似实现经验的多给意见。

解决方案 »

  1.   

    问题很长,请耐心慢慢看题外话:
    我最终是回避了延迟加载的,多加了个标记来判断是否获取外键来绕过循环调用的问题的,大概的实现如下:
    public User GetUserByUserId(int userId)
    {
    ......
    User user=new User();
    user.Roles=new RoleService().GetRolesByUserId(user.Id,true);
    }public IList<Role> GetRolesByUserId(int userId,bool noForeignKey)
    {
    foreach(....)
    {
    Role role=new Role();
    if (!noForeignKey)//如果需要获取外键
    {
    role.Users=new UserService.GetUsersByRoleId(role.Id,true);
    role.Rights=new RightService.GetRightsByRoleId(role.Id,true);
    }
    }
    }public IList<User> GetUsersByRoleId(int roleId,bool noForeignKey)
    {
    foreach(....)
    {
    User user=new User();
    if (!noForeignKey)//如果需要获取外键
    {
    user.Roles=new RoleService().GetRolesByUserId(user.Id,true);
    }
    }
    }
    这样的笨办法,避免了循环调用,但外键就只能获取user.Roles这样第一层外键了,一般情况下,足够使用了
      

  2.   

    public IList<Role> Roles
        {
            get
            {
                if (roles==null)
                    //调用数据访问层获取Roles集合
                return roles;
            }
            set 
            {
                roles = value;
            }
        }但是这样做明显会出现数据访问层引用实体层,而实体层引用数据访问层这样的循环引用
    ------------------------------------------------------------------------
    实体层引用数据访问层 是否会造成循环引用 这个不一定,要看具体代码。按照正常的需求,是不会造成循环引用的。
    跟踪后发现user对象的事件是null,事件没了
    --------------------------------------------------
    EventHandler 应该是可序列化的,目前不知道没了的原因。
    ps:最好提供测试的DEMO,这样有助于更好的解决问题。
      

  3.   


    数据访问层项目要生成实体,自然要添加实体层项目的引用,而如果实体层如果要直接调用数据访问层的方法,自然会造成循环引用Demo我得整理一个看看
      

  4.   

    你指的是dll的循环引用? 为何domain object 要放在单独的项目中呢?
      

  5.   

    习惯上都是这样啊,好像PetShop也是这样干的
      

  6.   

    奇怪的是,重现不了当时的事件丢失的bug了。。怪
    Demo的地址 http://download.csdn.net/source/2506390NHibernate等ORM是如何实现延迟加载的?
    使用事件监听机制正确吗?
      

  7.   


    具体问题具体分析,没有通用的。但不是说放在单独项目中不可以,你要是放在单独项目中,不使用项目引用就不会出现循环引用了。用引用DLL的方式。更多的做法是借助第三方项目来进行引用。
      

  8.   


    现在好用不丢失了是么? 
    -------------------------Q:NHibernate等ORM是如何实现延迟加载的?A:应该是类似public IList<Role> Roles
        {
            get
            {
                if (roles==null)
                    //调用数据访问层获取Roles集合
                return roles;
            }
            set 
            {
                roles = value;
            }
        }
    这样的方式。----------------------------------------------------
    Q:使用事件监听机制正确吗?A:可以啊,其实这样更灵活。只是之前你说有事件丢失的情况
      

  9.   

    实际上这样的,如果把实体层和数据访问层放在一块,那么意味着表示层引用实体对象的时候,同时也会引用到数据访问层,耦合度提高了,而且也不符合规范,单为了不出现循环引用,代价太高了,如果所有都放在表示层项目中,那么什么问题都没有了,不妥。事件消息通知机制实现延迟加载是我唯一能想到的,Demo放上去了,阿非能有空帮我看看可能会有缺陷否?
      

  10.   

    除了事件这个方式,也可借助接口来实现。DEMO 我下载了,运行了几次 没想到现在的事件方式有什么不妥。只是过多的应用了重量级服务器控件,也可能实际中你不会这样,只是便于演示。
      

  11.   

    你指的是gridview和detailview加上objectDatasource么?
    后台开发的时候是常用的,毕竟快。
    如果是网站前台,div布局时,需要前端优化就不可能这样用了,viewstate是个大问题。或者你指的是EnterPrise企业库?请指教
      

  12.   

    哦,我是指gridview和detailview。objectDatasource 我用的不多,它应该是应用了反射,故应该有比它更好的方案。其实许多时候没有绝对的正确,取舍是问题,就向在应用viewstate 造成带宽浪费和不应用viewstate 进行数据库查询 或 缓存查询。你了解应用环境和相关需要就可以了。
      

  13.   

    麻烦你看看那个例子,删除的时候,外键集合和延迟加载的事件都是null了
      

  14.   

    新的例子在这ORMDemo.rar
      

  15.   

    我看到错误了。如果你仍然使用 ObjectDataSource 的话,那做如下修改。
    public static bool DeleteUser(User user)
            {
                if (user.Roles == null)
                {
                    user = GetUserByUserId(user.UserId);    
                }            if (user.Roles.Count > 0)
                    return true;
                else
                    return false;
            }
    ObjectDataSource 只指定了删除的方法,没有指定参数。而删除的参数则是由 GridView 提供的因为在触发 GridView 的相关事件时 (GridView_RowDeleting),只能获取到GridView设置的DataKeyNames属性 所对应的当前删除项的值 ,DataKeyNames属性设置的是UserID ,因为删除方法需要的是 User 这个类的实例,所以会实例化一个 User 对象,并将 当前删除项的DataKeyNames所对应的值 赋值给 UserID然后才执行 UserService.cs 中的 DeleteUser 方法