EF Core (5) свойство навигации без связи с БД

#c# #sql-server #many-to-many #ef-database-first #ef-core-5.0

Вопрос:

У меня есть старая база данных, которую я сейчас не могу изменить. В базе данных нет связи между таблицами. Пожалуйста, посмотрите на приведенное ниже изображение БД. Здесь три таблицы, которые определяют отношение «многие ко многим», но не имеют физической связи (внешний ключ). С первым подходом к EFCore DB теперь мне нужно сопоставить модели EFCore с навигатором. Я создал классы моделей, как показано ниже,

 public class Company {
    [Key]
    [MaxLength(36)]
    [Column("COMPANY_ID")]
    public string CompanyId { get; set; }
    [Column("FULL_NAME")]
    public string FullName { get; set; }
    /*Others properties*/

    public virtual ICollection<TagCompany> TagCompanies { set; get; }
}

public class Tag {
    [Column("TAG_GROUP_ID")]
    public string GroupId { get; set; }

    [Column("TAG_ITEM_ID")]
    public string Id { get; set; }

    [Column("TAG_ITEM_NAME")]
    public string Name { get; set; }

    [Column("TAG_ITEM_DESCRIPTION")]
    public string Description { get; set; }

    public virtual ICollection<TagCompany> TagCompanies { set; get; }
}

public class TagCompany {
    [Column("COMPANY_ID")]
    public string CompanyId { get; set; }

    [Column("TAG_ITEM_ID")]
    public string TagId { get; set; }

    public virtual Company Company { set; get; }
    public virtual Tag Tag { set; get; }
}
 

И методы создания onmodel, как показано ниже,

             protected override void OnModelCreating(ModelBuilder modelBuilder) {

            modelBuilder.Entity<Company>().HasKey(o => o.CompanyId);
            modelBuilder.Entity<Tag>().HasKey(o => new { o.GroupId, o.Id });


            modelBuilder.Entity<TagCompany>().HasKey(o => new { o.CompanyId, o.TagId });
            modelBuilder.Entity<TagCompany>().HasOne(s => s.Tag).WithMany(s => s.TagCompanies).HasPrincipalKey(o => new { o.GroupId, o.Id });
            modelBuilder.Entity<TagCompany>().HasOne(s => s.Company).WithMany(s => s.TagCompanies).HasForeignKey(s => s.CompanyId);
            }
 

С указанным выше кодом я сталкиваюсь с ошибкой ниже,

Исключение Microsoft.Data.SqlClient.SQLException (0x80131904): Недопустимое имя столбца ‘TagGroupId’. Недопустимое имя столбца «TagId1». в Microsoft.Data.SqlClient.SqlConnection.Ошибка onError(исключение SQLException, Логическое прерывание соединения, Действие 1 wrapCloseInAction) at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action 1 Действие wrapCloseInAction) в

Любая помощь ценится по достоинству. Заранее спасибо. введите описание изображения здесь

Комментарии:

1. Является TAG.TAG_ITEM_ID уникальным?

2. Привет @GertArnold, ТЕГ. TAG_ITEM_ID не является уникальным. Как вы можете видеть, это составной ключ с тегом.TAG_GROUP_ID.

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

Ответ №1:

Проблема в том, что если Tag.Id не является уникальным , у вас есть скрытая связь «один ко многим «между TagCompany » и Tag «, которая требует свойства навигации по коллекции и, как правило, не может быть сопоставлена с одним TagCompany.TagId FK.

