C# 创建一级导航属性以处理复杂关系
我们设计了一个资产管理系统来跟踪机构中流通的有形资产。我们使用C# 创建一级导航属性以处理复杂关系,c#,asp.net-mvc,database-design,entity-framework-6,C#,Asp.net Mvc,Database Design,Entity Framework 6,我们设计了一个资产管理系统来跟踪机构中流通的有形资产。我们使用ASP.netmvc体系结构和EF6作为我们的ORM 实体: 资产是机构拥有的设备,可分配给其员工 人员是对其给定资产负责的员工 位置是机构内用于跟踪资产的单位 MovementDoc是一个包含资产大规模移动详细信息的文档(如适用于我们示例的重新分配/重新定位操作) 资产移动是资产的任何单个移动过程细节。工作原理类似于资产和移动文档之间的连接实体,但也有自己的属性,因此不可处置 关系: 每个资产在其生命周期中都会遇到许多移动过程 pu
ASP.netmvc
体系结构和EF6
作为我们的ORM
实体:
资产
是机构拥有的设备,可分配给其员工人员
是对其给定资产负责的员工位置
是机构内用于跟踪资产的单位MovementDoc
是一个包含资产大规模移动详细信息的文档(如适用于我们示例的重新分配/重新定位操作)资产移动
是资产的任何单个移动过程细节。工作原理类似于资产
和移动文档
之间的连接实体,但也有自己的属性,因此不可处置资产
在其生命周期中都会遇到许多移动过程
public class Asset {
public ICollection<AssetMovement> Movements { get; set; }
}
MovementDoc
可能包括许多资产移动
,每个移动都与不同的资产
相关。它可以同时指向人
或位置
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; }
}
这为我们提供了最后一次移动操作。因此,我们可以从中提取人员和位置信息。它可以工作,我们将其用于列表和筛选(在DelegateDecompiler
library的帮助下转换为LINQ
)。但它的速度很慢,而且随着数据库的增长,速度会越来越慢
最后,我们采用了一种更简单但更肮脏的方法:
public class Asset
{
public Person AssignedPerson { get; set; }
public long? AssignedPersonId { get; set; }
public Location AssignedLocation { get; set; }
public long? AssignedLocationId { get; set; }
}
是的,我们只是直接将资产
与当前分配的人员
和位置
绑定(同时保留旧关系)。当发生新的分配时,这些将更新
但感觉好像我们错过了什么。创造额外的一级关系真的聪明吗?还是有更有效的方法来处理这种关系的复杂性
顺便说一句,我们禁用了全局延迟加载,因此不必介意缺少
virtual
关键字。将分配的人员和位置反规范化到移动文档顶部的资产中可能面临的问题是,无法强制执行资产中引用的FK必须与最近的移动相关,甚至与该资产相关的任何移动。您将需要依赖于您的系统或系统中的库,因为它们是唯一能够处理这些关系的代码,并且这些代码没有bug。(不可能部分放弃变更)
获取最新移动的计算/未映射属性将变慢,因为它将要求您加载移动以访问该属性。这就是说,在数据中建立一个规范化的多对多关系并不是“慢”的,而是您建议如何访问它。一个关键的性能改进是,当您想要与数据交互时,依赖于将您的操作向下投影到DTO、ViewModels或匿名类型,而不是让您的业务逻辑尝试直接与实体图交互。例如,如果我想通过您的原始模型获取有关资产及其当前位置的详细信息,我会这样做:
var asset = context.Assets
.Include(x => Movements)
.ThenInclude(x => x.Document)
.Single(x => x.AssetId == assetId);
这将允许我访问LastMovementDoc属性。为了到达这里,我必须装载整个资产,以及它的所有运动。如果我想得到一个资产列表,并对它们的当前位置进行筛选,我必须加载它们的所有移动
通过投影,您可以优化查询以检索您所关心的细节。例如,仅获取该资产及其当前移动文档。。(作为实体)
这将生成一个SQL语句,只返回资产及其最新文档。这可以通过选择DTO或视图模型来进一步简化,该模型仅包含所需资产和文档中的字段。通过遵循惯例或提供映射配置,Automapper可以帮助隐藏ProjectTo
调用背后的丑陋之处,尽管我通常发现对于这样的东西,我更喜欢Select
,这样更容易理解所检索的内容。不需要以这种方式加载相关数据
它涉及更多的代码,但它很灵活,可以导致更高效/性能更好的数据查询。DocumentDate上是否有索引??我很难相信延迟加载被禁用。原因是要么移动应该始终为空(除非您指定一个include,但您没有包含最关键的代码,这就是您如何检索资产,所以这都是推测),要么您总是为您检索的所有资产加载所有移动(这是非常低效的)。您也没有缓存select的结果,因此每次有代码访问属性时,查询都会再次运行,效率非常低。@GertArnold不,没有。它会有帮助吗?是的,还有其他索引,如
AssetMovement.DocumentId
。谢谢您的回答。我们已经将DTO与Automapper(附带)一起使用,但我们始终使用Map
方法。事实证明,该方法调用所有字段,无论它们是否映射。因此,将它们更改为ProjectTo
确实起到了作用!现在我们仍在测试,因此我将相应地更新我的问题,包括查询代码。是的,ProjectTo
设计用于EF的IQueryable
实现,如果底层查询没有过早执行,可以大大加快速度。通过将数据投影到DTO,您无需担心急加载或延迟加载,因为相关表将自动包含在内。分析数据库以捕获SQL语句有助于了解EF在幕后试图做什么来发现瓶颈。
public class Asset
{
public Person AssignedPerson { get; set; }
public long? AssignedPersonId { get; set; }
public Location AssignedLocation { get; set; }
public long? AssignedLocationId { get; set; }
}
var asset = context.Assets
.Include(x => Movements)
.ThenInclude(x => x.Document)
.Single(x => x.AssetId == assetId);
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);