EF 延迟加载

Posted

技术标签:

【中文标题】EF 延迟加载【英文标题】:EF Lazy Loading 【发布时间】:2014-01-08 11:42:08 【问题描述】:

我对延迟加载有点困惑。 我的印象是它在访问导航属性时会加载它们,但在我的代码中,它似乎试图将其全部拉入。 这可能是由于我的服务/存储库模式,但目前我得到循环引用:(

当我这样调用我的服务时:

using (var service = new UserService(new CompanyService()))

    var u = await service.GetAll();                  

    return new JsonResult  Data = new  success = true, users = u  ; // Return our users

它正在引入一个用户

列表
public partial class User : IdentityUser

    public User()
    
        // ...
        this.MemberOf = new List<Group>();
        // ...
    

    // ...

    // ...
    public virtual ICollection<Group> MemberOf  get; set; 
    // ...

这似乎带来了 Group

的列表
public partial class Group

    public Group()
    
        // ...
    

    public int Id  get; set; 
    public string Name  get; set; 
    public string Description  get; set; 
    // ...

    // ...
    public virtual Company Company  get; set; 
    // ...

然后引入一个公司

public partial class Company

    public Company()
    
        this.Assets = new List<Asset>();
        // ..
    

    public string Id  get; set; 
    public string Name  get; set; 
    // ..
            
    // ...
    public virtual ICollection<Asset> Assets  get; set; 
    // ...

这会带来资产

的列表
public partial class Asset

    public Asset()
    
        // ...
        this.Categories = new List<Category>();
        // ...
    

    public int Id  get; set; 
    public string FileName  get; set; 
    public string ThumbNail  get; set; 
    // ...

    // ...
    public virtual ICollection<Category> Categories  get; set; 
    // ...

它引入了一个 Category 列表,这是循环引用发生的地方,因为它引入了一个 Asset 列表,它引入了一个 列表>类别等等。

我认为使用延迟加载只会引入用户及其导航属性,就是这样,除非我另有说明?

我尝试只使用这种方法而不是我的服务(只是为了测试);

var u = new SkipstoneContext().Users;

return new JsonResult  Data = new  success = true, users = u  ; // Return our users

但我仍然得到循环引用。

有人可以向我解释为什么它会尝试加载所有导航属性,以及我是否可以(轻松)做些什么来阻止它?

更新 2

Keith 建议使用接口和 ContractResolver 来帮助进行序列化,所以这就是我所做的。

首先,我创建了 2 个新接口:

public interface IBaseUser

    string Id  get; set; 
    string UserName  get; set; 
    string Email  get; set; 
    bool IsApproved  get; set; 
    bool IsLockedOut  get; set; 

    ICollection<Group> MemberOf  get; set; 

public interface IBaseGroup

    int Id  get; set; 
    string Name  get; set; 

模型实现了这些类

public partial class User : IdentityUser, IBaseUser

public partial class Group : IBaseGroup

这是第一步,第二步是创建 ContractResolver 类,如下所示:

public class InterfaceContractResolver : DefaultContractResolver

    private readonly Type interfaceType;

    public InterfaceContractResolver(Type interfaceType)
    
        this.interfaceType = interfaceType;
    

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    
        var properties = base.CreateProperties(this.interfaceType, memberSerialization);

        return properties;
    

我获取用户的方法现在如下所示:

public async Task<JsonNetResult> Get()

    try
    
        using (var service = new UserService(new CompanyService()))
        
            var u = await service.GetAll();

            var serializedObject = JsonConvert.SerializeObject(u,
                new JsonSerializerSettings()
                
                    ContractResolver = new InterfaceContractResolver(typeof(IBaseUser))
                );

            return new JsonNetResult  Data = new  success = true, users = serializedObject  ; // Return our users
        
    
    catch (Exception ex)
    
        return new JsonNetResult  Data = new  success = false, error = ex.Message  ;
    

所以,当这段代码运行时,我得到一个错误:

无法将类型为“System.Data.Entity.DynamicProxies.Group_D0E52FCCF207A8F550FE47938CA59DEC7F963E8080A64F04D2D4E5BF1D61BA0B”的对象转换为类型“Skipstone.Web.Identity.IBaseUser”。

这是有道理的,因为 InterfaceContractResolver 只需要接口类型。

问题是我该如何解决这个问题?

更新 3

好吧,现在这有点傻了。

所以我通过这样做禁用了延迟加载:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    
        base.Configuration.LazyLoadingEnabled = false; // Disable Lazy Loading

        // ...
    

然后我创建了这个存储库:

public abstract class Repository<TEntity> : IDisposable, IRepository<TEntity> where TEntity : class

    public DbContext Context  get; private set; 
    public IQueryable<TEntity> EntitySet  get  return this.DbEntitySet;  
    public DbSet<TEntity> DbEntitySet  get; private set; 

    public Repository(DbContext context)
    
        if (context == null)
            throw new ArgumentNullException("context");

        this.Context = context;
        this.DbEntitySet = context.Set<TEntity>();
    

    public Task<TEntity> GetAsync(object id)
    
        return this.DbEntitySet.FindAsync(new object[]
        
            id
        );
    

    public IEnumerable<TEntity> GetAll()
    
        return this.DbEntitySet;
    

    public IEnumerable<TEntity> GetAll(string include)
    
        return this.DbEntitySet.Include(include);
    

    public IEnumerable<TEntity> GetAll(string[] includes)
    
        foreach (var include in includes)
            this.DbEntitySet.Include(include);

        //var t = this.DbEntitySet.Include("Settings").ToList();

        // return this.GetAll();
        return this.DbEntitySet;
    

    public void Add(TEntity model)
    
        this.DbEntitySet.Add(model);
    

    public void Remove(TEntity model)
    
        this.Context.Entry<TEntity>(model).State = EntityState.Deleted;
    

    public void Dispose()
    
        this.Context.Dispose();
    

我的 UserRepository 继承的:

public class UserRepository : Repository<User>

    public UserRepository(DbContext context)
        : base(context)
    
    

那么我的服务有这个方法:

    public IList<User> GetAll(string include)
    
        return this.repository.GetAll(include).Where(model => model.CompanyId.Equals(this.companyId, StringComparison.OrdinalIgnoreCase)).ToList();
    

现在我的 JsonResult 方法如下所示:

    // 
    // AJAX: /Users/Get

    public JsonResult Get()
    
        try
        
            using (var service = new UserService(new CompanyService()))
            
                var u = service.GetAll("MemberOf");                  

                return new JsonResult  Data = new  success = true, users = u  ; // Return our users
            
        
        catch (Exception ex)
        
            return new JsonResult  Data = new  success = false, error = ex.Message  ;
        
    

你猜怎么着?!?!? 如果我在返回之前放置一个断点,我会看到 u 的属性全部填充,并且导航属性都没有设置,除了 MemberOf 这正是我想要的(对于现在)但是当我跨过返回时,我得到了和以前一样的错误!!!

Newtonsoft.Json.dll 中出现“Newtonsoft.Json.JsonSerializationException”类型的异常,但未在用户代码中处理

附加信息:检测到类型为“System.Data.Entity.DynamicProxies.Asset_AED71699FAD007BF6F823A5F022DB9888F62EBBD9E422BBB11D7A191CD784288”的自引用循环。路径'users[0].Company.Assets[0].Categories[0].Assets'。

如何/为什么会这样?

更新 4

这似乎是因为属性仍被标记为虚拟。当我删除 virtual 时,我不再收到 Assets 的错误,而是现在收到 CreatedBy 属性的错误。

这是我的用户类:

public partial class User : IdentityUser

    public string CompanyId  get; set; 
    public string CreatedById  get; set; 
    public string ModifiedById  get; set; 
    public System.DateTime DateCreated  get; set; 
    public Nullable<System.DateTime> DateModified  get; set; 
    public System.DateTime LastLoginDate  get; set; 
    public string Title  get; set; 
    public string Forename  get; set; 
    public string Surname  get; set; 
    public string Email  get; set; 
    public string JobTitle  get; set; 
    public string Telephone  get; set; 
    public string Mobile  get; set; 
    public string Photo  get; set; 
    public string LinkedIn  get; set; 
    public string Twitter  get; set; 
    public string Facebook  get; set; 
    public string Google  get; set; 
    public string Bio  get; set; 
    public string CompanyName  get; set; 
    public string CredentialId  get; set; 
    public bool IsLockedOut  get; set; 
    public bool IsApproved  get; set; 
    public bool CanEditOwn  get; set; 
    public bool CanEdit  get; set; 
    public bool CanDownload  get; set; 
    public bool RequiresApproval  get; set; 
    public bool CanApprove  get; set; 
    public bool CanSync  get; set; 
    public bool AgreedTerms  get; set; 
    public bool Deleted  get; set; 

    public Company Company  get; set; 
    public User CreatedBy  get; set; 
    public User ModifiedBy  get; set; 
    public ICollection<Asset> Assets  get; set; 
    public ICollection<Category> Categories  get; set; 
    public ICollection<Collection> Collections  get; set; 
    public ICollection<Comment> Comments  get; set; 
    public ICollection<LocalIntegration> LocalIntegrations  get; set; 
    public ICollection<Page> Pages  get; set; 
    public ICollection<Rating> Ratings  get; set; 
    public ICollection<Theme> Themes  get; set; 
    public ICollection<Group> MemberOf  get; set; 
    public ICollection<Category> ForbiddenCategories  get; set; 
    public ICollection<Page> ForbiddenPages  get; set; 

这是错误:

Newtonsoft.Json.dll 中出现“Newtonsoft.Json.JsonSerializationException”类型的异常,但未在用户代码中处理

附加信息:检测到类型为“System.Data.Entity.DynamicProxies.User_E9B58CAAA82234358C2DE2AF8788D33803C4440F800EA8E015BE49C58B010EDF”的属性“CreatedBy”的自引用循环。路径“用户 [0]”。

我不明白 如何 实现 CreatedBy 属性,因为它不是虚拟属性,我不使用 Eager 或 Explicit 加载来请求它。 ....

有人知道为什么吗?

【问题讨论】:

JsonNetResult 实际访问您的每个属性并将其转换为 JSON 对象。 我怀疑将实体序列化为 json 结果会导致它急切地加载整个对象图。要确认这一点,请在 SQL Profiler 运行时调试应用程序并在返回语句上设置断点。在执行语句之前查看它是否已经加载了所有数据。然后看看它之后是否已经加载了所有数据。 可以通过在导航属性上粘贴[JsonIgnore] 属性来阻止它序列化json 另一种方法是实现 ViewModel。 @Guillelon 我不想使用 ViewModels,因为它破坏了拥有导航属性的对象。如果我使用 ViewModels,我必须根据我希望确定可用的导航属性为每个 POCO 创建多个模型?如果是这样,那么使用急切加载会更有意义。我不明白的是为什么它甚至试图加载所有内容。 【参考方案1】:

JSON 序列化是通过使用反射在内部实现的。因此,它会尝试访问构建 JSON 结果所需的每个公共属性。

您可以在不想被序列化的导航属性上添加[JsonIgnore] 属性。这也可能有助于防止序列化产生循环引用(例如,对于多对多关系)。

话虽如此,我建议使用 ViewModels 而不是实体。迟早您需要添加其他属性,而不是直接来自实体本身的属性(例如:只读计算字段,与您的实体无关但在您的表单中需要的其他字段......等等)。您可以在实体的部分定义中添加这些属性,但是当您拥有许多视图时,它将不再可维护/可读。

第二点是,除非您确实出于特定原因需要它,否则我建议您出于三个主要原因停用延迟加载:

如果您对 ORM 不是非常熟悉,您可能会遇到严重性能问题(例如,您可以在 Google 上搜索“额外延迟加载”) 异常难以处理,因为代码的每个部分都可能引发与 SQL 请求执行失败相关的异常,而不仅仅是从数据库上下文本身查询实体的部分。 延迟加载往往会延长上下文的生命周期(因为在确定永远不会延迟加载属性之前,您无法释放它)

【讨论】:

当我发布这篇文章时,我决定尝试一下 Eager Loading。它涉及更改我的存储库以适应。你会说 Eager Loading 比 Lazy Loading 更好(在性能方面)? @r3plica 这个问题很难给出一个普遍的答案,但我个人认为不会再使用延迟加载了。正确处理异常太难了,而且很容易生成糟糕的代码,对数据库执行数千个查询而不是一个(导致性能极差)。当然你可以编写好的代码并激活延迟加载,但这太痛苦了 IMO。【参考方案2】:

这可能有效:

创建一个或多个定义要序列化的属性的接口。每个实体的“快照”一个接口。

让您的 EF 实体实现所有接口。

然后创建一个合同解析器,该解析器仅适用于在传递给解析器的接口类型上定义的属性。请参阅Serialize only interface properties to JSON with Json.net 并将答案与合同解析器代码一起使用。

对合约解析器进行编码后,您可以传递 EF 实体和您喜欢的任何接口。

希望序列化程序会忽略合约解析程序没有“看到”的属性。

如果可行,请使用您的工作代码发布您自己的答案并将其标记为答案。

【讨论】:

好的,我会试一试

以上是关于EF 延迟加载的主要内容,如果未能解决你的问题,请参考以下文章

EF延迟加载

EF急切加载和延迟加载的区别?

EF 延迟加载

asp.net EF学习系列----深入理解查询延迟加载技术

EF中的预先加载和延迟加载

EF的延迟加载LazyLoad