Вставка "многие ко многим" в EF Core: как избежать попытки EF добавить уже существующую подобъект с ограничением уникальности? - PullRequest
1 голос
/ 05 мая 2020

Добрый вечер: -)

У меня есть вопрос, на который я не могу ответить, относительно случая INSERT «многие ко многим» с EFCore.

У меня есть Recipe, у которого много Tag. A Tag (возьмем, например, «ужин») может быть во многих Recipe. У обоих есть своя таблица SQL.

Чтобы эта функция «многие ко многим» работала, у меня также есть объединяющая таблица SQL и связанный с ней объект RecipeTag.

Примечание №1: код классов находится в конце этого сообщения

Примечание №2: каждая операция GET / SELECT работает как шарм, реализовано и работает «многие ко многим»

Чтобы взять простой пример, начиная с fre sh пустых таблиц, я публикую свой новый Recipe:

POST /recipes
{
  "title": "A first title",
  "tags": [
    {
      "name": "dinner"
    }
  ]
}

Это хорошо. Я вижу свои новые записи в таблице Recipe, таблице Tag и RecipeTag, связывающих обе id.

Теперь я снова регистрирую новый Recipe с тем же Tag:

POST /recipes
{
  "title": "A second title",
  "tags": [
    {
      "name": "dinner"
    }
  ]
}

Здесь я получил нарушение ограничения UNIQUE key от EF Core. Он попытается зарегистрировать новый Tag с именем dinner. На самом деле это вполне нормально для исключения, поскольку я ввел уникальное ограничение в поле Tag. tagnam.

Вопрос: есть ли хороший способ обойти это? Это означает, что EF Core регистрирует вторую Recipe и присоединяющуюся RecipeTag сущность, распознавая сам Tag, но на этот раз не только ПК, а уникальным ограничением.

Кажется (и это logi c) EF Core не будет пытаться добавить объект, если увидит, что id (PK) уже находится в db. Но в моем случае: я бы хотел, чтобы EF Core также проверял уникальное ограничение, и если оно конфликтует, возьмите id конфликтующего объекта в db и lazy-put-it в моем объекте Tag, чтобы он мог (сразу после ) правильно вставьте присоединяемый объект RecipeTag с правильным id!

Извините, это немного беспорядочно ...

Единственное "неплохое" решение, которое у меня есть на данный момент :

  1. Контроллер API получает RecipeDto
  2. мы извлекаем теги из этого нового рецепта как string[]
  3. извлекаем существующие теги из базы данных, совпадающей с теми, которые мы хотели бы добавить
  4. при преобразовании RecipeDto в Recipe, поле (см. ниже) ICollection<RecipeTag> будет содержать правильный Tag с id, установленным для существующих , и некоторые объекты no-id Tag, для тех, которые нам действительно нужно добавить
  5. , как уже говорилось, EF Core распознает некоторые Tag с заполненным идентификатором и не будет пытаться вставить их в db - он просто вставит те, у которых нет идентификатора

Классы, которые я использую:

/// <summary>
/// Recipe that can contain many tags
/// </summary>
[Table("rcp")]
public class Recipe
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Column("id")]
    public Guid Id { get; set; }

    [Column("rcptit")]
    public string Title { get; set; }

    public ICollection<RecipeTag> RecipeTags { get; set; }
}

/// <summary>
/// Tag contained into many recipes
/// </summary>
[Table("tag")]
public class Tag
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Column("id")]
    public Guid Id { get; set; }

    [Column("tagnam")] // this has a UNIQUE constraint in db to avoid having 2 times the same tag
    public string Name { get; set; }
}

/// <summary>
/// Join table between Recipes and Tags
/// </summary>
[Table("rcptag")]
public class RecipeTag
{
    [Column("rcpid")]
    public Guid RecipeId { get; set; }

    [Column("tagid")]
    public Guid TagId { get; set; }

    public Recipe Recipe { get; set; }
    public Tag Tag { get; set; }
}

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    { }

    public DbSet<Recipe> Recipes { get; set; }
    public DbSet<Tag> Tags { get; set; }
    public DbSet<RecipeTag> RecipeTags { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.HasPostgresExtension("uuid-ossp");

        builder.Entity<Tag>().HasIndex(e => e.Name).IsUnique();
        // composite primary key for RecipeTag
        builder.Entity<RecipeTag>().HasKey(rt => new { rt.RecipeId, rt.TagId });
        builder.Entity<RecipeTag>().HasOne(rt => rt.Recipe).WithMany(r => r.RecipeTags).HasForeignKey(rt => rt.RecipeId);
    }
}

1 Ответ

0 голосов
/ 05 мая 2020

Похоже, в вашей конфигурации отсутствует одна сторона настройки «Многие ко многим», которая может быть причиной проблемы. Попробуйте переключить настройку следующим образом. Удалите

builder.Entity<RecipeTag>().HasOne(rt => rt.Recipe).WithMany(r => r.RecipeTags).HasForeignKey(rt => rt.RecipeId);

, а затем добавьте

builder.Entity<RecipeTag>().HasOne(rt => rt.Tag).WithMany(r => r.RecipeTags).HasForeignKey(rt => rt.TagId).IsRequired();
builder.Entity<RecipeTag>().HasOne(rt => rt.Recipe).WithMany(r => r.RecipeTags).HasForeignKey(rt => rt.RecipeId).IsRequired();

Это определяет отношения для обеих сторон составного ключа.

...