【问题标题】:Only include what is included Entity Framework仅包含实体框架所包含的内容
【发布时间】:2017-02-12 15:42:24
【问题描述】:

我正在为购物车进行大型数据库调用。它包括许多关系,都使用.Include() 方法指定。

现在我只希望 EF 包含我指定的内容。当我包含一个集合时,它会自动加载集合的关系。

所以我有一个ShoppingCart,购物车有一个集合,如果ShoppingCartProducts 并且该集合与ShoppingCartProduct 有关系。

所以我想再次包含产品,而不是购物车,所以我这样做了:

IQueryable<ShoppingCart> query = DbContext.ShoppingCarts
                        .Include(p => p.ShoppingCartProducts)
                        .Include(p => p.ShoppingCartProducts.Select(x => x.Product))

稍后我执行.FirstOrDefault() 来执行查询。通过这个调试,它还在每个ShoppingCartProducts中包含了ShoppingCart

这听起来有点小,但实际上在整个应用程序中都是这样。新架构将实体对象转换为具有不同静态方法和扩展的模型。最终导致 StackoverflowException,因为它递归地包含了它的关系。

那么我如何包含我已包含的内容?

我已将 LazyLoadingEnabled 设置为 false,并将 ProxyCreationEnabled 设置为 false。而且我的收藏/回复没有标有virtual

检查了这些答案:

DBContext lazyloadingenabled set to true still loads related entities by default 关于集合的包含确实如此,但是一旦包含集合,该集合将加载所有其他关系(我猜)

Entity Framework with Proxy Creation and Lazy Loading disabled is still loading child objects 几乎相同的问题,但不是一个好的答案,只是一个解释

EF 6 Lazy Loading Disabled but Child Record Loads Anyway 使用分离并没有帮助。

编辑:

正如 Evk 所提到的,这与 EF 自动填充已知关系的空白有关。现在的问题实际上是如何关闭它。

编辑 2:

因此,在 Evk 的回答和我自己的解决方法之后,我们了解到这些解决方案并不能解决全局问题。让我试着解释一下:

这些扩展和ConvertToModel 方法在每个存储库中实现,并在与它有关系时相互调用。这个概念实际上很棒:如果你有关系,只需转换为模型,如果你没有,不要做任何事情。然而,由于这个 EF“错误”,我了解到所有已知的关系都插入到任何地方。

以下是我们的解决方案不起作用的示例。对于这种情况,代码将首先为ShoppingCart 调用ConvertToModel,然后是其余的。但当然也可以反过来。

ShoppingCartRepository

    public static ShoppingCartModel ConvertToModel(ShoppingCart entity)
    {
        if (entity == null) return null;
        ShoppingCartModel model = new ShoppingCartModel
        {
            Coupons = entity.ShoppingCardCoupons?.SelectShoppingCouponModel(typeof(ShoppingCart)),
            Products = entity.ShoppingCartProducts?.SelectShoppingCartProductModel(typeof(ShoppingCart)),

        };
        return model;
    }

ShoppingCartProductRepository

    public static IEnumerable<ShoppingCartProductModel> SelectShoppingCartProductModel(this IEnumerable<ShoppingCartProduct> source, Type objSource = null)
    {
        bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartProduct>);
        return source.Select(x => new ShoppingCartProductModel
        {
            ShoppingCart = includeRelations && objSource != typeof(ShoppingCart) ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
            ShoppingCartCoupons = includeRelations && objSource != typeof(ShoppingCartCoupon) ? x.ShoppingCartCoupons?.SelectShoppingCouponModel(typeof(ShoppingCartProduct)) : null,
        });
    }

ShoppingCartCouponRepository

    public static IEnumerable<ShoppingCartCouponModel> SelectShoppingCouponModel(this IEnumerable<ShoppingCartCoupon> source, Type objSource = null)
    {
        bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartCoupon>);
        return source.Select(x => new ShoppingCartCouponModel
        {
            ShoppingCart = includeRelations && objSource != typeof(ShoppingCart) ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
            ShoppingCartProduct = includeRelations && objSource != typeof(ShoppingCartProductModel) ? ShoppingCartProductRepository.ConvertToModel(x.ShoppingCartProduct) : null
        });
    }

当你研究它时,你会发现它可以从ShoppingCartShoppingCartProductShoppingCartCoupon 再到ShoppingCart

我目前的解决方法是找出聚合根并选择哪一个需要哪一个。但我宁愿有一个优雅的解决方案来解决这个问题。最好的办法是阻止 EF 加载那些已知的关系,或者以某种方式确定属性是否以这种方式加载(反射?)。

【问题讨论】:

  • 在您的情况下,EF 不会从数据库中加载相关实体。对于每个 ShoppingCartProduct,相关的 ShoppingCart 是已知的 - 这就是您要查询的购物车。此 ShoppingCart 已经加载到上下文中,这就是 EF 填充 ShoppingCartProduct 的 ShoppingCart 属性的原因 - 它已经加载并且已知并且无需加载任何成本。不过,我不知道如何以某种方式禁用这种(相当合理的)行为。
  • 好点,这有助于理解“问题”。更新了答案。
  • 我认为您将不得不修改您的扩展方法,以通过跟踪您已经访问过的实体并忽略它们(不转换为模型)来防止堆栈溢出。或者不忽略,而是像 EF 一样做 - 替换您已经转换为模型的实体。
  • @Evk,你有什么好的建议吗?
  • 您介意使用 AutoMapper 吗?除了解决您的问题之外,它还可以使您的预测更有效率。

