使用 entity framework 根据对象类型从两个表之一传递外键
Pass foreign key from one of two tables based on object type using entity framework
我有两种对象类型,Movie
和 Episode
。两者都实现接口 ITitle
并连接到一个或多个 MediaFile
对象。
3 个相关的 table 是 Movies
、Episodes
和 MediaFiles
。
我正在尝试传递 MovieId
(来自 Movies
table)或 EpisodeId
(来自 Episodes
table) 到 MediaFile
对象,具体取决于它是什么类型的媒体。
Movie
/Episode
和 MediaFile
都必须能够通过一次数据库写入创建。所以我不能添加 Movie
/Episode
然后查询数据库来检查它是什么类型的媒体。还必须可以将 MediaFile
添加到现有 ITitle
。 ITitle
的每个实现都可以有许多与之关联的 MediaFile
对象。 MediaFile
个对象依赖于 ITitle
个对象,没有它们就无法创建。
起初我尝试在 MediaFile
中传递 virtual ITitle
来获取外键,如下所示:
public class Episode : ITitle {
[Key]
public int EpisodeId { get; set; }
public string Name { get; set; }
public int TmdbId { get; set; }
public int EpisodeNumber { get; set; }
public string Description { get; set; }
public string PosterUrl { get; set; }
public bool Played { get; set; }
public double Progress { get; set; }
public TitleType TitleType { get; set; }
// Foreign key from Seasons table
public virtual Season Season { get; set; }
[ForeignKey("Season")]
public int SeasonId { get; set; }
}
public class Movie : ITitle {
[Key]
public int MovieId { get; set; }
public string Name { get; set; }
public int TmdbId { get; set; }
public string Description { get; set; }
public string PosterUrl { get; set; }
public bool Played { get; set; }
public double Progress { get; set; }
public TitleType TitleType { get; set; }
}
public enum TitleType {
Other,
Episode,
Movie,
Featurette,
}
public interface ITitle {
public bool Played { get; set; }
public double Progress { get; set; }
public TitleType TitleType { get; set; }
}
public class MediaFile{
[Key]
public int MediaFileId { get; set; }
public string Filepath { get; set; }
public long Size { get; set; }
public string Hash { get; set; }
public TitleType TitleType { get; set; }
// Foreign key from either Movies table or Episodes table
// this does not work, throws error
public virtual ITitle MovieOrEpisode { get; set; }
[ForeignKey("TODO TableName")]
public ITitle EpisodeOrMovieId { get; set; }
}
尝试通过传递 virtual ITitle MovieOrEpisode
获取外键会在添加数据库迁移时导致此错误(并且没有单个 table 来检查此键):
The property 'MediaFile.MovieOrEpisode' is of an interface type ('ITitle'). If it is a navigation property manually configure the relationship for this property by casting it to a mapped entity type, otherwise ignore the property using the NotMappedAttribute or 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
然后我尝试制作一个抽象的 class Title
Movie
和 Episode
继承,但是这将两者分组到相同的 table,这是不是我想要的。
我还尝试使用 2 个字段,MovieId
和 EpisodeId
,而不是单个 EpisodeOrMovieId
。这给了我想要的结果,但我觉得这不是一个好习惯,因为一列应该总是 NULL
.
public class MediaFile{
[Key]
public int MediaFileId { get; set; }
public string Filepath { get; set; }
public long Size { get; set; }
public string Hash { get; set; }
public TitleType TitleType { get; set; }
// Foreign key from either Movies table or Episodes table
// this uses 2 fields where one is null and the other is used
public virtual Episode Episode { get; set; }
[ForeignKey("Episode")]
public int? EpisodeId { get; set; }
public virtual Movie Movie { get; set; }
[ForeignKey("Movie")]
public int? MovieId { get; set; }
}
我唯一的其他想法是添加某种带有字段 EpisodeOrMovieId
和 TitleType
的中间体 table,但我想尽可能避免这种情况。
我正在使用 .NET Core 3.1、Entity Framework Core 3.1(代码优先)和 SQLite 提供程序。
在Entity framework中,DbSet<...>
代表你数据库中的table,DbSet中的类型代表table中行的类型。
The non-virtual properties represent the columns in the table, the virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)
因为你的数据库 classes 代表你的 tables,这些 classes 应该是 POCO 的:只有 get/set 属性,没有像方法和接口这样的花哨的东西.
所有额外功能都应该在扩展方法中,或者在隐藏数据库结构的包装器 class 中更好 [=71=]s。
后一种方法通常称为存储库模式:您所知道的是存储库能够永久存储一些数据。您可以存储数据、检索数据、更新数据和删除数据。它是如何存储的(数据库?CSV-file?XML?Json?)是隐藏的,也是 table 的结构。存储库是数据库的适配器,以适应您对数据库的实际使用。
这样做的好处是您可以更改 table 的内部结构而无需更改存储库的用户。您可以提供内部需要(组)加入 table 的结构,而用户不知道它,并且它使使用您的存储库的软件测试更容易:您不需要真正的数据库来实现您的存储库.
你对两个外键的尴尬是正常的。但是,如果您说某个项目与 table A 或 table B 中的任一元素相关,那么您有两个外键,这在数据库中很常见。
一个解决方案是,从 Movies
和 Episodes
中提取 ITitle
的值,并将它们放在单独的 Titles
table. Movies
将有一个指向其 Title
的外键,Episodes
将有一个指向其 Title
的外键。
这是否是一个好的解决方案取决于您最常执行的数据库操作。可以预料,数据库中的字段比查询的更改要少得多。原因是通常在操作员键入更改的数据后才进行更改。所以我们将专注于查询。
您还想查询什么:
- 给我 MediaFiles 与他们的电影和剧集与他们的标题...
- 给我所有带有标题的电影媒体文件...,不用担心剧集
- 给我所有的媒体文件,包括他们的电影/剧集,没有他们的标题……(不太可能)
关于电影或剧集可以询问类似的问题:您是否想要标题?
如果您总是想在查询中使用标题,并且您会有一个单独的标题 table,那么每个涉及电影或剧集的查询都需要与标题进行额外连接 table .
使用两个外键的解决方案似乎更有效:一旦从数据库中获得电影,您就已经拥有了标题。当请求 Movie MediaFiles 时,是一个包含两个 table 的 Join,而不是三个。剧集媒体文件类似。如果您想要电影和剧集的 MediaFiles ...,它始终是三个 table 的连接,而不是四个。
可能是您访问数据的入口是基于标题的:
- 给我所有以...开头或包含单词...的标题
- 给定一些标题,给我所有带有该标题的电影和剧集
如果这种查询需要快速:运算符查询标题,选择标题后开始搜索具有该标题的电影或剧集,那么单独的标题 table 会更快。
在现代,存储不再是问题,处理能力才是极限。因此,在决定是将您的 Title 属性放在单独的 table 中,还是作为 Movies 和 Episodes 中的属性时,您应该首先问自己:我最常问哪种查询?什么样的查询会产生很大的结果?运营商会迫不及待地等待什么样的查询?
我猜起点是标题,所以尝试优化这些。
我有两种对象类型,Movie
和 Episode
。两者都实现接口 ITitle
并连接到一个或多个 MediaFile
对象。
3 个相关的 table 是 Movies
、Episodes
和 MediaFiles
。
我正在尝试传递 MovieId
(来自 Movies
table)或 EpisodeId
(来自 Episodes
table) 到 MediaFile
对象,具体取决于它是什么类型的媒体。
Movie
/Episode
和 MediaFile
都必须能够通过一次数据库写入创建。所以我不能添加 Movie
/Episode
然后查询数据库来检查它是什么类型的媒体。还必须可以将 MediaFile
添加到现有 ITitle
。 ITitle
的每个实现都可以有许多与之关联的 MediaFile
对象。 MediaFile
个对象依赖于 ITitle
个对象,没有它们就无法创建。
起初我尝试在 MediaFile
中传递 virtual ITitle
来获取外键,如下所示:
public class Episode : ITitle {
[Key]
public int EpisodeId { get; set; }
public string Name { get; set; }
public int TmdbId { get; set; }
public int EpisodeNumber { get; set; }
public string Description { get; set; }
public string PosterUrl { get; set; }
public bool Played { get; set; }
public double Progress { get; set; }
public TitleType TitleType { get; set; }
// Foreign key from Seasons table
public virtual Season Season { get; set; }
[ForeignKey("Season")]
public int SeasonId { get; set; }
}
public class Movie : ITitle {
[Key]
public int MovieId { get; set; }
public string Name { get; set; }
public int TmdbId { get; set; }
public string Description { get; set; }
public string PosterUrl { get; set; }
public bool Played { get; set; }
public double Progress { get; set; }
public TitleType TitleType { get; set; }
}
public enum TitleType {
Other,
Episode,
Movie,
Featurette,
}
public interface ITitle {
public bool Played { get; set; }
public double Progress { get; set; }
public TitleType TitleType { get; set; }
}
public class MediaFile{
[Key]
public int MediaFileId { get; set; }
public string Filepath { get; set; }
public long Size { get; set; }
public string Hash { get; set; }
public TitleType TitleType { get; set; }
// Foreign key from either Movies table or Episodes table
// this does not work, throws error
public virtual ITitle MovieOrEpisode { get; set; }
[ForeignKey("TODO TableName")]
public ITitle EpisodeOrMovieId { get; set; }
}
尝试通过传递 virtual ITitle MovieOrEpisode
获取外键会在添加数据库迁移时导致此错误(并且没有单个 table 来检查此键):
The property 'MediaFile.MovieOrEpisode' is of an interface type ('ITitle'). If it is a navigation property manually configure the relationship for this property by casting it to a mapped entity type, otherwise ignore the property using the NotMappedAttribute or 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
然后我尝试制作一个抽象的 class Title
Movie
和 Episode
继承,但是这将两者分组到相同的 table,这是不是我想要的。
我还尝试使用 2 个字段,MovieId
和 EpisodeId
,而不是单个 EpisodeOrMovieId
。这给了我想要的结果,但我觉得这不是一个好习惯,因为一列应该总是 NULL
.
public class MediaFile{
[Key]
public int MediaFileId { get; set; }
public string Filepath { get; set; }
public long Size { get; set; }
public string Hash { get; set; }
public TitleType TitleType { get; set; }
// Foreign key from either Movies table or Episodes table
// this uses 2 fields where one is null and the other is used
public virtual Episode Episode { get; set; }
[ForeignKey("Episode")]
public int? EpisodeId { get; set; }
public virtual Movie Movie { get; set; }
[ForeignKey("Movie")]
public int? MovieId { get; set; }
}
我唯一的其他想法是添加某种带有字段 EpisodeOrMovieId
和 TitleType
的中间体 table,但我想尽可能避免这种情况。
我正在使用 .NET Core 3.1、Entity Framework Core 3.1(代码优先)和 SQLite 提供程序。
在Entity framework中,DbSet<...>
代表你数据库中的table,DbSet中的类型代表table中行的类型。
The non-virtual properties represent the columns in the table, the virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)
因为你的数据库 classes 代表你的 tables,这些 classes 应该是 POCO 的:只有 get/set 属性,没有像方法和接口这样的花哨的东西.
所有额外功能都应该在扩展方法中,或者在隐藏数据库结构的包装器 class 中更好 [=71=]s。
后一种方法通常称为存储库模式:您所知道的是存储库能够永久存储一些数据。您可以存储数据、检索数据、更新数据和删除数据。它是如何存储的(数据库?CSV-file?XML?Json?)是隐藏的,也是 table 的结构。存储库是数据库的适配器,以适应您对数据库的实际使用。
这样做的好处是您可以更改 table 的内部结构而无需更改存储库的用户。您可以提供内部需要(组)加入 table 的结构,而用户不知道它,并且它使使用您的存储库的软件测试更容易:您不需要真正的数据库来实现您的存储库.
你对两个外键的尴尬是正常的。但是,如果您说某个项目与 table A 或 table B 中的任一元素相关,那么您有两个外键,这在数据库中很常见。
一个解决方案是,从 Movies
和 Episodes
中提取 ITitle
的值,并将它们放在单独的 Titles
table. Movies
将有一个指向其 Title
的外键,Episodes
将有一个指向其 Title
的外键。
这是否是一个好的解决方案取决于您最常执行的数据库操作。可以预料,数据库中的字段比查询的更改要少得多。原因是通常在操作员键入更改的数据后才进行更改。所以我们将专注于查询。
您还想查询什么:
- 给我 MediaFiles 与他们的电影和剧集与他们的标题...
- 给我所有带有标题的电影媒体文件...,不用担心剧集
- 给我所有的媒体文件,包括他们的电影/剧集,没有他们的标题……(不太可能)
关于电影或剧集可以询问类似的问题:您是否想要标题?
如果您总是想在查询中使用标题,并且您会有一个单独的标题 table,那么每个涉及电影或剧集的查询都需要与标题进行额外连接 table .
使用两个外键的解决方案似乎更有效:一旦从数据库中获得电影,您就已经拥有了标题。当请求 Movie MediaFiles 时,是一个包含两个 table 的 Join,而不是三个。剧集媒体文件类似。如果您想要电影和剧集的 MediaFiles ...,它始终是三个 table 的连接,而不是四个。
可能是您访问数据的入口是基于标题的:
- 给我所有以...开头或包含单词...的标题
- 给定一些标题,给我所有带有该标题的电影和剧集
如果这种查询需要快速:运算符查询标题,选择标题后开始搜索具有该标题的电影或剧集,那么单独的标题 table 会更快。
在现代,存储不再是问题,处理能力才是极限。因此,在决定是将您的 Title 属性放在单独的 table 中,还是作为 Movies 和 Episodes 中的属性时,您应该首先问自己:我最常问哪种查询?什么样的查询会产生很大的结果?运营商会迫不及待地等待什么样的查询?
我猜起点是标题,所以尝试优化这些。