EF: ручные (не генерируемые автоматически) ключи требуют специальной c обработки новых сущностей - PullRequest
1 голос
/ 14 июля 2020

В приложении MVVM с EF Core в качестве ORM я решил смоделировать таблицу с вручную вставленным текстовым первичным ключом.

Это потому, что в этом c приложении я Я бы предпочел использовать значащие ключи вместо бессмысленных целочисленных идентификаторов, по крайней мере, для простых таблиц "ключ-значение", таких как таблица стран мира. У меня есть что-то вроде:

 Id   | Description  
 -----|--------------------------
 USA  | United States of America  
 ITA  | Italy   
 etc. etc.

Итак, сущность:

public class Country
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public string Id { get; set; }
    public string Description { get; set; }
}

Вот моя модель просмотра. Это немного больше, чем контейнер для ObservableCollection стран. Фактически он загружается из репозитория. Это тривиально, и я добавил весь код в конце. Это не совсем актуально, и я мог бы использовать только DbContext. Но я хотел показать все слои, чтобы увидеть, к чему относится решение. О да, тогда он содержит код синхронизации, который на самом деле оскорбляет EF Core.

public class CountriesViewModel
{
    //CountryRepository normally would be injected
    public CountryRepository CountryRepository { get; set; } = new CountryRepository(new AppDbContext());
    public ObservableCollection<Country> Countries {get; set;}

    public CountriesViewModel()
    {
        Countries = new ObservableCollection<Country>();
        Countries.CollectionChanged += Countries_CollectionChanged;
    }

    private void Countries_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        foreach (Country c in e.NewItems)
        {
            CountryRepository.Add(c);
        }
    }
}

В моем MainWindow у меня просто:

 <Window.DataContext>
    <local:CountriesViewModel/>
</Window.DataContext>

<DockPanel>
    <DataGrid ItemsSource="{Binding Countries}"/>
</DockPanel>

Проблема и вопрос

Теперь это не работает. Когда мы пытаемся вставить новую запись, в этом случае я делаю это с помощью функции Automati c DataGrid, я получаю:

System.InvalidOperationException: 'Unable to track an entity of type 'Country'
because primary key property 'Id' is null.'

Каждый раз, когда я добавляю новую запись в ObservableCollection Я также пытаюсь добавить его обратно в репозиторий, который, в свою очередь, добавляет его в EF DbContext, который не принимает объекты с нулевым ключом.

Итак, какие у меня здесь варианты? Один из них - откладывание добавления новой записи до тех пор, пока не будет вставлен идентификатор. Это нетривиально, как показанная мною обработка коллекции, но проблема не в этом. Хуже всего то, что таким образом у меня будет какая-то запись, которая отслеживается EF (обновленная и удаленная и новая с назначенным pk), а некоторые отслеживаются моделью представления (новые с еще не назначенным ключом).

Другой использует альтернативные ключи; У меня был бы целочисленный автогенерированный первичный ключ, а код ITA, USA et c был бы альтернативным ключом, который также использовался бы в отношениях. Это не так уж плохо с точки зрения простоты, но я бы хотел решение только для приложений.

То, что я ищу

Я ищу здесь изящное решение, шаблон для использоваться всякий раз, когда возникает эта проблема, и это хорошо работает в контексте приложения MVVM / EF. Конечно, я мог бы также посмотреть в направлении событий просмотра, то есть заставить пользователя вставить ключ перед определенным событием, которое запускает вставку. Я бы счел это второсортным решением, потому что оно зависит от вида.

Оставшийся код

Просто для полноты, если вы хотите запустить код, вот оставшийся код .

DbContext

(Настроен для postgres)

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    optionsBuilder.UseNpgsql("Host=localhost;Database=WpfApp1;Username=postgres;Password=postgres");
    }
    public DbSet<Country> Countries { get;set; }
}

Репозиторий

Причина, по которой я реализовал репозиторий для такого простого примера: потому что я думаю, что возможным решением может быть включение управления записями без ключа в репозиторий, а не в модель просмотра. Я все еще надеюсь, что кто-нибудь предложит более простое решение.

public class CountryRepository
{
    private AppDbContext AppDbContext { get; set; }
    public CountryRepository(AppDbContext appDbContext) => AppDbContext = appDbContext;
    public IEnumerable<Country> All() => AppDbContext.Countries.ToList();
    public void Add(Country country) => AppDbContext.Add(country);

    //ususally we don't have a save here, it's in a Unit of Work; 
    //for the example purpose it's ok
    public int Save() => AppDbContext.SaveChanges(); 
}

1 Ответ

1 голос
/ 15 июля 2020

Вероятно, самый чистый способ решить вышеупомянутую проблему в EF Core - это использовать временное создание значения при добавлении . Для этого вам понадобится настраиваемый ValueGenerator вроде этого:

using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration;

public class TemporaryStringValueGenerator : ValueGenerator<string>
{
    public override bool GeneratesTemporaryValues => true; // <-- essential
    public override string Next(EntityEntry entry) => Guid.NewGuid().ToString();
}

и плавная конфигурация, подобная этой:

modelBuilder.Entity<Country>().Property(e => e.Id)
    .HasValueGenerator<TemporaryStringValueGenerator>()
    .ValueGeneratedOnAdd();

Возможные недостатки: :

  • В версиях до EF Core 3.0 сгенерированное временное значение устанавливается для экземпляра сущности, таким образом, оно будет отображаться в пользовательском интерфейсе. Это было исправлено в EF Core 3.0, поэтому теперь Временные значения ключей больше не устанавливаются на экземпляры сущностей

  • Даже если свойство выглядит пустым (нулевым) и требуется (по умолчанию для первичных / альтернативных ключей), если вы не укажете явное значение, EF Core попытается выполнить команду INSERT и считать «фактическое» значение обратно из базы данных, аналогично идентификатору и другим значениям, сгенерированным базой данных, которые в в этом случае возникнет исключительная ситуация, созданная во время выполнения, которая не является удобной для пользователя. Но EF Core, как правило, не выполняет проверки, так что все будет не так сильно - вам нужно добавить и проверить правило, требуемое для свойства на соответствующем уровне.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...