EF Eager Loading包括重复实体

时间:2011-04-07 09:31:52

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

我在多对多关系中有一个用户实体和角色实体。它们被注入了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.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()中加载Roles,并让延迟加载的Roles属性调用Repository.GetRolesByUserId(UserId),则不会返回重复的Role实体。

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

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

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

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

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

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

1 个答案:

答案 0 :(得分: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; }
}

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