如何在"many side"上创建虚拟List实现无外键的一对多引用

How to create virtual List to realize reference like one-to-many without foreign key on "many side"

我在 .NET Core 应用程序上使用 Entity Framework Core 5.0.0 和 SQLite3(Entity Framework 6.4.4 也已安装),并且有这两个 table:

CREATE TABLE string 
(
    id     INTEGER NOT NULL,
    locale TEXT    NOT NULL,
    text   TEXT    NOT NULL,
    CONSTRAINT PK_string PRIMARY KEY (id, locale)
);

CREATE TABLE elem 
(
    id      INTEGER NOT NULL PRIMARY KEY,
    name    TEXT    NOT NULL,
    titleId INTEGER NOT NULL,
    briefId INTEGER NOT NULL
);

这个方案是正确的并且有效,但是在 Entity Framework 的上下文中,这并没有完全实现 EF 的便利:在 elem table 中,结果,我只有一个整数字段,而不是 List<string>.

一方面,我这里是多对多关系,但是多对多不正确,因为没有中间table。这也不是一对多,因为 stringelem 没有任何关系(因为不仅 elem 引用 string)。

这里即使是多对多也不是最方便的,因为我必须创建几个中间 tables...

我想在 class 中有一个 List<string>,而不仅仅是一个整数字段。

有没有什么方法可以在不更改架构的情况下在 EF 中执行此操作(或进行最少的更改,不像更改来完成多对多关系)?

除非“字符串”table 有一个 ElementId 指向一个元素,否则元素不会包含“字符串”实体列表。

从您的模式来看,我想您的 titleId 和可能的 briefId 是 FK 对本地化标题和简短文本的“字符串”table 的引用? (使用“字符串”作为 table/entity 名称会导致很多混淆)

如果是这样,你会得到类似的东西:

[Table("elem")]
public class Element
{
    // ...
    public int TitleId {get; set;}
    [ForeignKey("TitleId")]
    public virtual StringEntity Title { get; set; } 

    public int BriefId {get; set;}
    [ForeignKey("BriefId")]
    public virtual StringEntity Brief { get; set; } 
}

[Table("string")]
public class StringEntity
{ // ...
}

然后获取标题的本地化资源:element.Title.Text

更新:好的,字符串资源 table 使用 ID 和语言环境的复合键。由于您加入的 table 没有语言环境 ID,因此 EF 无法直接映射这两个元素。您可以通过连接和投影到视图模型来查询这些 table 中的信息...

[Table("string")]
public class StringEntry
{
    [Column("id"), DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }
    [Column("locale"), Required]
    public string Locale { get; set; }
    [Column("text"), Required]
    public string Text { get; set; }
    // Navigation properties
    public virtual StringTable Table { get; set; }
}

[Table("elem")]
public class Element
{
    [Column("id"), DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }
    [Column("name"), Required]
    public string Name { get; set; }
    [Column("titleId")]
    public int TitleId { get; set; }
    [Column("briefId")]
    public int BriefId { get; set; }
}

给定一个像这样的视图模型:

[Serializable]
public class ElementViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    public string Brief { get; set; }
}

查询本地化数据...

int localeId = GetCurrentLocaleId(); // Some method/code to get the current locale.

var elements = context.Elements
    .Join(context.Strings, 
        x => new { Id = x.TitleId, Locale = localeId }, 
        x => new { x.Id, x.LocaleId }, 
        (e, s) => new { Element = e, Title = s })
    .Join(context.Strings, 
        x => new { Id = x.Element.BriefId, LocaleId = localeId }, 
        x => new { x.Id, x.LocaleId }, 
        (e, s) => new { Element = e.Element, Title = e.Title, Brief = s })
    .Select(x => new ElementViewModel
    { 
        Id = x.Element.Id, 
        Name = x.Element.Name,
        Title = x.Title.Text,
        Brief = x.Brief.Text 
    }).ToList();

不用说,如果您使用大量这样的本地化字符串,查询将变得相当复杂和繁琐。

所提供的数据库模型缺少一个重要的(从关系的角度来看)部分 - 代表 string.idelem.titleId、[= 的多对一关系一侧的唯一主体实体20=] 和类似的。

