如何在Entity Framework中无外键约束进行关联查询
我遇到一种场景,使用通用外键会非常合适——即存储其他表ID但不定义外键约束的列。
如下代码中,Transfer.FromId和Transfer.ToId存储其他表的ID,具体关联的表由Transfer.FromType和Transfer.ToType列决定:
public enum TransferType { Ship, // 关联Ships表 Railcar, // 关联Railcars表 Truck, // 关联Trucks表 } public class Transfer { public TransferType FromType { get; set; } public int FromId { get; set; } public TransferType ToType { get; set; } public int ToId { get; set; } public DateTime TimeStamp { get; set; } public double Quantity { get; set; } }
例如,要查询装入特定卡车的所有铁路车厢,对应的SQL语句如下:
select t.* from Transfers t join Railcars r on t.ToType = 2 and t.ToId = truckId and t.FromType = 1 and r.Id = truckId
但我该如何在Entity Framework中实现类似逻辑?看起来我需要先查询Railcar的ID,再查询实际的Railcar数据,是否有我不知道的相关实现技巧?
方法1:直接编写LINQ查询模拟SQL逻辑
EF支持直接映射原生SQL的逻辑到LINQ查询,无需拆分步骤,能一次性生成等效的数据库查询,避免多次访问数据库。
针对上述需求,对应的LINQ查询可以这么写:
int targetTruckId = 123; // 目标卡车ID var railcarsLoadedIntoTruck = context.Transfers .Where(t => t.ToType == TransferType.Truck && t.ToId == targetTruckId && t.FromType == TransferType.Railcar) .Join(context.Railcars, transfer => transfer.FromId, railcar => railcar.Id, (transfer, railcar) => railcar) .ToList();
这个查询会直接转换为你提供的原生SQL逻辑,一次性从数据库获取所有符合条件的Railcar实体,不需要先查询ID再查询数据。
方法2:手动添加导航属性并配置条件映射
如果需要更直观的实体关联,可以为Transfer类添加导航属性,再通过EF的模型配置实现条件化的外键映射:
首先修改Transfer类,添加对应表的导航属性:
public class Transfer { public TransferType FromType { get; set; } public int FromId { get; set; } public TransferType ToType { get; set; } public int ToId { get; set; } public DateTime TimeStamp { get; set; } public double Quantity { get; set; } // 来源端导航属性 public Ship? FromShip { get; set; } public Railcar? FromRailcar { get; set; } public Truck? FromTruck { get; set; } // 目标端导航属性 public Ship? ToShip { get; set; } public Railcar? ToRailcar { get; set; } public Truck? ToTruck { get; set; } }
然后在DbContext的OnModelCreating方法中配置条件关联:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置来源端的条件外键 modelBuilder.Entity<Transfer>() .HasOne(t => t.FromShip) .WithMany() .HasForeignKey(t => t.FromId) .IsRequired(false) .HasFilter("FromType = 0"); // 对应TransferType.Ship的枚举值 modelBuilder.Entity<Transfer>() .HasOne(t => t.FromRailcar) .WithMany() .HasForeignKey(t => t.FromId) .IsRequired(false) .HasFilter("FromType = 1"); // 对应TransferType.Railcar的枚举值 modelBuilder.Entity<Transfer>() .HasOne(t => t.FromTruck) .WithMany() .HasForeignKey(t => t.FromId) .IsRequired(false) .HasFilter("FromType = 2"); // 对应TransferType.Truck的枚举值 // 配置目标端的条件外键 modelBuilder.Entity<Transfer>() .HasOne(t => t.ToShip) .WithMany() .HasForeignKey(t => t.ToId) .IsRequired(false) .HasFilter("ToType = 0"); modelBuilder.Entity<Transfer>() .HasOne(t => t.ToRailcar) .WithMany() .HasForeignKey(t => t.ToId) .IsRequired(false) .HasFilter("ToType = 1"); modelBuilder.Entity<Transfer>() .HasOne(t => t.ToTruck) .WithMany() .HasForeignKey(t => t.ToId) .IsRequired(false) .HasFilter("ToType = 2"); }
配置完成后,查询时可以直接通过导航属性获取关联实体:
int targetTruckId = 123; var railcars = context.Transfers .Where(t => t.ToType == TransferType.Truck && t.ToId == targetTruckId) .Select(t => t.FromRailcar) .Where(r => r != null) .ToList();
方法3:表继承(TPH)重构(可选)
如果业务逻辑相对固定,也可以考虑用**表继承(TPH)**重构模型,将Transfer拆分为ShipToRailcarTransfer、RailcarToTruckTransfer等具体类型,让EF自动处理外键关联。但这种方案灵活性低于通用外键,适合场景固定的情况。
内容的提问来源于stack exchange,提问作者Jonathan Wood

