【问题标题】:Temporary variables in LINQ select statements for EF CoreEF Core 的 LINQ 选择语句中的临时变量
【发布时间】:2019-07-23 19:53:15
【问题描述】:

我在编写稍微复杂的 Select 语句时遇到问题。

我的 EF 对象如下所示:

public partial class SensorEvent
{
    public SensorEvent()
    {
       SensorData = new HashSet<SensorData>();
    }

    public int Id { get; set; }
    public int SensorId { get; set; }
    public int RecordTime { get; set; }

    public Sensor Sensor { get; set; }
    public ICollection<SensorData> SensorData{ get; set; }
}

public partial class SensorData
{
    public int Id { get; set; }
    public int SensorEventId { get; set; }
    public string DataType { get; set; }
    public string Description { get; set; }

    public SensorEvent SensorEvent { get; set; }
}

public partial class Sensor
{
    public Sensor()
    {
        SensorEvent = new HashSet<SensorEvent>();
        SensorPlacement = new HashSet<SensorPlacement>();
    }

    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<SensorEvent> SensorEvent{ get; set; }
    public ICollection<SensorPlacement> SensorPlacement{ get; set; }
}

public partial class Room
{
    public Sensor()
    {
        SensorPlacement = new HashSet<SensorPlacement>();
    }

    public int Id { get; set; }
    public int FloorId { get; set; }
    public string Name { get; set; }

    public Floor Floor { get; set; }
    public ICollection<SensorPlacement> SensorPlacement{ get; set; }
}

public partial class Floor
{
    public Floor()
    {
        Room = new HashSet<Room>();
    }

    public int Id { get; set; }
    public int BuildingId { get; set; }
    public string Name { get; set; }

    public Building Building { get; set; }
    public ICollection<Room> Room{ get; set; }
}

public partial class Building
{
    public Building()
    {
        Floor = new HashSet<Floor>();
    }

    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Floor> Floor{ get; set; }
}

public partial class SensorPlacement
{
    public int Id { get; set; }
    public int SensorId { get; set; }
    public int RoomId { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }

    public Sensor Sensor { get; set; }
    public Room Room { get; set; }
}

让我解释一下。插入到数据库的主要数据是 SensorEvent。 SensorEvent 本质上是从传感器发送的 SensorData 对象列表,以及发送它的传感器和记录时间。到目前为止非常简单,如果我想展示所有 SensorEvent 以及它们来自哪个传感器,我可以返回以下域对象

public class DomainSensorEvent
{
    public int Id { get; set; }
    public int SensorName { get; set; }
    public int RecordTime { get; set; }

    public IList<DomainSensorData> SensorData{ get; set; }
}

public partial class DomainSensorData
{
    public int Id { get; set; }
    public string DataType { get; set; }
    public string Description { get; set; }
}

我可以像这样得到它(为了简单起见,忽略 Where、Take、OrderBy 和 Skip 部分)。

public List<DomainSensorEvent> GetDomainSensorEvents()
{
    return DbContext.SensorEvent.Select(dse => new DomainSensorEvent
    {
        Id = dse.Id,
        SensorName = dse.Sensor.Name,
        RecordTime = dse.RecordTime,
        SensorData = dse.SensorData.Select(sd => new DomainSensorData
        {
            Id = sd.Id,
            DataType = sd.DataType,
            Description = sd.Description
        }).ToList()
    }).ToList();
}

到目前为止,很简单,当我想在记录此数据时包括传感器的放置位置(哪个房间,哪个楼层和建筑物)时,就会出现问题。看,我在插入时没有这些信息。传感器可以四处移动,因此 SensorPlacement 表可能会在插入该时间间隔内来自该传感器的 SensorEvent 之后更新。这意味着不是将 SensorEvents 链接到 Room 和 Sensor,而是在查询时解析 Room。是的,这意味着如果 SensorPlacement 尚未更新,则查询可能会返回错误的房间,但它最终会是正确的。更复杂的当然是 Room 中可能没有发生 SensorEvent,所以我还需要检查 null。

新的域模型和查询(我需要帮助的部分)如下所示。

public class DomainSensorEvent
{
    public int Id { get; set; }
    public int SensorName { get; set; }
    public int Room { get; set; }
    public int Floor { get; set; }
    public int Building { get; set; }
    public int RecordTime { get; set; }

    public IList<DomainSensorData> SensorData{ get; set; }
}


