如何在"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。这也不是一对多,因为 string
与 elem
没有任何关系(因为不仅 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.id
、elem.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);
我在 .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。这也不是一对多,因为 string
与 elem
没有任何关系(因为不仅 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.id
、elem.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);