使用 entity framework 根据对象类型从两个表之一传递外键

Pass foreign key from one of two tables based on object type using entity framework

我有两种对象类型,MovieEpisode。两者都实现接口 ITitle 并连接到一个或多个 MediaFile 对象。

3 个相关的 table 是 MoviesEpisodesMediaFiles

我正在尝试传递 MovieId(来自 Movies table)或 EpisodeId(来自 Episodes table) 到 MediaFile 对象,具体取决于它是什么类型的媒体。

Movie/EpisodeMediaFile 都必须能够通过一次数据库写入创建。所以我不能添加 Movie/Episode 然后查询数据库来检查它是什么类型的媒体。还必须可以将 MediaFile 添加到现有 ITitleITitle 的每个实现都可以有许多与之关联的 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 MovieEpisode 继承,但是这将两者分组到相同的 table,这是不是我想要的。

我还尝试使用 2 个字段,MovieIdEpisodeId,而不是单个 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; }
}

我唯一的其他想法是添加某种带有字段 EpisodeOrMovieIdTitleType 的中间体 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 中更好 [=7​​1=]s。

后一种方法通常称为存储库模式:您所知道的是存储库能够永久存储一些数据。您可以存储数据、检索数据、更新数据和删除数据。它是如何存储的(数据库?CSV-file?XML?Json?)是隐藏的,也是 table 的结构。存储库是数据库的适配器,以适应您对数据库的实际使用。

这样做的好处是您可以更改 table 的内部结构而无需更改存储库的用户。您可以提供内部需要(组)加入 table 的结构,而用户不知道它,并且它使使用您的存储库的软件测试更容易:您不需要真正的数据库来实现您的存储库.

你对两个外键的尴尬是正常的。但是,如果您说某个项目与 table A 或 table B 中的任一元素相关,那么您有两个外键,这在数据库中很常见。

一个解决方案是,从 MoviesEpisodes 中提取 ITitle 的值,并将它们放在单独的 Titles table. Movies 将有一个指向其 Title 的外键,Episodes 将有一个指向其 Title 的外键。

这是否是一个好的解决方案取决于您最常执行的数据库操作。可以预料,数据库中的字段比查询的更改要少得多。原因是通常在操作员键入更改的数据后才进行更改。所以我们将专注于查询。

您还想查询什么:

  • 给我 MediaFiles 与他们的电影和剧集与他们的标题...
  • 给我所有带有标题的电影媒体文件...,不用担心剧集
  • 给我所有的媒体文件,包括他们的电影/剧集,没有他们的标题……(不太可能)

关于电影或剧集可以询问类似的问题:您是否想要标题?

如果您总是想在查询中使用标题,并且您会有一个单独的标题 table,那么每个涉及电影或剧集的查询都需要与标题进行额外连接 table .

使用两个外键的解决方案似乎更有效:一旦从数据库中获得电影,您就已经拥有了标题。当请求 Movie MediaFiles 时,是一个包含两个 table 的 Join,而不是三个。剧集媒体文件类似。如果您想要电影和剧集的 MediaFiles ...,它始终是三个 table 的连接,而不是四个。

可能是您访问数据的入口是基于标题的:

  • 给我所有以...开头或包含单词...的标题
  • 给定一些标题,给我所有带有该标题的电影和剧集

如果这种查询需要快速:运算符查询标题,选择标题后开始搜索具有该标题的电影或剧集,那么单独的标题 table 会更快。

在现代,存储不再是问题,处理能力才是极限。因此,在决定是将您的 Title 属性放在单独的 table 中,还是作为 Movies 和 Episodes 中的属性时,您应该首先问自己:我最常问哪种查询?什么样的查询会产生很大的结果?运营商会迫不及待地等待什么样的查询?

我猜起点是标题,所以尝试优化这些。