Вы можете обмануть EF, который Tag.Id уникален, сопоставив его как альтернативный ключ, который затем позволит вам сопоставить TagCompany.TagId как можно больше FK к одному, заменив

 .HasPrincipalKey(o => new { o.GroupId, o.Id }
 

с

 .HasPrincipalKey(o => o.Id)
 

Но теперь некоторые запросы будут возвращать неверные результаты в Tag данных, содержащих дубликаты Id s. Например, здесь

 var companies = db.Set<Company>()
    .Include(e => e.TagCompanies).ThenInclude(e => e.Tag)
    .ToList();

var includedTags = companies
    .SelectMany(e => e.TagCompanies).Select(e => e.Tag)
    .ToList();

var actualTags = db.Set<Company>()
    .SelectMany(e => e.TagCompanies).Select(e => e.Tag)
    .ToList();
 

actualTags является правильным и includedTags нет (содержит меньше элементов).

Таким образом, лучшим взломом, который, по-видимому, работает с EFC 5, было бы настроить отношения «многие ко многим» с помощью так называемых пропусков навигации. Вот измененная модель (существенными являются два свойства навигации по коллекции):

 public class Company
{
    [Key]
    [MaxLength(36)]
    [Column("COMPANY_ID")]
    public string Id { get; set; }
    [Column("FULL_NAME")]
    public string FullName { get; set; }
    /*Others properties*/

    public virtual ICollection<Tag> Tags { get; set; } // <--
}

public class Tag
{
    [Column("TAG_GROUP_ID")]
    public string GroupId { get; set; }

    [Column("TAG_ITEM_ID")]
    public string Id { get; set; }

    [Column("TAG_ITEM_NAME")]
    public string Name { get; set; }

    [Column("TAG_ITEM_DESCRIPTION")]
    public string Description { get; set; }

    public virtual ICollection<Company> Companies { get; set; } // <--
}

public class TagCompany
{
    [Column("COMPANY_ID")]
    public string CompanyId { get; set; }

    [Column("TAG_ITEM_ID")]
    public string TagId { get; set; }

    public virtual Company Company { set; get; }
    public virtual Tag Tag { get; set; }
}

 

и плавная конфигурация:

 
// composite PK
modelBuilder.Entity<Tag>().HasKey(e => new { e.GroupId, e.Id });

// M2M relationship and join entity configuration
modelBuilder.Entity<Company>()
    .HasMany(e => e.Tags)
    .WithMany(e => e.Companies)
    .UsingEntity<TagCompany>(
        j => j.HasOne(e => e.Tag).WithMany().HasForeignKey(e => e.TagId)
            .HasPrincipalKey(e => e.Id), // fake alternate key
        j => j.HasOne(e => e.Company).WithMany().HasForeignKey(e => e.CompanyId),
        j => j.HasKey(e => new { e.CompanyId, e.TagId }) // composite PK
    );

 

Теперь тот же тест, что и раньше

 var companies = db.Set<Company>()
    .Include(e => e.Tags)
    .ToList();

var includedTags = companies
    .SelectMany(e => e.Tags)
    .ToList();

var actualTags = db.Set<Company>()
    .SelectMany(e => e.Tags)
    .ToList();
 

дают одни и те же результаты.


Теперь последний хак, похоже, работает в EFC 5 (не тестировался с прогнозами и другими запросами LINQ, не возвращающими сущности), но может сломаться в будущих версиях EFC, поэтому используйте его на свой страх и риск. С другой стороны, нет другого способа отобразить такую модель БД, так что…

Комментарии:

1. ЭФ: «Извините, не могу этого сделать». Иван: «Что? Я заставлю тебя!»

Ответ №2:

TAG_COMPANY должен был бы иметь столбец TAG_GROUP_ID, чтобы отношения EF работали. Внешний ключ в базе данных технически не требуется, но требуется наличие всех ключевых столбцов.

Если ПОМЕТИТЬ.TAG_ITEM_ID уникален, вы можете настроить его как ключ Tag .

Комментарии:

1. Извините, мы не можем изменить схему базы данных, так как это очень старая существующая база данных и миллионы уже сохраненных данных.

2. Вы можете настроить Ключ сущности для сопоставления с любым уникальным индексом в базе данных без изменения схемы базы данных.

Ответ №3:

у вас есть ошибка , попробуйте это

 modelBuilder.Entity<TagCompany>().HasKey(o => new { o.CompanyId, o.TagId });
 

Комментарии:

1. Привет @Serge, Извини, что опечатка. Я исправляю это в описании.

2. @Скажи, что все в порядке. Мне интересно, как могла быть сделана эта опечатка. Вы снова вводите свой код здесь вместо копипаста?

3. Это очень большие таблицы и классы, я просто поставил здесь соответствующую часть. Это очень старая база данных, и она не нормализована, и плохо то, что мы не можем изменить схему базы данных.

4. @Sayem Вы можете сделать некоторые из ваших классов без ключа, если не можете найти ничего лучшего. Единственная плохая вещь в этом заключается в том, что вам придется создавать много соединений вручную вместо использования Include.