EF Core 的 LINQ 选择语句中的临时变量



我在编写稍微复杂的 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 对象的列表,以及哪个传感器发送了它以及何时记录了它。到目前为止非常简单,如果我想呈现所有 SensorEvents 以及它们来自哪个传感器,我可以返回以下域对象

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();
}

到目前为止,非常简单,当我想在记录此数据时包括传感器的放置位置(哪个房间,哪个楼层和建筑物)时,就会出现问题。看,我在插入时没有此信息。传感器可以四处移动,因此在插入该时间间隔内来自该传感器的 SensorEvent后,可能会更新 SensorPlacement 表。这意味着不是将传感器事件链接到房间和传感器,而是在查询时解析房间。是的,这意味着如果 SensorPlacement 尚未更新,查询可能会返回错误的房间,但它最终是正确的。当然,使问题进一步复杂化的是,房间中可能没有发生 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 次(使已经很慢的查询变得更糟)。我试图通过将其存储为临时变量来解决它,但显然您只能在转换为 SQL 的 Select 查询中有一个语句。我尝试在此语句之前运行一个额外的 Select 语句,返回一个带有 SensorEvent 和相应 SensorPlacement 的匿名类型,但这会破坏导航属性。使用联接还会使导航属性的使用变得困难。

我是从错误的角度看待这个问题吗?我是否缺少一些语法,或者我需要以另一种方式执行此操作?我知道最好的解决方案是能够在插入时知道传感器放置,但目前这是不可能的。

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

更快地使用导航属性,并将 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 和导航属性实际上是正确的)为您提供实体列表以及位于 中的列表导航。包括,在 where 子句上的匹配(我认为您不希望它以这种方式

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

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

由于您知道某些属性和属性的属性在数据库中为 null,因此可以在映射甚至 .AfterMap() 根据需要。

我也会重新考虑你的命名策略。DomainEntityName是描述性的,但是当每个类都以相同的单词开头时,它会破坏智能感知。这也让我想起了为什么你不想使用匈牙利符号。

在 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))))

这应该为您提供包含的列表(在您的例子中为列表)具有任何匹配属性的任何父级。

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

最新更新