标签: c# entity-framework


【解决方案1】:

如 cmets 中所述,这是实体框架的默认行为,我认为无法更改。相反,您可以更改代码以防止 stackoverflow 异常。如何很好地做到这一点很大程度上取决于您的代码库,但我将提供一个草图。在上面的草图中,我使用了其他实体名称(因为我总是检查我的代码示例是否至少在将它们发布到此处之前编译):

public static partial class Ex {
    public static CodeModel ConvertToModel(Code entity) {
        if (entity == null) return null;
        CodeModel model = new CodeModel();
        var map = new Dictionary<object, object>();
        map.Add(entity, model);
        model.Errors = entity.Errors?.SelectShoppingCartProductModel(map);
        return model;
    }        

    public static ErrorModel[] SelectShoppingCartProductModel(this IEnumerable<Error> source, Dictionary<object, object> map = null) {
        bool includeRelations = source.GetType() != typeof(DbQuery<Error>); //so it doesn't call other extensions when we are a db query (linq to sql)
        return source.Select(x => new ErrorModel {
            Code = includeRelations ? (map?.ContainsKey(x.Code) ?? false ? (CodeModel) map[x.Code] : ConvertToModel(x.Code)) : null,
            // other such entities might be here, check the map
        }).ToArray();
    }
}

另一种选择是将当前模型存储在线程局部变量中。如果你调用了一些ConvertToModel 方法并且这个线程局部变量不为空——这意味着这个方法已经被递归调用了。示例:

public static partial class Ex {
    private static readonly ThreadLocal<CodeModel> _code = new ThreadLocal<CodeModel>();
    public static CodeModel ConvertToModel(Code entity) {
        if (entity == null) return null;
        if (_code.Value != null)
            return _code.Value;

        CodeModel model = new CodeModel();
        _code.Value = model;
        model.Errors = entity.Errors?.SelectShoppingCartProductModel();
        // other setters here
        _code.Value = null;
        return model;
    }

    public static ErrorModel[] SelectShoppingCartProductModel(this IEnumerable<Error> source) {
        bool includeRelations = source.GetType() != typeof(DbQuery<Error>); //so it doesn't call other extensions when we are a db query (linq to sql)
        return source.Select(x => new ErrorModel {
            Code = includeRelations ? ConvertToModel(x.Code) : null,
        }).ToArray();
    }
}

如果您在所有 ConvertToModel 方法中实现此功能 - 无需传递任何参数或更改代码的其他部分。

【讨论】:

  • dayum,很好,但我希望有一个更优雅的解决方案,可以避免发送额外的参数。我现在用它发送对象的类型并使用它来查看它是否相同。我不确定,但两种解决方案(你的和我的)似乎只能解决 1 个关系的问题?
  • 您可以将该映射传递给所有执行转换的方法。您还可以将所有转换方法组合在一个非静态类中,并将映射存储在该类的私有字段中。这种方式不需要额外的参数。
  • 感谢您的建议,确实更好,但恐怕它不能解决大问题。请参阅更新的答案。不怪你,感谢迄今为止的大力帮助!我只是在寻找越来越深入的解决方案。
  • 你需要保持一致。在一个地方您手动创建 ShoppingCartProductModel (SelectShoppingCartProductModel),在另一个地方您调用 ShoppingCartProductRepository.ConvertToModel。如果您将在任何地方调用 ShoppingCartProductRepository.ConvertToModel - 我的最后一个想法应该可以正常工作。
  • 我必须同时支持转换单个对象和列表。 IEnumerable 扩展如果仍然是 linq-to-sql 也可以使用,我从中获得了一些好处。不过仍然是一个很好的解决方案。
【解决方案2】:

此解决方案检查源对象类型是否不等于我们调用 ConvertToModel 的对象类型。

public static ShoppingCartModel ConvertToModel(ShoppingCart entity)
{
    if (entity == null) return null;
    ShoppingCartModel model = new ShoppingCartModel
    {
        ...
        Products = entity.ShoppingCartProducts?.SelectShoppingCartProductModel(typeof(ShoppingCart)),
    };
    return model;
}

SelectShoppingCartProductModel 扩展:

public static partial class Ex
{
    public static IEnumerable<ShoppingCartProductModel> SelectShoppingCartProductModel(this IEnumerable<ShoppingCartProduct> source, Type objSource = null)
    {
        bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartProduct>);//so it doesn't call other extensions when we are a db query (linq to sql)
        return source.Select(x => new ShoppingCartProductModel
        {
            ....
            ShoppingCart = includeRelations && objSource != typeof(ShoppingCart)  ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
        });
    }
}

但这可能并不能解决整个问题。如果你有另一个实体,比如ShoppingCart 中的AdditionalCosts,它也引用了ShoppingCartProduct,它仍然会“旋转”。 如果有人对此有解决方案,那就太好了!

ShoppingCart -> ConvertToModel(shoppingCart) -> SelectAdditionalCostsModel -> ShoppingCartProduct -> ConvertToModel(shoppingCartProduct) -> ShoppingCart -> ConvertToModel(shoppingCart)。等等……

【讨论】:

    猜你喜欢
    • 2015-09-08
    • 1970-01-01
    • 2011-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多