Хранить список внешних ключей во многих отношениях Entity Framework

У меня есть отношение "многие ко многим" в моей первой модели Entity Framework. Представьте себе, что у нас есть две таблицы: "Компания" и "Статья", которые имеют такие отношения между ними. Моя упрощенная модель кода выглядит следующим образом:

public class Article
{
    public int Id { get; set; }
    public string Text { get; set; }
    public virtual ICollection<Company> Companies { get; set; }
}

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Article> Articles { get; set; }
}

Используя текущее сопоставление, я создаю отношения "многие ко многим":

modelBuilder.Entity<Company>().HasMany(c => c.Articles).WithMany(a => a.Companies);

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

public virtual ICollection<int> ArticlesId { get; set; } // to Company
public virtual ICollection<int> CompaniesId { get; set; } // to Article

Я знаю, что одним решением является создание промежуточной модели таблицы в EF и вручную выбирать соответствующие идентификаторы в каждом вызове, но, возможно, сопоставление EF может обеспечить более удобный способ выполнения такой операции? Заранее благодарю за любые советы.

Ответ 1

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

Первое решение, предложенное Беном Райхом.
Реализовать свойство get-only, которое будет возвращать только идентификаторы связанных объектов.

public class Company
{
    public virtual ICollection<Article> Articles { get; set; }

    public IEnumerable<int> ArticlesIds
    {
        get { return Articles.Select(a => a.Id); }
    }
}

Кажется, что он удобен в использовании, однако имеет недостаток: весь объект будет считываться из базы данных, чтобы получить единственный идентификатор. Вот журнал такого вызова из SQL Profiler:

exec sp_executesql N'SELECT 
[Extent2].[Id] AS [Id], 
[Extent2].[Header] AS [Header], 
[Extent2].[Description] AS [Description], 
[Extent2].[Text] AS [Text], 
[Extent2].[CreationDate] AS [CreationDate], 
[Extent2].[AccountId] AS [AccountId], 
[Extent2].[ImageSetId] AS [ImageSetId]
FROM  [dbo].[CompanyArticles] AS [Extent1]
INNER JOIN [dbo].[Articles] AS [Extent2] ON [Extent1].[Article_Id] = [Extent2].[Id]
WHERE [Extent1].[Company_Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1

Второе решение.
Используя ту же модель, читайте идентификаторы отдельно после чтения объекта.

var ids = db.Set<Article>().Where(a => a.Companies.Select(c => c.Id).Contains(f.Id)).ToList();

Этот подход работает совершенно так же, как и предыдущий, будет выбран весь набор объектов.

exec sp_executesql N'SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Header] AS [Header], 
[Extent1].[Description] AS [Description], 
[Extent1].[Text] AS [Text], 
[Extent1].[CreationDate] AS [CreationDate], 
[Extent1].[AccountId] AS [AccountId], 
[Extent1].[ImageSetId] AS [ImageSetId]
FROM [dbo].[Articles] AS [Extent1]
WHERE  EXISTS (SELECT 
    1 AS [C1]
    FROM [dbo].[ArticleCompanies] AS [Extent2]
    WHERE ([Extent1].[Id] = [Extent2].[Article_Id]) AND ([Extent2].[Company_Id] = @p__linq__0)
)',N'@p__linq__0 int',@p__linq__0=1

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

public class ArticleCompany
{
    public int CompanyId { get; set; }
    public int ArticleId { get; set; }

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

Сопоставьте оба объекта с этим объектом как отношения "один к одному". Не забывайте сопоставлять новую сущность.

modelBuilder.Entity<Article>().HasMany(a => a.ArticlesCompanies).WithRequired(ac => ac.Article).HasForeignKey(ac => ac.ArticleId);
modelBuilder.Entity<Company>().HasMany(c => c.ArticlesCompanies).WithRequired(ac => ac.Company).HasForeignKey(ac => ac.CompanyId);

modelBuilder.Entity<ArticleCompany>().ToTable("ArticlesCompanies");
modelBuilder.Entity<ArticleCompany>().HasKey(ac => new { ac.ArticleId, ac.CompanyId });

Затем, после извлечения объекта, используйте промежуточную таблицу для извлечения связанных идентификаторов:

var ids = db.Set<ArticleCompany>().Where(ca => ca.CompanyId == companyEntity.Id).Select(ca => ca.ArticleId);

Соответствующий SQL-журнал (только идентификаторы извлекаются из базы данных):

exec sp_executesql N'SELECT 
[Extent1].[ArticleId] AS [ArticleId]
FROM [dbo].[ArticlesCompanies] AS [Extent1]
WHERE [Extent1].[CompanyId] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=1

Ответ 2

Достаточно ли достаточно простых геттеров?

public IEnumerable<int> ArticlesId { get { return this.Articles.Select(a => a.Id); } }
public IEnumerable<int> CompaniesId { get { return this.Companies.Select(c => c.Id); } }

Это будет охватывать многие варианты использования. Конечно, поскольку это не свойство виртуальной ассоциации, вы теряете некоторую гибкость использования этих свойств в реальных запросах Linq-to-Entities.