public List<DomainSensorEvent> GetDomainSensorEvents()
{
    return DbContext.SensorEvent.Select(dse => new DomainSensorEvent
    {
        Id = dse.Id,
        SensorName = dse.Sensor.Name,
        Room = dse.Sensor.SensorPlacement.Where(sp => dse.RecordTime > sp.From && (sp.To == null || dse.RecordTime < sp.To)).FirstOrDefault() != null ? 
               dse.Sensor.SensorPlacement.Where(sp => dse.RecordTime > sp.From && (sp.To == null || dse.RecordTime < sp.To)).FirstOrDefault().Room.Name : null,

        Floor = dse.Sensor.SensorPlacement.Where(sp => dse.RecordTime > sp.From && (sp.To == null || dse.RecordTime < sp.To)).FirstOrDefault() != null ? 
                dse.Sensor.SensorPlacement.Where(sp => dse.RecordTime > sp.From && (sp.To == null || dse.RecordTime < sp.To)).FirstOrDefault().Room.Floor.Name : null,

        Building = dse.Sensor.SensorPlacement.Where(sp => dse.RecordTime > sp.From && (sp.To == null || dse.RecordTime < sp.To)).FirstOrDefault() != null ? 
                   dse.Sensor.SensorPlacement.Where(sp => dse.RecordTime > sp.From && (sp.To == null || dse.RecordTime < sp.To)).FirstOrDefault().Room.Floor.Building.Name : null,
        RecordTime = dse.RecordTime,
        SensorData = dse.SensorData.Select(sd => new DomainSensorData
        {
            Id = sd.Id,
            DataType = sd.DataType,
            Description = sd.Description
        }).ToList()
    }).ToList();
}

最明显的问题当然是同一个语句重复了 6 次(使已经很慢的查询变得更糟)。我试图通过将其存储为临时变量来解决它,但显然您只能在 Select 查询中使用一条语句来转换为 SQL。我曾尝试在此之前运行一个额外的 Select 语句,返回一个带有 SensorEvent 和相应 SensorPlacement 的异常类型,但这会破坏导航属性。使用 Joins 也会使导航属性的使用变得困难。

我是不是从错误的角度看待这个问题?是否有一些我遗漏的语法,或者我需要以另一种方式做到这一点?我知道最好的解决方案是能够在插入时知道 SensorPlacement,但目前还不可能。

【问题讨论】:

    标签: c# linq entity-framework-core


    【解决方案1】:

    您在这里有几个问题,所以我不知道这是否真的可以回答,但有一些快速的想法:

    尽快使用您的导航属性,并将您的 where 子句放在初始选择上:

    public List<DomainSensorEvent> GetDomainSensorEvents()
    {
        return DbContext.SensorEvent
        .Include("Sensor")
        .Include("SensorData")
        .Include("Sensor.SensorPlacement")
        .Where(w => w.Sensor.SensorPlacement.Where(sp => w.RecordTime > sp.From 
            && (sp.To == null || w.RecordTime < sp.To)))
        .ToList();        
    }
    

    这应该(假设 fks 和 Navigation 属性实际上是正确的)为您提供一个实体列表,其中包含 .Includes 中的列表导航,与 where 子句匹配(我认为您不想要就像这样)。

    然后我会查看 AutoMapper 或类似的东西以将其转换为您的域/视图模型。

     var results = GetDomainSensorEvents();
     Mapper.Map<DomainSensorEvent>(results);
    

    由于你知道数据库中的某些属性和属性的属性为空,你可以根据需要在映射甚至.AfterMap()中处理它们。

    我还会重新考虑您的命名策略。 DomainEntityName 是描述性的,但是当每个类都以相同的单词开头时,它会破坏 Intellisense。这也让我想起了why you don't want to use Hungarian Notation.

    关于 Where 子句:这应该适用于一对一的分层属性(具有一个孩子的导航属性:

        .Where(w => w.Sensor.SensorPlacement.Where(sp => w.RecordTime > sp.From 
        && (sp.To == null || w.RecordTime < sp.To)))
    

    但要让父母拥有一对多,你需要稍微不同的逻辑:

        .Where(w => w.Sensor.SensorPlacement.Where(sp => sp.Any(w.RecordTime > sp.From 
        && (sp.To == null || w.RecordTime < sp.To))))
    

    这应该会为您提供包含 List (在您的情况下 List )具有任何匹配属性的任何父级。

    最后,这是减少我显然无法在本地复制和运行的代码的一个尝试。希望这会有所帮助,但这不会是确切的解决方案。

    【讨论】:

    • 基本上是从数据库中获取所需的模型并在之后进行映射。这样看起来干净了很多。我会试试的。此外,DomainEntityName 不是我使用的实际名称,我只是认为它可能会更清楚地说明它在这种情况下的含义。
    • @Duckslayer 没错,我希望它有所帮助。如果有帮助,请随时投票!
    • 您能否为我澄清一下 Where 声明?嵌套的 Where 语句将返回匹配 SensorPlacements 的列表,但嵌套语句将需要一个布尔值。理想情况下,我只会“包含”与内部 Where 语句匹配的元素,但我不确定这是否可能。
    • @Duckslayer 这是可能的,但我复制了你的例子作为一个非常粗略的指南。实际上,它将是一个层次结构,其中子元素与条件匹配,然后返回该父元素及其子元素。我更新了示例以希望对您有所帮助
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-30
    • 2015-03-02
    • 2017-08-03
    • 1970-01-01
    • 2020-12-09
    • 2012-01-12
    相关资源
    最近更新 更多