简体   繁体   English

EF Core:如何在相关 model 的 model 中获得平均值

[英]EF Core: How to best get average value in a model of a related model

I've got a Blazor Server App using the Entity Framework (EF Core).我有一个使用实体框架(EF Core)的 Blazor 服务器应用程序。

I use a code first approach with two models, Entry and Target .我使用两种模型的代码优先方法,即EntryTarget

Each entry has a target.每个条目都有一个目标。 So a target can have more than one entry pointing to it.因此,一个目标可以有多个指向它的条目。

The model for the Target looks like this: Target的 model 如下所示:

public class Target
  {
    public string TargetId { get; set; }
    
    [Required]
    public string Name { get; set; }

    [InverseProperty("Target")]
    public List<Entry> Entries { get; set; }

    [NotMapped]
    public double AverageEntryRating => Entries != null ? Entries.Where(e => e.Rating > 0).Select(e => e.Rating).Average() : 0;
  }

An entry can have a rating, the model Entry looks like this:条目可以有等级,model Entry如下所示:

public class Entry
  {
    public string EntryId { get; set; }
    
    public int Rating { get; set; }
    
    [Required]
    public string TargetId {get; set; }

    [ForeignKey("TargetId")]
    public Target Target { get; set; }    
  }

As you can see in my Target model, I would like to know for each Target , what the average rating for it is, based on the average of all entries that point to the Target - that's why there is this ( NotMapped ) property in the target:正如您在我的Target NotMapped中看到的那样,我想知道每个Target的平均评分是多少,基于指向Target的所有条目的平均值 - 这就是为什么在目标:

public double AverageEntryRating => Entries != null ? Entries.Where(e => e.Rating > 0).Select(e => e.Rating).Average() : 0;

But this does (of course) not always work, as the Entries of the target are not guaranteed to be loaded at the time the property is accessed.但这(当然)并不总是有效,因为不能保证在访问属性时加载目标的条目。

I tried to solve it differently, for example to have a method in my TargetService, where I can pass in a targetId and gives me the result:我试图以不同的方式解决它,例如在我的 TargetService 中有一个方法,我可以在其中传入一个 targetId 并给我结果:

 public double GetTargetMedianEntryRating(string targetId) {
        var median = _context.Entries
            .Where(e => e.TargetId == targetId && e.Rating > 0)
            .Select(e => e.Rating)
            .DefaultIfEmpty()
            .Average();
        return median;
    }

But when I list out my targets in a table and then in a cell want to display this value (passing in the current targetId of the foreach loop) I get a concurrency exception, as the database context is used in multiple threads (I guess one from looping through the rows/targets and one other from getting the average value)... so this leads me into new troubles.但是当我在一个表中列出我的目标然后在一个单元格中想要显示这个值时(传入 foreach 循环的当前 targetId)我得到一个并发异常,因为数据库上下文在多个线程中使用(我猜一个从循环遍历行/目标和另一个从获取平均值)......所以这让我陷入了新的麻烦。

Personally I would prefer to work with the AverageEntryRating property on the Target model, as it seems natural to me and it would also be convenient to access the value just like this.就我个人而言,我更喜欢使用Target model 上的AverageEntryRating属性,因为这对我来说似乎很自然,而且像这样访问值也很方便。

But how would I make sure, that the entries are loaded, when I access this property.但是,当我访问此属性时,我将如何确保已加载条目。 Or is this not a good approach because this would mean I would have to load Entries anyway for all the targets which would lead to performance degradation?或者这不是一个好方法,因为这意味着我必须为所有会导致性能下降的目标加载条目? If yes, what would be a good way to get to the average/median value?如果是,那么获得平均值/中值的好方法是什么?

There are a couple of options I could think of, and it depends on you situation what to do.我可以想到几个选项,这取决于你的情况该怎么做。 There might be more alternatives, but at least I hope that this can give you some options you hadn't considered.可能有更多的选择,但至少我希望这可以给你一些你没有考虑过的选择。

