【问题标题】:EF Core: use a dictionary propertyEF Core:使用字典属性
【发布时间】:2020-06-28 19:18:54
【问题描述】:

有没有办法用 Entity Framework Core 填充字典属性?

出于性能原因,我们喜欢在应用程序而不是数据库中进行搜索。由于列表不能很好地扩展,我们喜欢使用字典。

例如(简化示例)

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:无法映射属性“Persons”,因为它属于“字典”类型,不是受支持的原始类型或有效的实体类型。显式映射此属性,或使用“[NotMapped]”属性或使用“OnModelCreating”中的“EntityTypeBuilder.Ignore”忽略它。

  • 尝试添加value conversion(与HasConversion),但转换仅适用于单个项目而不适用于集合。 HasMany 已经给出了编译错误:

    builder
      .HasMany<Person>(c => c.Persons) //won't compile, Persons isn't a IEnumerable<Person>
      .WithOne().HasForeignKey("PersonId");
    
  • 创建一个自定义集合类(继承自 Collection&lt;T&gt; 并实现 InsertItemSetItem 等)——不幸的是,这也不起作用,因为 EF Core 会将项目添加到集合中,然后首先这将填充属性(至少使用我们的 OwnsOne 属性,在演示案例中没有) - 之后不会调用 SetItem

  • 添加将构建字典的“计算”属性,将不会调用 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)

【问题讨论】:

  • 我的回答有帮助吗?如果不让我知道什么不起作用,我误解了什么,或者需要额外的信息/指导,我很乐意提供帮助。
  • 你不能添加一个引用表(实体),它的行为就像一个字典吗?
  • 看看这个问题:Entity Framework 4 POCO with Dictionary - Chris Moschini 的回答。
  • 这个:Store a Dictionary as a JSON string using EF Core 2.1 可能也有兴趣,看来这就是你要找的东西。
  • 字典使用哈希桶来查找实体。您可以使用 Hashset&lt;T&gt; 并覆盖实体类(Person)的 GetHashCode 和 Equals 方法以依赖于 key 属性,这将导致 similar performance。 Hashset 是受支持的集合类型,您可以避免所有麻烦。

标签: c# dictionary entity-framework-core entity-framework-core-3.1


【解决方案1】:

我不认为保存字典是一个好主意(我什至无法想象它在数据库中是如何完成的)。正如我从您的源代码中看到的那样,您使用 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;
    }
}

请注意,这意味着在您向字典添加/删除字典后,您的人员集将变得不同步。为了使更改同步,您应该在上下文中覆盖 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 { ... }

【讨论】:

  • 这是最好的解决方案,请参阅performance analysis 的答案。该链接问题的背景不同,因此他们的结论可能与您无关。
  • 我会对此进行测试,但我有点困惑:建议是使用哈希集,但之后的答案只涉及字典? (而不是哈希集)
  • 使用 hashset 时是否需要更改 savechanges?
  • 是的,因为您想在代码的其他部分使用字典(我假设),如果您只想使用 HashSet 而不是不需要字典部分:D。
  • 不,如果你只使用 hashset,那么你每次都会更新它,所以不,你不需要覆盖 savechanges()。
【解决方案2】:

似乎有人一直在努力解决这个问题并找到了解决方案。见:Store a Dictionary as a JSON string using EF Core 2.1

实体的定义如下:

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>();
}

在数据库上下文的 OnModelCreating 方法中我只调用了 HasConversion,它对字典进行序列化和反序列化:

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));
}

然而,我注意到的一件重要的事情是,在更新实体和更改字典中的项目时,EF 更改跟踪不会发现字典已更新这一事实,因此您需要显式调用DbSet 上的更新方法以将实体设置为在更改跟踪器中进行修改。

【讨论】:

  • 将字典存储为 json。这对我来说不是一个选择。 (也不是 xml)
  • @Julian,我认为没有其他办法。 Json 通常用作 asp.net 核心项目中的“传输层”。如果我理解你,你想访问一些数据而不从数据库中获取数据。如果我错了,请纠正我。
  • 不,我不能改变数据库结构。
  • @Julian,这是我之前问题的答案吗?
  • 亲爱的先生,这是一个很好的方法。谢谢。
【解决方案3】:
  1. 创建由 EF 生成的类型的部分类。
  2. 创建一个包含字典或实现 IDictionary 的包装类。
  3. 实现 Add 函数,以便它还将值添加到 EF 使用的列表中。
  4. 第一次调用对 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
  • 如果您要直接访问 Persons 列表,那么您还需要修改您的部分类,以便添加到列表中将添加到字典中(可能使用 Persons 列表的包装器或一起使用包装器类)
  • 如果处理大型数据集或需要优化,则需要进行一些改进,例如在设置新字典时不删除/重新添加所有元素
  • 您可能需要根据您的要求实现其他事件和自定义逻辑

【讨论】:

  • 感谢您的回答,但不幸的是,这也是我尝试过的。不幸的是,列表和字典会不同步,因为它们的生命周期不同。
【解决方案4】:

您可以添加一个新属性 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; }
}

【讨论】:

  • 谢谢,但是这已经被建议了,这当然不是改变数据库的想法。我喜欢调整 ORM,而不是数据库。
【解决方案5】:

我不知道这是否能解决问题,但是当我尝试运行您提供的代码时。它触发了运行时错误,需要我将Persons 属性声明修改为像这样

public Dictionary<string, Person> Persons { get; set; } = new Dictionary<string, Person>();

这消除了运行时错误,一切正常。

【讨论】:

  • 键的内容是什么?我很确定这是不可能的......
猜你喜欢
  • 2021-11-04
  • 2022-01-01
  • 2018-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-22
  • 2018-10-10
相关资源
最近更新 更多