英文:
EF Core Inheritence: Using same FK column for multiple relations
问题
我目前正在开发一个票务系统,在这个系统中,应该可以将文档链接到票务。有不同类型的票务,每种票务都与其自己的文档表相关联。文档表本身存储有关文档的一些元信息,并与票务本身存在关系。元信息是相同的,所以唯一的区别在于不同的关联。
问题是,票务类型的数量正在增加,因此我的文档表也在增加。为了减少需要更多表格的需求,我想将所有文档合并到一个表格中,并且同时使将来添加新文档变得容易。
我已经研究了使用继承,具体来说是Table-per-hierarchy方法,但我有点卡住了,希望有人可以指导我朝正确的方向前进。
// 这是我的主要类,其中定义了所有共享属性
// 我还计划将其用于仅与客户而不是票务相关联的文档
public class CustomerDocumentsModel : EntityBase
{
[Required]
[Column("customer_id")]
[MaxLength(24)]
public string CustomerId { get; set; }
[Required]
[Column("file_name")]
[MaxLength(500)]
public string FileName { get; set; }
// ... 其他共享属性
}
// 我也在OnModelCreating中设置了这个
modelBuilder.Entity<CustomerDocumentsModel>().HasDiscriminator<string>("document_type");
// 这是我的通用类,应该将文档关联到其他表格
public class CustomerDocumentType<T> : CustomerDocumentsModel
{
public virtual T ExternalEntity { get; set; }
[Column("fk_external_id")]
[ForeignKey(nameof(ExternalEntity))]
public int? FKExternalId { get; set; }
[Required]
[Column("external_type")]
public string ExternalType { get; set; } = typeof(T).Name;
}
// 我用以下方式创建通用表格与票务关联
public class CustomerDocumentsAML : CustomerDocumentType<AMLTicketModel> { }
public class CustomerDocumentsDisputes : CustomerDocumentType<DisputeTicketModel> { }
到目前为止一切顺利。我可以创建迁移并将其应用到我的数据库,但问题出现在这里。当我尝试插入不同类型的文档(除了与特定票务无关的文档之外),我会违反外键约束。
造成这个问题的原因,正如你们中的一些人可能已经猜到的,是因为我重用了所有不同类型文档的相同FK列。因此,当我尝试连接AMLTicketModel中的9001时,会出现错误,因为在DisputeTicketModel中没有相应的ID。这是我开始意识到我可能正在实施一种反模式,但从代码角度来看,这种方法太精炼了,不想放弃它。
我搜索了网络,最接近解决我的问题的两种方法是:
- 为每种类型使用唯一的FK列
尽管在负载较低的情况下,这种方法相对容易实现,但会导致我的文档表格迅速拥有许多空列。从性能的角度来看可能不是太糟糕,但很可能会导致直接查询它的任何人视觉疲劳。
- 忽略FK的使用,并在票务中建立连接
所以,不是将我的列定义为FK,而是像这样建立连接:
modelBuilder.Entity<AMLTicketModel>()
.HasMany(x => x.Documents)
.WithOne()
.HasPrincipalKey(x => x.Id)
.HasForeignKey(y => y.ExternalId);
这将导致每次添加新类型时都需要大量工作,因为所有连接都必须手动完成。还会导致我无法直接获取文档并从中获取相关联的票务(而不需要进行更多手动操作)。
我非常需要有条理且灵活的代码,所以是否有人有聪明的主意可以解决我的问题?如果没有,我将不得不随机选择上述其中一种方法,所以请帮忙:(
英文:
I'm currently working on a ticket system where it should be possible to link documents to the ticket. There are different types of tickets, and each has it's own document table connected to it. The document table itself stores some meta information about the document as well as having a relation to the ticket itself. The meta information is the same, so the only difference is the different relations.
The issue that is rising is the number of ticket types are growing, hence also my document tables. To limit the need for more tables, I would like to merge all documents into one table and at the same time making it easy to add new ones in the future.
I've looked into using inheritance, more specifically Table-per-hierarchy approach, but I've got a bit stuck and are hoping someone can guide me in the right direction.
// This is my main class where all shared properties are defined
// I also plan to use this for documents that are just connected to a customer and not a ticket
public class CustomerDocumentsModel : EntityBase
{
[Required]
[Column("customer_id")]
[MaxLength(24)]
public string CustomerId { get; set; }
[Required]
[Column("file_name")]
[MaxLength(500)]
public string FileName { get; set; }
// ... other shared properties
}
// I also have this setup in my OnModelCreating
modelBuilder.Entity<CustomerDocumentsModel>().HasDiscriminator<string>("document_type");
// This is my generic class that should link a documents relation to the other table
public class CustomerDocumentType<T> : CustomerDocumentsModel
{
public virtual T ExternalEntity { get; set; }
[Column("fk_external_id")]
[ForeignKey(nameof(ExternalEntity))]
public int? FKExternalId { get; set; }
[Required]
[Column("external_type")]
public string ExternalType { get; set; } = typeof(T).Name;
}
// And I create the generic tables with the ticket relation like this
public class CustomerDocumentsAML : CustomerDocumentType<AMLTicketModel> { }
public class CustomerDocumentsDisputes : CustomerDocumentType<DisputeTicketModel> { }
So far so good. I can create a migration and apply it to my db but this is where things becomes tricky. When I try to insert my different type of documents (except ones that are not connected to a specific ticket) I get a violation of foreign key constraint.
The cause of this as some of you probably have guessed is because I've reused the same FK column for all of my different type of documents. So when I try to connect a AMLTicketModel of 9001, I get an error because there are no ID that correspond in DisputeTicketModel. This is where I start to realise I might be into implementing a anti-pattern, but code wise this approach is to neat to give up on yet.
I've searched the web and the closest I have come is two options to solve my issue.
- Have a unique FK column for each type
Where this approach would be quite easy to do with low amount of overload, it would result in my document table having a lot of null columns real fast. Maybe not bad from a performance point of view, but it would most likely cause a ophthalmia for anyone querying it directly.
- Ignore the usage of FK and make the connection in the ticket
So instead of defining my column as a FK, I would instead make the connection something like this:
modelBuilder.Entity<AMLTicketModel>()
.HasMany(x => x.Documents)
.WithOne()
.HasPrincipalKey(x => x.Id)
.HasForeignKey(y => y.ExternaltId);
This would result in a lot of work every time a new type is added since all connection has to be done manually. It would also result that I can no longer fetch a document directly and get the connected ticket from that way (without doing more manual stuff)
I have quite the need of having neat and dynamic code, so does anyone have a cleaver idea how I can solve my problem? If not I'll have to pick one of the above at random, so please help
答案1
得分: 0
以下是翻译好的部分:
"So an update if anyone find this question in the future and are interested in a potential solution (if somewhat hacky).
如果将来有人发现这个问题并对潜在解决方案感兴趣(即使有点巧妙),我想提供一个更新。"
"I modified my generic class slightly and made all the connection by annotations. Creating of the models are primarily also made through the constructor so we can reuse the base constructor as well."
我稍微修改了我的通用类,并通过注释设置了所有连接。模型的创建主要也通过构造函数完成,因此我们可以重用基础构造函数。
"By using the annotations we are setting up the connection in a simply way that will also automatically be added if a new document type is added in the future. In the migration however one always needs to remember to remove the actual foreign key generation. If we remove it, no FKs will be created in the SQL, but all connections still exists in EF and can therefore still work it's magic."
通过使用注释,我们以一种简单的方式设置了连接,如果将来添加新的文档类型,它也会自动添加。但是,在迁移中,我们始终需要记住删除实际的外键生成。如果我们删除它,SQL 中将不会创建外键,但在 EF 中仍然存在所有连接,因此仍然可以发挥其作用。
"I also added a document type (enum) in my base class that is used in combination with a factory to generate the correct model when creating the object. As long as I generate the document with the correct model, EF will take care of the rest when quering."
我还在基类中添加了一个文档类型(枚举),与工厂一起使用,用于在创建对象时生成正确的模型。只要我使用正确的模型生成文档,EF 在查询时将处理其余部分。
"This surely breaks some best practices and principles, but it works. Hope you find it helpful!"
这肯定违反了一些最佳实践和原则,但它起作用。希望你会觉得有用!
英文:
So an update if anyone find this question in the future and are interested in a potential solution (if somewhat hacky).
I modified my generic class slightly and made all the connection by annotations. Creating of the models are primarily also made through the constructor so we can reuse the base constructor as well.
[Index(nameof(ExternalId), IsUnique = false)]
public abstract class CustomerDocumentsModel<T> : CustomerDocumentsModel
{
public CustomerDocumentsAMLModel() : base() { }
public CustomerDocumentsAMLModel(/* params */) : base(/* params */)
{ }
[Column("external_id")]
[ForeignKey(nameof(ExternalEntity))]
public int? ExternalId { get; set; }
public virtual T ExternalEntity { get; set; }
}
By using the annotations we are setting up the connection in a simply way that will also automatically be added if a new document type is added in the future. In the migration however one always needs to remember to remove the actual foreign key generation. If we remove it, no FKs will be created in the SQL, but all connections still exists in EF and can therefore still work it's magic.
// Remove all appearances of this
table.ForeignKey(
name: "FK_CustomerDocuments_AMLTickets_external_id",
column: x => x.external_id,
principalTable: "AMLTickets",
principalColumn: "id");
I also added a document type (enum) in my base class that is used in combination with a factory to generate the correct model when creating the object. As long as I generate the document with the correct model, EF will take care of the rest when quering.
public static class CustomerDocumentFactory
{
public static CustomerDocumentsModel Create(/* dto */)
=> dto.DocumentType switch
{
CustomerDocumentType.General =>
new CustomerDocumentsModel(/* params */),
CustomerDocumentType.AMLDocument =>
new CustomerDocumentsAMLModel(/* params */),
// ... etc
};
}
This surely breaks some best practices and principles, but it works. Hope you find it helpful!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论