#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.