【问题标题】:How to compute object property in database with C# code?如何使用 C# 代码计算数据库中的对象属性?
【发布时间】:2021-06-04 09:18:22
【问题描述】:

例如,假设我的数据库有一个包含数千艘船的表,每艘船都可能有数千名乘客作为导航属性:

public DbSet<Ship> Ship { get; set; }
public DbSet<Passenger> Passenger { get; set; }

public class Ship
{
    public List<Passenger> passengers { get; set; }
    //properties omitted for example
}

public class Passenger
{
    //properties omitted for example
}

示例用例是有人按 API 获取 所有 艘船,并想知道每艘船是否为空(0 名乘客),因此返回的 JSON 将包含船舶列表每个都有一个布尔值是否为空。

我目前的代码似乎效率很低(包括所有乘客,只是为了确定一艘船是否是空的):

List<Ship> ships = dbContext.Ship
    .Include(x => x.passengers)
    .ToList();

以及稍后将船只序列化为 JSON 时:

jsonShip.isEmpty = !ship.passengers.Any();

我想要一个性能更高(而不是臃肿)的替代方案,而不是包括所有乘客。我有什么选择?

我查看了computed columns,但它们似乎只支持将 sql 作为字符串。如果可能的话,我想留在 C# 代码世界中,例如,通过在 SQL 查询中自动编织来正确设置一个属性将是最佳选择。

【问题讨论】:

  • 您如何确定您的代码效率低下?您是否测量了它的执行时间并发现它不符合特定的软件要求?
  • 您实际上并没有展示如何计算与一艘船相关的乘客数量。您现在所做的就是将所有船只及其相关乘客检索到内存中。 EF 支持过滤它返回的数据——它甚至可以在检索所有结果之前在数据库端进行过滤。您是否研究过如何使用 EF 过滤数据? documentation 是使用任何框架时入门的好地方。
  • @RobertHarvey 您的评论可能针对过早的优化。我的问题不是针对一个地方的具体用例,而是为了了解我的选择是什么,以使用 C# 代码在数据库端计算值。有一天可能会派上用场。
  • @mason 以最简单的方式:bool shipIsEmpty = !ship.passengers.Any();。你能为我指出正确的方向吗?我看不出过滤器如何确定一艘船是否有乘客。

标签: c# postgresql entity-framework-core ef-core-5.0


【解决方案1】:

Ship 创建一个反映 JSON 结果形状的数据传输对象,例如 -

public class ShipDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsEmpty { get; set; }
}

然后在查询中使用投影 -

var ships = dbCtx.Ships
            .Select(p => new ShipDto
            {
                Id = p.Id,
                Name = p.Name,
                IsEmpty = !p.Passengers.Any()
            })
            .ToList();

通常,API 需要生成各种形状的响应,而 DTO 会为您提供定义明确的模型来表示 API 响应的形状。域实体并不总是适用于此。

如果您的域实体 (Ship) 有很多属性,那么在 .Select() 方法中复制所有这些属性可能会很麻烦。您可以使用AutoMapper 为您映射它们。 AutoMapper 有一个 ProjectTo&lt;T&gt;() 方法,可以生成 SQL 并返回投影结果。例如,你可以通过映射配置来实现上述结果 -

CreateMap<Ship, ShipDto>()
    .ForMember(d => d.IsEmpty, opt => opt.MapFrom(s => !s.Passengers.Any()));

还有一个查询 -

var ships = Mapper.ProjectTo<ShipDto>(dbCtx.Ships).ToList();

假设ShipDto 中的所有其他属性与Ship 实体中的名称相似。

编辑:
如果您不想要 DTO 模型 -

  1. 您可以在Ship 模型中添加NotMapped 属性-
public class Ship
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    [NotMapped]
    public bool IsEmpty { get; set; }
    
    public List<Passenger> passengers { get; set; }
}

然后像 -

一样进行查询
var ships = dbCtx.Ships
            .Select(p => new Ship
            {
                Id = p.Id,
                Name = p.Name,
                IsEmpty = !p.Passengers.Any()
            })
            .ToList();
  1. 您可以返回匿名类型 -
var ships = dbCtx.Ships
            .Select(p => new
            {
                Id = p.Id,
                Name = p.Name,
                IsEmpty = !p.Passengers.Any()
            })
            .ToList();

【讨论】:

  • 这确实是一个很好的解决方案,但这是我已经掌握的知识。用例是返回 all 附带一个属性,无论它们是否为空。我试图在问题中更清楚地说明这一点。
  • p.Passengers.Count == 0 可以通过将其替换为!p.Passengers.Any() 来提高效率。
  • @Tom 如果我未能解释您的问题,我深表歉意。我会更新答案。
  • @RobertHarvey 我刚刚发现了这一点。谢谢。我将在更新的答案中使用它。
  • @atiyar 使用 Select 方法的投影正是我们所需要的!在数据库上执行并用 C# 代码编写。 DTO 是一个不错的奖励,但它也适用于匿名类型。唯一(理论上)更好的方法是能够直接为原始 ship 对象上的 [NotMapped] isEmpty 属性定义投影,但目前这不是 ef core 的功能。
【解决方案2】:

如果我正确理解您的意图...

一种方法是将乘客人数存储在每个Ship 实体中。如果您使用域驱动设计,将Ship 视为聚合根,并且仅通过在给定Ship 实体上公开的方法添加或删除乘客,这可能会很好地工作,例如RegisterPassenger()/RemovePassenger()。在这些方法中,增加或减少乘客数量以及添加或删除乘客。

然后,显然,您可以使用 PassengerCount &lt; 0 投影查询 Ships dbset 到您需要的布尔值。而且,很明显,它甚至不会触及Passengers 表。

在传统的贫血域 ASP.NET 系统中,这种数据冗余可能有点风险,因为属性通常是公开可变的,并且您有多个“按摩”实体的服务,这是数据完整性的潜在来源损失。

【讨论】:

    猜你喜欢
    • 2017-03-22
    • 2019-12-05
    • 2019-09-12
    • 2022-11-20
    • 2015-09-20
    • 1970-01-01
    • 1970-01-01
    • 2012-01-03
    • 1970-01-01
    相关资源
    最近更新 更多