当我尝试使用 .Include() 进行预加载时,EF Core 是延迟加载

EF Core is lazy loading when I try to eager load with .Include()

我正在使用 EF Core 2.2.6(数据库优先),似乎只是启用延迟加载使我无法进行预加载。启用延迟加载是否会排除以任何容量使用预加载?

namespace Example.Models
{
    public class Lead
    {
        public int Id { get; set; }
        public LeadOrganization LeadOrganization { get; set; }

        public Lead(ExampleContext.Data.Lead dbLead)
        {
            Id = dbLead.Id;
            LeadOrganization = new LeadOrganization(dbLead.LeadOrganization);
        }

        public static Lead GetLead(int id)
        {
            using (var db = new ExampleContext())
            {
                var dbLead = db.Leads
                    .Include(l => l.LeadOrganization)
                        .ThenInclude(lo => lo.LeadOrganizationAddresses)
                            .ThenInclude(loa => loa.AddressType)
                    .FirstOrDefault(l => l.Id== id);

                return new Lead(dbLead);
            }
        }
    }
}
namespace Example.Models
{
    public class LeadOrganization
    {
        public IEnumerable<LeadOrganizationAddress> Addresses { get; set; }

        public LeadOrganization(ExampleContext.Data.LeadOrganization dbLeadOrganization)
        {
            Addresses = dbLeadOrganization.LeadOrganizationAddresses.Select(loa => new LeadOrganizationAddress(loa));
        }
    }
}
namespace Example.Models
{
    public class LeadOrganizationAddress
    {
        public AddressType AddressType { get; set; }

        public LeadOrganizationAddress(ExampleContext.Data.LeadOrganizationAddress dbLeadOrganizationAddress)
        {
            AddressType = new AddressType(dbLeadOrganizationAddress.AddressType);
        }
    }
}
namespace Example.Models
{
    public class AddressType
    {
        public short Id { get; set; }

        public AddressType(ExampleContext.Data.AddressType dbAddressType)
        {
            Id = dbAddressType.Id;
        }
    }
}

ExampleContext.Data 命名空间包含 EF 从数据库生成的部分 类。 LeadLeadOrganizationLeadOrganizationAddressAddressType 是 类 基本上 1:1 具有属性方面的部分,但具有静态方法添加(是的,这很奇怪,但这是我必须处理的)。

Lead 有一个 LeadOrganization,LeadOrganization 又至少有一个 LeadOrganizationAddress,而 LeadOrganizationAddress 又有一个 AddressType。

GetLead 调用 Lead 构造函数时,查询中的数据尚未加载,尽管它应该预先加载。这会导致嵌套对象的问题。当它最终到达 LeadOrganizationAddress 构造函数时,DbContext 已被释放,因此无法延迟加载关联的 AddressType.

我是不是误解了预加载的全部意义?我认为它会在初始查询时检索所有数据,然后让我毫无问题地将其传递给构造函数。我不需要继续返回数据库并延迟加载任何东西。

如果启用了延迟加载,是否可以简单地不进行预加载?是否有其他解决方法,例如强制它加载任何代理实体?

延迟加载并不是阻止属性实例化的原因,而是缺少适当的构造函数。

除了 EF Core,这些类型很奇怪,例如LeadOrganization 需要在它们的构造函数中传入它们自己的实例。这有点像先有鸡还是先有蛋的问题——您如何创建第一个?

public class LeadOrganization
{
    public IEnumerable<LeadOrganizationAddress> Addresses { get; set; }

    public LeadOrganization(ExampleContext.Data.LeadOrganization dbLeadOrganization)
    {
        Addresses = dbLeadOrganization.LeadOrganizationAddresses.Select(loa => new LeadOrganizationAddress(loa));
    }
}

在任何情况下,EF Core only supports simple constructors with parameters based on convention(基本上是参数到属性的 1-1 映射),因此它不知道如何实例化和水化那些嵌套的 类,急切或懒惰。

我建议让这些构造函数成为无参数的,或者如果您希望 EF 通过属性来混合对象,至少向您的 类.

添加一个无参数的构造函数

我假设您使用 UseLazyLoadingProxies() 但想禁用查询中特定包含的延迟加载。这还没有实现:

https://github.com/aspnet/EntityFrameworkCore/issues/10787

你现在唯一能做的事:

1.) 禁用延迟加载代理 ("default lazy loading for all properties")

2.) 然后对特定属性使用(手动实现的)延迟加载,例如在您的一种情况下:

public class LeadOrganization
{
    private ILazyLoader _lazyLoader { get; set; }

    private IEnumerable<LeadOrganizationAddress> _addresses;

    public LeadOrganization(ILazyLoader lazyLoader)
    {
        _lazyLoader = lazyLoader;
    }

    public IEnumerable<LeadOrganizationAddress> Addresses
    {
        get => _addresses;
        set => _addresses = value;
    }

    public IEnumerable<LeadOrganizationAddress> AddressesLazy
    {
        get
        {
            _lazyLoader?.Load(this, ref _addresses);
        }
        set => this._addresses = value;
    }
}

因此对于预加载使用 .Include(lo=>lo.Addresses),对于延迟加载使用 .Include(lo=>lo.AddressesLazy)


编辑 1

不应为所有属性默认启用 IMO 延迟加载 - 这可能会影响整个实现的性能。因此,在延迟加载为您带来优势的情况下,上述解决方案是一种替代方案。我也想在每个包含中都有这个选项,比如 .Include(o=>o.Addresses, LoadingBehaviour.Eager) - 也许将来会存在。

好的,在调查问题后,EF Core 2.x 通过代理实现延迟加载存在问题。相关的跟踪问题是

问题是导航属性 急切加载的,但是 LazyLoader 不知道在处理时 - 无法安全地访问上下文更改跟踪器,只是抛出异常。相关代码可见here,第一行:

if (_disposed)
{
    Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName);
}

正如我所读,它应该在 EF Core 3.0 中被修复,当它与以下 "breaking change" - Lazy-loading proxies no longer assume navigation properties are fully loaded 一起发布时。它还部分解释了当前的问题:

Old behavior

Before EF Core 3.0, once a DbContext was disposed there was no way of knowing if a given navigation property on an entity obtained from that context was fully loaded or not.

很遗憾,这不能帮助您解决当前的问题。我看到的选项是:

  1. 等待 EF Core 3.0 发布
  2. 不要通过代理使用延迟加载
  3. 关闭 lazy loading on disposed context warning - 默认为 Throw,将其更改为 LogIgnore,例如:

    optionsBuilder.ConfigureWarnings(warnings => warnings
        .Log(CoreEventId.LazyLoadOnDisposedContextWarning)
    );