【问题标题】:Creating first degree navigation properties to handle complex relations创建一级导航属性以处理复杂关系
【发布时间】:2019-11-04 07:30:51
【问题描述】:

我们设计了一个资产管理系统来跟踪机构中流通的有形资产。我们使用ASP.NET MVC 架构和EF6 作为我们的ORM。

实体

  1. Asset 是该机构拥有的设备,可以分配给其员工。
  2. Person 是对其给定资产负责的员工。
  3. Location 是机构内用于跟踪资产的单位。
  4. MovementDoc 是一份文档,其中包含有关资产大规模移动的详细信息(例如适合我们示例的重新分配/重新定位操作)
  5. AssetMovement 是资产的任何单个移动过程详细信息。像 AssetMovementDoc 之间的连接实体一样工作,但也有自己的属性,因此不是一次性的。

关系

每个Asset 在其生命周期中都会遇到无数的运动过程。

public class Asset {
    public ICollection<AssetMovement> Movements { get; set; }
}

AssetMovement 是需要记录的网桥实体

public class AssetMovement
{
    public Asset Asset { get; set; }
    public long? AssetId { get; set; }

    public MovementDoc Document { get; set; }
    public long? DocumentId { get; set; }
}

MovementDoc 可能包含许多AssetMovement,每个Asset 都与不同的Asset 相关。它可以同时指向PersonLocation

public class MovementDoc
{
    public Location TargetLocation { get; set; }
    public long? TargetLocationId { get; set; }

    public Person PersonReceived { get; set; }
    public long? PersonReceivedId { get; set; }

    public ICollection<AssetMovement> Movements { get; set; }
}

这个关系网络似乎适合我们的应用。但在实践中,它有一些缺点。

问题

我们的用户希望在数据表中列出他们的资产以及分配的人员和位置信息。我们的第一种方法是在Asset 上创建一个计算属性:

[NotMapped, Computed]
    public MovementDoc LastMovementDoc
    {
        get
        {
            if (Movements.Count == 0)
                return null;
            else
                return Movements.Select(x => x.Document).OrderByDescending(x => x.DocumentDate).FirstOrDefault();
        }
        private set { }
    }

这为我们提供了最后一次移动操作资产遭遇。所以我们可以从中提取人物和位置信息。它有效,我们将它用于列表和过滤(在DelegateDecompiler 库的帮助下转换为LINQ)。但它很慢,随着数据库的增长它会变慢。

最后,我们选择了一种更简单但更脏的方法:

public class Asset
{
    public Person AssignedPerson { get; set; }
    public long? AssignedPersonId { get; set; }

    public Location AssignedLocation { get; set; }
    public long? AssignedLocationId { get; set; }
}

是的,我们只是将Asset 绑定到它当前直接分配的PersonLocation(也保持旧关系)。发生新任务时会更新这些信息。

但感觉好像我们在这里遗漏了一些东西。创造额外的一级关系真的很聪明吗?或者有没有更有效的方法来处理这种关系复杂性?

顺便说一句,我们在全局范围内禁用了延迟加载,所以不要介意缺少 virtual 关键字。

【问题讨论】:

  • DocumentDate 上是否有索引?
  • 我很难相信延迟加载已被禁用。原因是要么运动应该总是空的(除非你指定一个包含,但你没有包含最关键的代码,这是你如何检索资产,所以这都是猜测)或者你总是加载所有您检索的所有资产的移动(这非常低效)。您也没有缓存 select 的结果,所以每次一些代码访问属性时,查询都会再次运行,非常低效。
  • @GertArnold 不,没有。会有帮助吗?
  • 是的,还有其他索引,比如AssetMovement.DocumentId

标签: c# asp.net-mvc database-design entity-framework-6


【解决方案1】:

将分配的人员和位置非规范化到移动文档顶部的资产中可能会面临的问题是,无法强制资产中引用的 FK 必须与最近的移动相关,甚至与该资产相关的任何移动。您将需要依赖您的系统或系统中的库作为唯一代码来触及这些关系,并且该代码没有错误。 (不可能部分放弃更改)

获取最新动作的计算/未映射属性会变慢,因为它需要您预先加载动作才能访问该属性。也就是说,在数据中具有标准化的多对多关系并不“慢”,只是您建议如何访问它。一个关键的性能改进是当您想要与数据交互时依赖于将您的操作投射到 DTO、ViewModel 或匿名类型中,而不是让您的业务逻辑尝试直接与实体图交互。例如,如果我想使用您的原始模型获取有关资产及其当前位置的一些详细信息,我这样做了:

var asset = context.Assets
    .Include(x => Movements)
    .ThenInclude(x => x.Document)
    .Single(x => x.AssetId == assetId);

这将允许我访问 LastMovementDoc 属性。为了到达这里,我必须加载完整的资产,以及它的所有动作。如果我想获取资产列表并过滤它们的当前位置,我必须加载它们的所有动作。

使用投影,您可以优化查询以仅检索您关心的细节。例如,仅获取该资产及其当前的移动文档..(作为实体)

var assetDetails = context.Assets
    .Select(x => new 
    { 
        Asset = x, 
        CurrentDocument = x.Movements
            .OrderByDescending(m => m.Document.DocumentDate)
            .Select(m => m.Document)
            .FirstOrDefault()
    }).Single(x => x.Asset.AssetId == assetId);

这将生成一条 SQL 语句,该语句将只返回资产及其最新文档。这可以通过选择仅包含所需资产和文档中的字段的 DTO 或视图模型来进一步简化。通过遵循约定或提供映射配置,Automapper 可以帮助隐藏ProjectTo&lt;T&gt; 调用背后的丑陋,尽管我通常发现对于这样的东西我更喜欢Select,因此更容易跟踪正在检索的内容。这种方式无需预先加载相关数据。

它涉及更多的代码,但它很灵活,可以提高数据查询的效率/性能。

【讨论】:

  • 感谢您的回答。我们已经在使用带有 Automapper 的 DTO(带有 ASP.NET Boilerplate),但我们始终使用 Map&lt;T&gt; 方法。事实证明,该方法正在调用所有字段,无论它们是否已映射。所以将它们更改为ProjectTo&lt;T&gt; 真的很重要!现在我们仍在测试,所以我会相应地更新我的问题,包括查询代码。
  • 是的,ProjectTo 旨在与 EF 的 IQueryable 实现一起使用,如果底层查询没有过早执行,则可以大大加快速度。通过将数据投影到 DTO,您无需担心急切或延迟加载,因为相关表将自动包含在内。分析数据库以捕获 SQL 语句有助于了解 EF 在幕后尝试执行的操作以查找瓶颈。
  • 好吧,我接受这个。因为实现ProjectTo 方法足以解决我们的速度问题。此外,Select(x =&gt; new {...}) 查询非常适合检索复杂查询,例如实体子项及其道具的计数。但这是另一个话题的讨论。谢谢史蒂夫!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-02
  • 2014-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多