【问题标题】:EF Eager Loading Includes Duplicate EntitiesEF Eager Loading 包括重复的实体
【发布时间】:2011-04-07 09:31:52
【问题描述】:

我有一个多对多关系中的用户实体和角色实体。它们被注入 Repository 实例,以便能够在 DbContext 被处置后(即在 Repository 层之外)进行延迟加载,如下所示:

public class User
{
    public int UserId { get; set; }
    public string UserName { get; set; }

    // Lazy loaded property
    public ICollection<Role> Roles
    {
        get { return _roles ?? (_roles = Repository.GetRolesByUserId(UserId)); }
        set { _roles = value; }
    }
    private ICollection<Role> _roles;

    public IRepository Repository { private get; set; }
}

public class Role
{
    public int RoleId { get; set; }
    public string Name { get; set; }

    // Lazy loaded property
    public ICollection<User> Users
    {
        get { return _users ?? (_users = Repository.GetUsersByRoleId(RoleId)); }
        set { _users = value; }
    }
    private ICollection<User> _users;

    public IRepository Repository { private get; set; }
}

public class Repository : IRepository
{
    public ICollection<User> GetAllUsers()
    {
        using (var db = CreateContext())
        {
            // Using 'Include' to eager load the Roles collection for each User
            return db.Users.Include(u => u.Roles).ToList();
        }
    }

    public ICollection<Role> GetRolesByUserId(int userId)
    {
        using (var db = CreateContext())
        {
            return db.Roles.Where(r => r.Users.Any(u => u.UserId == userId))
                           .ToList();
        }
    }

    public ICollection<User> GetUsersByRoleId(int roleId)
    {
        using (var db = CreateContext())
        {
            return db.Users.Where(u => u.Roles.Any(r => r.RoleId == roleId))
                           .ToList();
        }
    }

    private CustomContext CreateContext()
    {
        var db = new CustomContext();
        ((IObjectContextAdapter)db).ObjectContext.ObjectMaterialized += OnObjectMaterialized;
        return db;
    }

    private void OnObjectMaterialized(object sender, ObjectMaterializedEventArgs args)
    {
        if (args.Entity is User)
        {
            (args.Entity as User).Repository = this;
        }

        if (args.Entity is Role)
        {
            (args.Entity as Role).Repository = this;
        }
    }
}

public class CustomContext : DbContext
{
    public CustomContext()
        : base()
    {
        Configuration.LazyLoadingEnabled = false;
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }
}

运行以下代码时,对于返回的每个 User 实体,user.Roles 中的每个 Role 实体都有重复的对

IRepository repository = new Repository();
ICollection users = repository.GetAllUsers();
foreach (User user in users)
{
    foreach (Role role in user.Roles)
    {
        ...
    }
}

无论是否启用 EF 延迟加载,以及是否将 User.Roles 属性标记为虚拟,都会出现此问题。

但是,如果我不急于在 Repository.GetAllUsers() 中加载角色,并让延迟加载的角色属性调用 Repository.GetRolesByUserId(UserId),则不会返回重复的角色实体。

public ICollection<User> GetAllUsers()
{
    using (var db = CreateContext())
    {
        // No eager loading
        return db.Users.ToList();
    }
}

如果我将 User.Roles 属性更改为始终命中存储库,则不会返回重复的角色实体。

public ICollection<Role> Roles
{
    get { return (_roles = Repository.GetRolesByUserId(UserId)); }
    set { _roles = value; }
}

看起来调用 db.Users.Include(u =&gt; u.Roles) 会触发 User.Roles 属性的 get() 操作,这会导致 Roles 集合被填充两次。

我已经确认,在枚举 IQueryable 对象时,User.Roles 属性实际上被填充了两次。例如拨打.ToList()时。这意味着,为了解决此行为,无法避免对 Roles 属性的 get() 主体进行更改。这意味着将 EF 特定逻辑放在您的域层中,而不再使其与数据无关。

有没有办法防止这种情况发生?或者有没有更好的方法在 DbContext 被释放后(在 Repository 层之外)实现延迟加载。

【问题讨论】:

    标签: c# entity-framework repository lazy-loading code-first


    【解决方案1】:

    也许这样的事情可以工作:

    public class Repository : IRepository
    {
        public bool RunningEagerLoading { get; set; } // false by default
    
        public ICollection<User> GetAllUsers()
        {
            using (var db = CreateContext())
            {
                try
                {
                    RunningEagerLoading = true;
                    return db.Users.Include(u => u.Roles).ToList();
                    // Materializing (by ToList()) is important here,
                    // deferred loading would not work
                }
                finally
                // to make sure RunningEagerLoading is reset even after exceptions
                {
                    RunningEagerLoading = false;
                }
            }
        }
    
        // ...
    }
    
    public class User
    {
        // ...
    
        public ICollection<Role> Roles
        {
            get
            {
                if (Repository.RunningEagerLoading)
                    return _roles; // Eager loading cares for creating collection
                else
                    return _roles ?? (_roles = Repository.GetRolesByUserId(UserId));
            }
            set { _roles = value; }
        }
        private ICollection<Role> _roles;
    
        public IRepository Repository { private get; set; }
    }
    

    但在我看来,这是一个丑陋的编程技巧。

    【讨论】:

    • 是的,看起来它会起作用。但我想我会围绕 API 进行挖掘,看看是否还有其他方法。希望这确实是一个错误,并在最终的 4.1 版本中得到修复。
    猜你喜欢
    • 2014-02-05
    • 1970-01-01
    • 2013-05-18
    • 2019-04-27
    • 2011-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多