创建一级导航属性来处理复杂的关系

Creating first degree navigation properties to handle complex relations

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

实体:

  1. Asset 是机构拥有的设备,可以分配给其员工。
  2. Person 是负责给定资产的员工。
  3. Location 是机构内用于跟踪资产的单位。
  4. MovementDoc 是一个文档,其中包含有关资产大规模移动的详细信息(例如适合我们示例的 reassignment/relocation 操作)
  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 相关。它可以同时指向 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 关键字。

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

获取最新动态的 computed/unmapped 属性 会变慢,因为它需要您急切加载您的动态才能访问 属性。也就是说,在数据中拥有规范化的多对多关系并不是 "slow",而是您建议如何访问它。一个关键的性能改进是在您想要与数据交互时依赖于将您的操作向下投影到 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 语句,该语句仅 return 资产及其最新文档。这可以通过选择仅包含所需资产和文档字段的 DTO 或视图模型来进一步简化。通过遵循约定或提供映射配置,Automapper 可以帮助隐藏 ProjectTo<T> 调用背后的这种丑陋之处,尽管我通常发现对于这样的东西我更喜欢 Select 这样更容易跟踪正在检索的内容.这样就不需要急切加载相关数据了。

它涉及更多的代码,但它很灵活并且可以导致更多的 efficient/performant 数据查询。