如何配置带版本实体间的一对多/多对一关系
问题:EF Core 中如何建模版本化实体的关联,获取关联实体的所有版本
数据库背景
接手了一个不可修改的数据库,实体通过version列实现版本化,涉及Person、Domicile、Address三个实体,每个都有id+version的复合主键:
- Person 与 Domicile 是一对多关系
- Domicile 与 Address 是多对一关系,但关联仅通过
id(版本独立)
表结构示例
Person表
|id|version|etc. |1 |1 |... |1 |2 |...
Domicile表
|id|version|person_id|address_id|etc... |2 |1 |1 |3 |... |2 |2 |1 |3 |... |3 |1 |1 |4 |...
Address表
|id|version|etc... |3 |1 |... |3 |2 |... |4 |1 |...
主键与外键定义
- 复合主键:
PRIMARY KEY (id, version); - Domicile的外键:仅引用Address的
id,而非完整复合主键:
CREATE TABLE Domicile( -- 其他字段 CONSTRAINT fk_address FOREIGN KEY (address_id) REFERENCES address (id) );
期望的实体模型
public class Person { public long Id {get; set;} public long Version {get; set;} public List<Domicile> Domiciles {get; set;} } public class Domicile { public long Id {get; set;} public long Version {get; set;} public long AddressId {get; set;} public List<Address> AddressVersions {get; set;} } public class Address { public long Id {get; set;} public long Version {get; set;} }
已完成的配置(Person与Domicile)
modelBuilder.Entity<Person>(entity => { // 其他配置 entity.HasMany(p => p.Domiciles) .WithOne() .HasForeignKey(d => d.PersonId) .HasPrincipalKey(p => p.Id); });
遇到的问题:Domicile与AddressVersions的配置
希望在Domicile中获取关联Address的所有版本,但尝试多种配置都无法实现:
- 多对多配置:编译失败,因为需要双方都有导航属性
modelBuilder.Entity<Domicile>(entity => { // 其他配置 entity.HasMany(p => p.AddressVersions) .WithMany()); // 编译错误 });
- 一对多配置:无法生效,Address中没有指向Domicile的外键
modelBuilder.Entity<Domicile>(entity => { // 其他配置 entity.HasMany(p => p.AddressVersions) .WithOne() .HasPrincipalKey(p => p.AddressId); });
- 反向一对多配置:只能获取Address的单个版本,丢失其他版本
modelBuilder.Entity<Domicile>(entity => { // 其他配置 entity.HasOne(p => p.Address) .WithMany() .HasForeignKey(p => p.AddressId) .HasPrincipalKey(d => d.Id); });
预期查询结果
查询id=1、version=2的Person时:
- 返回3条Domicile记录(id=2的2个版本 + id=3的1个版本)
- 每个id=2的Domicile,关联2条Address记录(id=3的version1和version2)
- id=3的Domicile,关联1条Address记录(id=4的version1)
对id=2的Domicile的断言示例:
Assert.That(domicile.AddressVersions.Count, Is.EqualTo(2)); Assert.That(domicile.AddressVersions[0].Id, Is.EqualTo(3)); Assert.That(domicile.AddressVersions[0].Version, Is.EqualTo(1)); Assert.That(domicile.AddressVersions[1].Id, Is.EqualTo(3)); Assert.That(domicile.AddressVersions[1].Version, Is.EqualTo(2));
是否能用EF Core实现这种关系建模?
解决方案
EF Core本身不支持直接建模这种“基于部分主键的一对多(获取所有同id的版本)”的导航关系,因为导航关系通常要求关联到唯一实体。可以通过以下方式实现需求:
方式1:手动投影查询
在查询时手动关联Address的所有同id版本,不依赖导航属性:
var person = await context.Persons .Where(p => p.Id == 1 && p.Version == 2) .Select(p => new Person { Id = p.Id, Version = p.Version, Domiciles = p.Domiciles.Select(d => new Domicile { Id = d.Id, Version = d.Version, AddressId = d.AddressId, AddressVersions = context.Addresses.Where(a => a.Id == d.AddressId).ToList() }).ToList() }) .FirstOrDefaultAsync();
方式2:非映射属性+手动加载
在Domicile类中添加非映射属性,查询后手动加载关联的所有Address版本:
public class Domicile { public long Id {get; set;} public long Version {get; set;} public long AddressId {get; set;} // 非EF映射属性,用于存储所有关联版本 [NotMapped] public List<Address> AddressVersions {get; set;} } // 查询逻辑 var person = await context.Persons .Include(p => p.Domiciles) .FirstOrDefaultAsync(p => p.Id == 1 && p.Version == 2); if (person != null) { var addressIds = person.Domiciles.Select(d => d.AddressId).Distinct(); var allAddresses = await context.Addresses.Where(a => addressIds.Contains(a.Id)).ToListAsync(); foreach (var domicile in person.Domiciles) { domicile.AddressVersions = allAddresses.Where(a => a.Id == domicile.AddressId).ToList(); } }
方式3:封装查询扩展方法
把查询逻辑封装成扩展方法,简化重复调用:
public static async Task<Person> GetPersonWithAllAddressVersionsAsync(this DbContext context, long personId, long version) { var person = await context.Persons .Include(p => p.Domiciles) .FirstOrDefaultAsync(p => p.Id == personId && p.Version == version); if (person == null) return null; var addressIds = person.Domiciles.Select(d => d.AddressId).Distinct(); var addresses = await context.Addresses.Where(a => addressIds.Contains(a.Id)).ToListAsync(); foreach (var domicile in person.Domiciles) { domicile.AddressVersions = addresses.Where(a => a.Id == domicile.AddressId).ToList(); } return person; } // 使用示例 var targetPerson = await context.GetPersonWithAllAddressVersionsAsync(1, 2);
内容的提问来源于stack exchange,提问作者Gwinn
相关产品推荐
相关产品推荐