Have a BaseQuery extension method that always include all Entries有一个始终包含所有条目的 BaseQuery 扩展方法

You could make sure of doing .Include(x => x.Entries) whenever you are querying for Target .您可以确保在查询Target时执行.Include(x => x.Entries) You can even create an extension method of the database context called something like TargetBaseQuery() that includes all necessary relationship whenever you use it.您甚至可以创建数据库上下文的扩展方法,称为TargetBaseQuery()之类的东西,在您使用它时包含所有必要的关系。 Then you will be sure that the Entries List of each Target will be loaded when you access the property AverageEntryRating .然后,您将确保在访问属性AverageEntryRating时将加载每个目标的Entries列表。

The downside will be a performance hit, since every time you load a Target you will need to load all its entries... and that's for every Target you query.不利的一面是性能下降,因为每次加载 Target 时都需要加载其所有条目……这适用于您查询的每个 Target。 However, if you need to get it working fast, this would be probably the easiest.但是,如果您需要让它快速工作,这可能是最简单的。 The pragmatic approach would be to do this, measure the performance hit, and if it is too slow then try something else, instead of doing premature optimization.务实的方法是这样做,测量性能影响,如果速度太慢,请尝试其他方法,而不是过早优化。 The risk of course would be that it might work fast now, but it might scale badly in the future.当然,风险在于它现在可能工作得很快,但将来可能会严重扩展。 So it's up to you to decide.因此,由您决定。

Another thing to consider would be to not Include the Entries every single time, but only in those places where you know you need the average.另一件要考虑的事情是不要每次都包括条目,而只在那些你知道你需要平均值的地方。 It might however become a maintainability issue.然而,它可能会成为一个可维护性问题。

Have a different model and service method to calculate the TargetStats有不同的 model 和服务方法来计算 TargetStats

You could create another class Model that stores the related data of a Target , but it's not persisted in the database.您可以创建另一个 class Model 来存储Target的相关数据,但它不会保存在数据库中。 For example:例如:

public class TargetStats
  {    
    public Target Target { get; set; }

    public double AverageEntryRating { get; set; }
  }

Then in your service you could have a method ish like this (haven't tested, so it might not work as is, but you get the idea) :然后在您的服务中,您可以使用这样的方法(尚未测试,因此它可能无法按原样工作,但您明白了)

public List<TargetStats> GetTargetStats() {
        var targetStats = _context.Target
            .Include(x => x.Entries)
            .Select(x => new TargetStats 
            {
              Target = x, 
              AverageEntryRatings = x.Entries.Where(e => e.Rating > 0).Select(e => e.Rating).Average(),
            })
            .ToList()

        return targetStats;
    }

The only advantage of this is that you don't have to degrade the performance of all Target related queries, but only of those that requires the average rating.这样做的唯一好处是您不必降低所有 Target 相关查询的性能,而只需降低那些需要平均评分的查询。 But this query in particular might still be slow.但特别是这个查询可能仍然很慢。 What you could do to further tweak it, is write raw SQL instead of LINQ or have a view in the database that you can query.你可以做些什么来进一步调整它,写原始的 SQL 而不是 LINQ 或者在数据库中有一个可以查询的视图。

Store and update the Target's average rating as a column将目标的平均评分存储并更新为列

Probably the best you could do to keep the code clean and have good performance while reading, is to store the average as a column in the Target table.为了保持代码干净并在阅读时获得良好的性能,您可以做的最好的事情可能是将平均值作为列存储在 Target 表中。 This will move the performance cost of the calculation to the saving/updating of a Target or its related Entries, but the readings will be super fast since the data is already available.这会将计算的性能成本转移到目标或其相关条目的保存/更新上,但由于数据已经可用,因此读数将非常快。 If the readings happen way more often than the updates, then it's probably worth doing it.如果读数发生的频率高于更新,那么可能值得这样做。

You could take a look at EF Core docs on perfomance , since they talk a little bit about the different perfomance tunning alternatives.您可以查看有关性能的 EF Core 文档,因为他们谈到了一些不同的性能调整替代方案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM