EF Core: использовать свойство словаря - PullRequest
4 голосов
/ 17 марта 2020

Есть ли способ заполнить свойство словаря с помощью 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' не может быть отображено, поскольку оно имеет тип 'Dictionary', который не является поддерживаемым типом примитива или допустимым типом объекта. Либо явным образом сопоставьте это свойство, либо проигнорируйте его, используя атрибут «[NotMapped]» или «EntityTypeBuilder.Ignore» в «OnModelCreating».

  • Попробуйте добавить преобразование значений HasConversion), но преобразование один работает только для одного элемента, а не для коллекций. HasMany уже выдает ошибку компиляции:

    builder
      .HasMany<Person>(c => c.Persons) //won't compile, Persons isn't a IEnumerable<Person>
      .WithOne().HasForeignKey("PersonId");
    
  • Создание пользовательского класса коллекции (унаследованного от Collection<T> и реализации InsertItem, SetItem et c.) - к сожалению, это также не сработает, потому что EF Core добавит элемент в коллекцию и сначала заполнит свойства (по крайней мере, нашими свойствами OwnsOne, чего нет в демонстрационном случае) - SetItem впоследствии вызываться не будет.

  • При добавлении «вычисляемого» свойства, которое будет создавать словарь, метод вызова не будет вызываться (список обновляется каждый раз с частичным значения, немного такие же, как указано выше). Смотрите try:

    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 ), но это сложно, так как я мог забыть некоторые части - и я действительно предпочитаю ошибки времени компиляции, а не ошибки времени выполнения, и декларативный стиль, а не код императивного стиля.

Обновление, чтобы быть понятным

  • это не первый подход кода
  • идея изменить отображение в EF Core, поэтому база данных не изменяется. - Я не помечал базу данных специально;)
  • Если я использую List вместо словаря, отображение работает
  • Это отношение 1: n или n: m в база данных (см. HasMany - WithOne)

Ответы [ 5 ]

3 голосов
/ 17 апреля 2020

Я не думаю, что сохранение словаря является хорошей идеей (я даже не могу представить, как это будет сделано в базе данных). Как я вижу из вашего исходного кода, вы используете 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;
    }
}

Обратите внимание, что это будет означать, что ваш набор сотрудников станет несинхронизированным после добавления / удаления в / из словаря. Для внесения изменений в syn c вы должны переписать метод 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();
        }
    }
}

EDIT: Чтобы иметь работающий код, вы нужно будет указать EF не отображать словарь через атрибут NotMapped.

[NotMapped]
public Dictionary<string, Person> PeopleDictionary { ... }
0 голосов
/ 21 апреля 2020

Я не знаю, решит ли это проблему или нет, но когда я попытался запустить предоставленный вами код. это вызвало ошибку во время выполнения, из-за которой мне пришлось изменить объявление свойства Persons на подобное

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

, это устранило ошибку времени выполнения, и все пошло нормально.

0 голосов
/ 18 апреля 2020

Вы можете добавить новое свойство PersonsJson для хранения данных JSON. Он автоматически сериализует или десериализует данные 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; }
}
0 голосов
/ 16 апреля 2020

Кажется, кто-то боролся с этим и нашел решение. См .: Сохранение словаря в виде строки JSON с использованием 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 не учитывает тот факт, что словарь был обновлен, поэтому вам потребуется явно вызвать метод Update в DbSet <>, чтобы установить сущность для изменения в трекере изменений.

0 голосов
/ 15 апреля 2020
  1. Создание частичного класса типа, сгенерированного EF.
  2. Создание класса-оболочки, который содержит словарь или реализует IDictionary.
  3. Реализуйте функцию Add, чтобы она также добавляла значение в список, который использует EF.
  4. При первом вызове метода, работающего со списком лиц или словарем, убедитесь, что они правильно инициализированы

В результате вы получите что-то вроде:

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
  • если вы собираетесь получить доступ к списку лиц напрямую, вам также необходимо изменить частичный класс, чтобы добавление в список добавляло словарь (возможно, с использованием оболочки для списка персон или класса оболочки всех вместе)
  • есть некоторые улучшения, которые нужно сделать, если вы имеете дело с большими наборами данных или нуждаетесь в оптимизации, например, не удаляя / повторно добавляя все элементы при установке нового словаря
  • , вам может потребоваться реализовать другие события и пользовательские логи c в зависимости от ваших требований
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...