如果没有那部分,数据库模型就不能 use/enforce 外键关系,而这些对于“方便的”EF 关系映射是必不可少的。

所以最小的修改是引入 entity/table,例如 stringTable:

CREATE TABLE stringTable 
(
    id      INTEGER NOT NULL PRIMARY KEY
);

对于现有数据库,它应该使用来自 string table.

的不同 id 值填充

现在可以介绍FK关系了:

CREATE TABLE string 
(
    id     INTEGER NOT NULL CONSTRAINT FOREIGN KEY REFERENCES stringTable(id) ON DELETE CASCADE,
    locale TEXT    NOT NULL,
    text   TEXT    NOT NULL,
    CONSTRAINT PK_string PRIMARY KEY (id, locale)
);

CREATE TABLE elem 
(
    id      INTEGER NOT NULL PRIMARY KEY,
    name    TEXT    NOT NULL,
    titleId INTEGER NOT NULL CONSTRAINT FOREIGN KEY REFERENCES stringTable(id),
    briefId INTEGER NOT NULL CONSTRAINT FOREIGN KEY REFERENCES stringTable(id)
);

相应的 EF 实体模型将是这样的(实体类型和 属性 名称是任意的):

[Table("stringTable")]
public class StringTable
{
    [Column("id"), DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }
    // Navigation properties
    public virtual ICollection<StringEntry> Entries { get; set; }
}

[Table("string")]
public class StringEntry
{
    [Column("id"), DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }
    [Column("locale"), Required]
    public string Locale { get; set; }
    [Column("text"), Required]
    public string Text { get; set; }
    // Navigation properties
    public virtual StringTable Table { get; set; }
}

[Table("elem")]
public class Element
{
    [Column("id"), DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }
    [Column("name"), Required]
    public string Name { get; set; }
    [Column("titleId")]
    public int TitleId { get; set; }
    [Column("briefId")]
    public int BriefId { get; set; }
    // Navigation properties
    public virtual StringTable Title { get; set; }
    public virtual StringTable Brief { get; set; }
}

具有复合 PK 和关系映射:

modelBuilder.Entity<StringEntry>()
    .HasKey(e => new { e.Id, e.Locale });

modelBuilder.Entity<StringEntry>()
    .HasRequired(e => e.Table)
    .WithMany(e => e.Entries)
    .HasForeignKey(e => e.Id)
    .WillCascadeOnDelete();

modelBuilder.Entity<Element>()
    .HasRequired(e => e.Title)
    .WithMany()
    .HasForeignKey(e => e.TitleId)
    .WillCascadeOnDelete(false);

modelBuilder.Entity<Element>()
    .HasRequired(e => e.Brief)
    .WithMany()
    .HasForeignKey(e => e.BriefId)
    .WillCascadeOnDelete(false);

所有这些都准备就绪后,您可以使用 Element elem 访问关联的字符串,如下所示:

elem.Title.Entries
elem.Brief.Entries

project/extract关联文字如下:

TitleTexts = elem.Title.Entries.Select(e => e.Text)
BriefTexts = elem.Brief.Entries.Select(e => e.Text)

project/extract 特定 string locale 的文本:

TitleText = elem.Title.Entries.Where(e => e.Locale == locale).Select(e => e.Text).FirstOrDefault()
BriefText = elem.Brief.Entries.Where(e => e.Locale == locale).Select(e => e.Text).FirstOrDefault()

等等

更新: 对于 EF Core,实体 model/data 注释所需的与上面完全相同,只是流畅的配置必须使用 EF Core 等效项(所有这些转到 OnModelCreating 派生的 DbContext 方法覆盖 class):

modelBuilder.Entity<StringEntry>()
    .HasKey(e => new { e.Id, e.Locale });

modelBuilder.Entity<StringEntry>()
    .HasOne(e => e.Table)
    .WithMany(e => e.Entries)
    .HasForeignKey(e => e.Id)
    .OnDelete(DeleteBehavior.Cascade);

modelBuilder.Entity<Element>()
    .HasOne(e => e.Title)
    .WithMany()
    .HasForeignKey(e => e.TitleId)
    .OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<Element>()
    .HasOne(e => e.Brief)
    .WithMany()
    .HasForeignKey(e => e.BriefId)
    .OnDelete(DeleteBehavior.Restrict);