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 延迟加载的主要内容,如果未能解决你的问题,请参考以下文章