EF Core:使用字典 属性
EF Core: use a dictionary property
有没有办法用 Entity Framework 核心填充字典 属性?
出于性能原因,我们喜欢在应用程序而不是数据库中进行搜索。由于列表不能很好地扩展,我们喜欢使用字典。
例如(简化示例)
class Course
{
public Dictionary<string, Person> Persons { get; set; }
public int Id { get; set; }
}
class Person
{
public string Firstname { get; set; }
public string Lastname { get; set; }
}
我尝试过的东西
- 天真地只是添加一个字典属性。这将导致以下错误:
System.InvalidOperationException: The property 'Persons' could not be mapped, because it is of type 'Dictionary' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
尝试添加一个 value conversion(使用 HasConversion
),但转换一仅适用于单个项目而不适用于集合。 HasMany
已经给出编译错误:
builder
.HasMany<Person>(c => c.Persons) //won't compile, Persons isn't a IEnumerable<Person>
.WithOne().HasForeignKey("PersonId");
创建自定义集合class(继承自Collection<T>
并实现InsertItem
、SetItem
等)——不幸的是这也不会工作,因为 EF Core 会将项目添加到集合中,然后首先填充属性(至少使用我们的 OwnsOne 属性,这不在演示案例中)- SetItem
之后不会被调用。
添加一个 "computed" 属性 来构建字典,setter 不会被调用(列表每次都会更新部分值,和上面有点一样)。见尝试:
class Course
{
private Dictionary<string, Person> _personsDict;
public List<Person> Persons
{
get => _personsDict.Values.ToList();
set => _personsDict = value.ToDictionary(p => p.Firstname, p => p); //never called
}
public int Id { get; set; }
}
当然我可以在存储库中构建一个字典(使用 Repository pattern),但这很棘手,因为我可能会忘记一些部分——而且我真的更喜欢编译时错误而不是 运行-时间命令式代码的错误和声明式风格。
更新,要清楚
- 这不是代码优先方法
- 更改 EF Core 中的映射的想法,因此不会更改数据库。 - 我没有故意标记数据库 ;)
- 如果我使用
List
而不是字典,则映射有效
- 它是数据库中的 1:n 或 n:m 关系(参见 HasMany - WithOne)
- 创建由 EF 生成的类型的部分 class。
- 创建一个包含字典或实现 IDictionary 的包装器 class。
- 实现 Add 函数,以便它也将值添加到 EF 使用的列表中。
- 第一次调用对 Persons 列表或字典进行操作的方法时,请确保它们已正确初始化
你最终会得到这样的结果:
private class PersonsDictionary
{
private delegate Person PersonAddedDelegate;
private event PersonAddedDelegate PersonAddedEvent; // there can be other events needed too, eg PersonDictionarySetEvent
private Dictionary<string, Person> dict = ...
...
public void Add(string key, Person value)
{
if(dict.ContainsKey(key))
{
// .. do custom logic, if updating/replacing make sure to either update the fields or remove/re-add the item so the one in the list has the current values
} else {
dict.Add(key, value);
PersonAddedEvent?.(value); // your partial class that holds this object can hook into this event to add it its list
}
}
// ... add other methods and logic
}
public partial class Person
{
[NotMapped]
private Dictionary<string, Person> _personsDict
[NotMapped]
public PersonsDictionary<string, Person> PersonsDict
{
get
{
if(_personsDict == null) _personsDict = Persons.ToDictionary(x => x.FirstName, x => x); // or call method that takes list of Persons
return _personsDict;
}
set
{
// delete all from Persons
// add all to Persons from dictionary
}
}
}
public List<Person> Persons; // possibly auto-generated by entity framework and located in another .cs file
- 如果您要直接访问人员列表,那么您还需要修改您的部分 class,以便添加到列表中将添加到字典中(可能使用人员列表的包装器或包装器 class 一起)
- 如果处理大型数据集或需要优化,则需要进行一些改进,例如设置新字典时不deleting/re-adding所有元素
- 您可能需要根据您的要求实施其他事件和自定义逻辑
似乎有人一直在努力解决这个问题并找到了解决方案。参见:Store a Dictionary as a JSON string using EF Core 2.1
The definition of the entity is as follows:
public class PublishSource
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public Dictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
}
In the OnModelCreating method of the database context I just call HasConversion, which does the serialization and deserialization of the dictionary:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PublishSource>()
.Property(b => b.Properties)
.HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
}
One important thing I have noticed, however, is that when updating the entity and changing items in the dictionary, the EF change tracking does not pick up on the fact that the dictionary was updated, so you will need to explicitly call the Update method on the DbSet<> to set the entity to modified in the change tracker.
我不认为保存字典是个好主意(我什至无法想象它在数据库中是如何完成的)。正如我从您的源代码中看到的那样,您正在使用 FirstName 作为键。在我看来,您应该将字典更改为 HashSet。这样您既可以保持速度,又可以将其保存到数据库中。
这是一个例子:
class Course
{
public Course() {
this.People = new HashSet<Person>();
}
public ISet<Person> People { get; set; }
public int Id { get; set; }
}
之后您可以从中创建字典,或者继续使用哈希集。字典示例:
private Dictionary<string, Person> peopleDictionary = null;
public Dictionary<string, Person> PeopleDictionary {
get {
if (this.peopleDictionary == null) {
this.peopleDictionary = this.People.ToDictionary(_ => _.FirstName, _ => _);
}
return this.peopleDictionary;
}
}
请注意,这意味着您的人物集在您 add/remove to/from 词典之后变得不同步。为了使更改同步,您应该在您的上下文中覆盖 SaveChanges 方法,如下所示:
public override int SaveChanges() {
this.SyncPeople();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess) {
this.SyncPeople();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
this.SyncPeople();
return base.SaveChangesAsync(cancellationToken);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) {
this.SyncPeople();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void SyncPeople() {
foreach(var entry in this.ChangeTracker.Entries().Where(_ = >_.State == EntityState.Added || _.State == EntityState.Modified)) {
if (entry.Entity is Course course) {
course.People = course.PeopleDictionary.Values.ToHashSet();
}
}
}
编辑: 为了获得 运行 代码,您需要通过 NotMapped 属性告诉 EF 不要映射字典。
[NotMapped]
public Dictionary<string, Person> PeopleDictionary { ... }
您可以添加一个新的 属性 PersonsJson
来存储 JSON 数据。当从 DB 中检索数据或将数据存储到 DB 时,它会自动将 JSON 数据序列化或反序列化为 Persons
属性。 Persons
属性 未映射,仅映射 PersonsJson
。
class Course
{
[NotMapped]
public Dictionary<string, Person> Persons { get; set; }
public string PersonsJson
{
get => JsonConvert.SerializeObject(Persons);
set => Persons = JsonConvert.DeserializeObject<Dictionary<string, Person>>(value);
}
public int Id { get; set; }
}
我不知道这是否能解决问题,但是当我尝试 运行 您提供的代码时。它触发了一个 运行 时间错误,需要我修改 Persons
属性 声明,使其像这样
public Dictionary<string, Person> Persons { get; set; } = new Dictionary<string, Person>();
这消除了 运行时间错误,一切都很顺利。
有没有办法用 Entity Framework 核心填充字典 属性?
出于性能原因,我们喜欢在应用程序而不是数据库中进行搜索。由于列表不能很好地扩展,我们喜欢使用字典。
例如(简化示例)
class Course
{
public Dictionary<string, Person> Persons { get; set; }
public int Id { get; set; }
}
class Person
{
public string Firstname { get; set; }
public string Lastname { get; set; }
}
我尝试过的东西
- 天真地只是添加一个字典属性。这将导致以下错误:
System.InvalidOperationException: The property 'Persons' could not be mapped, because it is of type 'Dictionary' which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
尝试添加一个 value conversion(使用
HasConversion
),但转换一仅适用于单个项目而不适用于集合。HasMany
已经给出编译错误:builder .HasMany<Person>(c => c.Persons) //won't compile, Persons isn't a IEnumerable<Person> .WithOne().HasForeignKey("PersonId");
创建自定义集合class(继承自
Collection<T>
并实现InsertItem
、SetItem
等)——不幸的是这也不会工作,因为 EF Core 会将项目添加到集合中,然后首先填充属性(至少使用我们的 OwnsOne 属性,这不在演示案例中)-SetItem
之后不会被调用。添加一个 "computed" 属性 来构建字典,setter 不会被调用(列表每次都会更新部分值,和上面有点一样)。见尝试:
class Course { private Dictionary<string, Person> _personsDict; public List<Person> Persons { get => _personsDict.Values.ToList(); set => _personsDict = value.ToDictionary(p => p.Firstname, p => p); //never called } public int Id { get; set; } }
当然我可以在存储库中构建一个字典(使用 Repository pattern),但这很棘手,因为我可能会忘记一些部分——而且我真的更喜欢编译时错误而不是 运行-时间命令式代码的错误和声明式风格。
更新,要清楚
- 这不是代码优先方法
- 更改 EF Core 中的映射的想法,因此不会更改数据库。 - 我没有故意标记数据库 ;)
- 如果我使用
List
而不是字典,则映射有效 - 它是数据库中的 1:n 或 n:m 关系(参见 HasMany - WithOne)
- 创建由 EF 生成的类型的部分 class。
- 创建一个包含字典或实现 IDictionary 的包装器 class。
- 实现 Add 函数,以便它也将值添加到 EF 使用的列表中。
- 第一次调用对 Persons 列表或字典进行操作的方法时,请确保它们已正确初始化
你最终会得到这样的结果:
private class PersonsDictionary
{
private delegate Person PersonAddedDelegate;
private event PersonAddedDelegate PersonAddedEvent; // there can be other events needed too, eg PersonDictionarySetEvent
private Dictionary<string, Person> dict = ...
...
public void Add(string key, Person value)
{
if(dict.ContainsKey(key))
{
// .. do custom logic, if updating/replacing make sure to either update the fields or remove/re-add the item so the one in the list has the current values
} else {
dict.Add(key, value);
PersonAddedEvent?.(value); // your partial class that holds this object can hook into this event to add it its list
}
}
// ... add other methods and logic
}
public partial class Person
{
[NotMapped]
private Dictionary<string, Person> _personsDict
[NotMapped]
public PersonsDictionary<string, Person> PersonsDict
{
get
{
if(_personsDict == null) _personsDict = Persons.ToDictionary(x => x.FirstName, x => x); // or call method that takes list of Persons
return _personsDict;
}
set
{
// delete all from Persons
// add all to Persons from dictionary
}
}
}
public List<Person> Persons; // possibly auto-generated by entity framework and located in another .cs file
- 如果您要直接访问人员列表,那么您还需要修改您的部分 class,以便添加到列表中将添加到字典中(可能使用人员列表的包装器或包装器 class 一起)
- 如果处理大型数据集或需要优化,则需要进行一些改进,例如设置新字典时不deleting/re-adding所有元素
- 您可能需要根据您的要求实施其他事件和自定义逻辑
似乎有人一直在努力解决这个问题并找到了解决方案。参见:Store a Dictionary as a JSON string using EF Core 2.1
The definition of the entity is as follows:
public class PublishSource
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public Dictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
}
In the OnModelCreating method of the database context I just call HasConversion, which does the serialization and deserialization of the dictionary:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PublishSource>()
.Property(b => b.Properties)
.HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
}
One important thing I have noticed, however, is that when updating the entity and changing items in the dictionary, the EF change tracking does not pick up on the fact that the dictionary was updated, so you will need to explicitly call the Update method on the DbSet<> to set the entity to modified in the change tracker.
我不认为保存字典是个好主意(我什至无法想象它在数据库中是如何完成的)。正如我从您的源代码中看到的那样,您正在使用 FirstName 作为键。在我看来,您应该将字典更改为 HashSet。这样您既可以保持速度,又可以将其保存到数据库中。 这是一个例子:
class Course
{
public Course() {
this.People = new HashSet<Person>();
}
public ISet<Person> People { get; set; }
public int Id { get; set; }
}
之后您可以从中创建字典,或者继续使用哈希集。字典示例:
private Dictionary<string, Person> peopleDictionary = null;
public Dictionary<string, Person> PeopleDictionary {
get {
if (this.peopleDictionary == null) {
this.peopleDictionary = this.People.ToDictionary(_ => _.FirstName, _ => _);
}
return this.peopleDictionary;
}
}
请注意,这意味着您的人物集在您 add/remove to/from 词典之后变得不同步。为了使更改同步,您应该在您的上下文中覆盖 SaveChanges 方法,如下所示:
public override int SaveChanges() {
this.SyncPeople();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess) {
this.SyncPeople();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
this.SyncPeople();
return base.SaveChangesAsync(cancellationToken);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) {
this.SyncPeople();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void SyncPeople() {
foreach(var entry in this.ChangeTracker.Entries().Where(_ = >_.State == EntityState.Added || _.State == EntityState.Modified)) {
if (entry.Entity is Course course) {
course.People = course.PeopleDictionary.Values.ToHashSet();
}
}
}
编辑: 为了获得 运行 代码,您需要通过 NotMapped 属性告诉 EF 不要映射字典。
[NotMapped]
public Dictionary<string, Person> PeopleDictionary { ... }
您可以添加一个新的 属性 PersonsJson
来存储 JSON 数据。当从 DB 中检索数据或将数据存储到 DB 时,它会自动将 JSON 数据序列化或反序列化为 Persons
属性。 Persons
属性 未映射,仅映射 PersonsJson
。
class Course
{
[NotMapped]
public Dictionary<string, Person> Persons { get; set; }
public string PersonsJson
{
get => JsonConvert.SerializeObject(Persons);
set => Persons = JsonConvert.DeserializeObject<Dictionary<string, Person>>(value);
}
public int Id { get; set; }
}
我不知道这是否能解决问题,但是当我尝试 运行 您提供的代码时。它触发了一个 运行 时间错误,需要我修改 Persons
属性 声明,使其像这样
public Dictionary<string, Person> Persons { get; set; } = new Dictionary<string, Person>();
这消除了 运行时间错误,一切都很顺利。