Entity Framework Core 无键导航问题

6

我正在为我的公司建立一个概念验证。我们有一个现有的平台,将逐步使用现代技术进行替换。目前,我只能使用我们现有的数据库,其中有几个无关键字的表格。我正在构建一个Blazor WebAssembly应用程序,该应用程序使用gRPC调用到一个.NET Core Web应用程序。我的问题是我正在使用EF Core与现有数据库通信,但在调用DbContext.Attach(shipment)时出现异常。

"The navigation '' cannot be added because it targets the keyless entity type 'Document'. Navigations can only target entity types with keys."
Document是实体类在DbContext中被 Attach 的一个导航属性。 Shipment是一个非常庞大且具有多层结构的数据结构。是否有一种避免在模型结构中为每个实体设置实体状态(DbContext.Entry(entity).State = EntityState.Added)的方法?
或者可能将DbSet的CRUD操作映射到存储过程中? 编辑 这里是一个极度缩短版本的模式,但这就是问题所在的全部内容。
public class Shipment
{
    public int Id { get; set; }
    // 60 other properties

    public virtual ICollection<Document> Documents { get; set; }
}

public class Document
{
    public int ShippingId { get; set; }
    public string DocumentType { get; set; }
    public byte[] Content { get; set; }

    public virtual Shipment Shipment { get; set; }
}

当我尝试将Shipment实体附加到DbContext或手动设置其状态时,我会收到上面的异常。
再次说明,我现在无法更改数据库架构,并且有许多其他与Document类似但没有关键字或添加关键字的模型。
编辑2:
我意识到这需要进一步解释。显然,DbContext不能插入/更新/删除,因为没有键,它不知道如何操作。我只需要在获取Shipping实体并设置导航属性Documents时能够读取即可。但是,在保存Shipping时,完全忽略导航属性。

1
你有模式或数据库图表可以分享吗?我很难理解你想要一个无键表的导航属性如何工作。 - Ian Newson
考虑阅读《无键实体类型》文档。它们有许多限制,基本上只能用于数据读取,而不能用于添加/更新/删除。即只有CRUD中的R部分。 - Ivan Stoev
正如错误提示所示,向您的文档实体添加一个键。您可以使用数据注释 [Key] 将属性标识为主键。 - LinkedListT
正如我之前多次提到的那样,我无法更改数据库模式。@LinkedListT - Paul - Soura Tech LLC
为了确保我理解正确,我需要向“Document”模型添加一个属性,并告诉“DbContext”它是PK,即使该新属性在数据库中没有后备字段? - Paul - Soura Tech LLC
显示剩余2条评论
2个回答

4
尝试证明 @jdweng 错误时,我发现当实体模型没有在数据库中有后备字段时,我经常遇到的异常。于是我找到了这个解决方案。
我为所有无键实体添加了 public Guid Id { get; set; } = Guid.NewGuid(); ,然后使用 ModelBuilderentity.HasNoKey(); 替换为 entity.HasAlternateKey(x => x.Id);HasAlternateKey 的摘要:如果此实体类型上不存在指定属性的备用键,则在模型中创建一个备用键。这将强制属性为只读。
这正是我所期望的。

如何将新属性“Id”添加到模型中,而不需要在“EntityTypeBuilder”中排除它的映射,例如builder.Ignore(e => e.Id) - user3231784
@user3231784,你的语法有些混乱。你是想要将Id排除在数据库映射之外,还是不想排除? - Paul - Soura Tech LLC
抱歉语法不好。我有类似的问题,我理解您已将新属性 Id 添加到模型类以定义键。但是无键数据库表不包含此列。如果是这样,我认为需要 entity.Ignore(e => e.Id)?我想知道在这种情况下是否可以定义 entity.HasAlternateKey(e => e.Id)?但也许我误解了。 - user3231784
1
@user3231784,您不需要调用entity.Ignore(e => e.Id),因为entity.HasAlternateKey(e => e.Id)已经包含了这个操作。但是,这会使对象完全只读。 - Paul - Soura Tech LLC
我的问题是我试图将一个对象保存到数据库中,但它包含了一组无键对象。EF 没有办法确定子对象是新的还是已经存在,因此它抛出了异常。使用 entity.HasAlternateKey(e => e.Id),子对象被完全忽略。不会被保存或更新。 - Paul - Soura Tech LLC
当我尝试使用这个解决方案时,我遇到了这个异常:Microsoft.Data.SqlClient.SqlException (0x80131904): Invalid column name 'Id'。但我在使用dotnet 6,所以有些东西可能已经改变了。 - dnanon

0
如果Document类反映了文档表的完整模式,则您无法(也可能不应该)尝试通过EF导航属性关联文档。虽然架构可以完美地从一个没有主键的表的单向关系中运行,但EF不能以这种方式管理关系。您如何期望区分一个文档与另一个文档?系统实际上可能永远不需要这样做,但作为设计用于可靠加载和管理数据的ORM,这是必须的。
通常,在表没有主键时,您可以通过定义Key来满足EF,只要有足够的字段来唯一区分记录即可。例如ShippingId + CreatedAt,如果需要可能还包括CreatedBy。但是,根据您概述的模式,只有二进制内容,它无法成为Key的一部分。
如果您无法调整模式以引入无意义的Key(所有表都应该有PK),那么我会说您唯一的选择是根据EF的要求删除关联,并完全单独加载Shipment文档。对于很少使用的大型二进制数据,具有导航属性可能是潜在危险的,因为尝试包含它们或者像序列化器这样的东西意外地触发其惰性加载代理可能会瘫痪一个系统。

我同意你所说的一切,并认为这最终是正确的。然而,在“Shipment”实体内或更深层嵌套中有数十个导航属性。因此,我希望在读取时有一种方法可以获取所有这些值,以避免手动进行数十次额外调用来加载所有只读字段。 - Paul - Soura Tech LLC
只要相关实体具有主键,或者至少具有足够的字段来唯一标识一行,并且可以在EF中配置为键,则可以利用导航属性。只有像文档这样缺乏标识符的实体需要单独处理。如果这些类型的表格很常见,我会探索为什么不允许模式修改。您无需向表格添加主键即可满足此要求,只需添加一个唯一(最好是索引)的标识列,EF即可将其映射为键。大多数数据库都可以自动追加和填充这些内容。 - Steve Py
模式修改不是立即可行的选择,因为我们有数百个客户,全球范围内这种更改将过于繁重。 - Paul - Soura Tech LLC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接