Entity Framework 4: Связь многие-ко-многим.

    • .NET
    • Linq-To-SQL
    • Visual Studio 2010
    • Entity Framework
    • LINQ
  • modified:
  • reading: 5 minutes

Недавно внедрил в проект Entity Framework 4. Надоело писать все на хранимых процедурах. Конечно, что-то можно и нужно делать на хранимых процедурах, но что-то быстрее и проще можно сделать с помощью ORM. Может встать вопрос, почему не nHibernate? Очень просто. nHibernate велик и всемогущ, но у него нет дизайнера. У нас очень простая бизнес логика, потому все прелести такого ORM нам не нужны. Честно, хватило бы и LINQ to SQL, но остановились на Entity Framework потому что: “а мало ли?”. Начал использовать я дизайнер, и первое с чем столкнулся – дизайнер, генерируя по базе, по связи многие-ко-многим (через таблицу связи) так и связывает сущности в дизайнере связью многие-ко-многим, а мне бы хотелось все-таки оставить промежуточный объект (вроде одно из отличий EF от LINQ to SQL, что есть поддержка связи многие-ко-многим, а мне не нравится). Полазил в интернете и единственное решение, которое нашел, было – добавить в промежуточную таблицу еще какое-нибудь поле, тогда все будет хорошо. Такое решение мне не подходило, и, конечно же, не хотелось мне лезть в XML файлы маппинга и править там все руками.

Давайте рассмотрим на примере. В базе данных у нас будут две таблицы Firm и Item, а так как у продукта (Item) может быть несколько фирм изготовителей (такое же тоже бывает, но редко), то связь сделаем через таблицу связи FirmItem:

Capture

А если эти таблицы импортировать в ADO.NET Entity Data Model, то там мы получим такие объекты:

Capture1

Как мы видим, оба класса имеют навигационные свойства с коллекциями. Честно, для меня сложнее в такой ситуации написать даже простой запрос, вроде, выбрать все Item по определенной фирме. Я могу придумать только такой вот код:

static void Main(string[] args)
{    using(ManyToManyTestEntities entities = new ManyToManyTestEntities())
    {
        var items = entities.Items.Where(x => x.Firms.Any(f => f.FirmID == 1));        foreach (var item in items)
        {
            Console.WriteLine(item.Name);
        }
    }
}

И в этом случае только надеяться, что Entity Framework сделает из этого inner join конструкцию. Но, этого не случится, конструкция будет следующая:

SELECT 
[Extent1].[ItemID] AS [ItemID], 
[Extent1].[Name] AS [Name]
FROM [dbo].[Item] AS [Extent1]
WHERE  EXISTS (SELECT 
    1 AS [C1]
    FROM [dbo].[FirmItem] AS [Extent2]
    WHERE ([Extent1].[ItemID] = [Extent2].[ItemID]) AND (1 = [Extent2].[FirmID])
)

В памяти у меня всплывает, вроде, что я находил конструкции LINQ, которые приводили к inner join, но не могу вспомнить какие. В общем, для меня работа без промежуточного объекта в этой связи была не понятна и не очевидна, потому я решил найти способ, как сделать “по обычному” при помощи дизайнера (через XML маппинг-то легко).

Для начала нужно убедиться, что открыто два окна в Visual Studio: Model Browser и Mapping Details. Очень странно, но у меня первое окно (Model Browser) не было изначально активно, и я просто потерялся, как же вообще можно что-то отредактировать.

Capture2

Сначала убьем существующую связь между двумя объектами (просто Delete при выбранной этой связи, либо через контекстное меню). Вас предупредят о том, что FirmItem из описанной store model останется незамапленным никуда, предлагают удалить и его, жмем в окошке No. Теперь мы можем создать свой элемент  с именем FirmItem (до этого бы дизайнер ругнулся, если связь не была бы удалена, так как уже присутствовал элемент с таким именем), для этого правой кнопкой мыши в дизайнере модели и “Add –> Entity…”, где мы установим имя этому элементу, так же имя коллекции в нашей модели и укажем, что не нужно создавать никаких key property:

Capture3

Теперь добавим две связи через команду “Add –> Association…” в контекстном меню (там же где добавление элемента). Я эти связи называю так же как и в базе данных. То есть, если в базе данных внешний ключ называется FK_FirmItem_Item, то и в модели так же называю связь (по такой же логике поступает и дизайнер Entity Framework). Для одной из связей я указал следующие поля (главное не перепутать, где Many а где One), и не забудьте указать то, чтобы добавили свойство в FirmItem для связи:

Capture4

Делаем тоже самое второй раз только для внешнего ключа FK_FirmItem_Firm, отличается только тем, что указать нужно таблицу Firm вместо Item и дать опять правильное имя для связи. Должно получиться так:

Capture5

Следующим шагом необходимо настроить элемент FirmItem. Сначала переименуем свойства и укажем, что эти свойства являются ключами для объекта. Для этого выбираем свойство и в контекстному меню ищем Properties, в окне Properties делаем изменения, например, для свойства FirmFirmID я поменял так:

Capture6

Для свойства ItemItemID делаем аналогично. Следующим шагом связываем наш элемент с таблицей. Для этого выбираем FirmItem элемент и идем в окно Mapping Details, там просто из выпадающего списка выбираем таблицу FirmItem, у нас должно автоматически замапиться правильно колонки таблицы и свойства элемента:

Capture7

Все, можно пользоваться. Самое приятно, что при обновлении модели из базы данных – наши изменения не потеряются, так и останется промежуточный элемент. Теперь пример выбора всех элементов по идентификатору фирмы можно переписать так:

static void Main(string[] args)
{    using(ManyToManyTestEntities entities = new ManyToManyTestEntities())
    {        var items = from i in entities.Items
                    join fi in entities.FirmItems on i.ItemID equals fi.ItemID
                    where fi.FirmID == 1
                    select i;        foreach (var item in items)
        {
            Console.WriteLine(item.Name);
        }
    }
}

И получить нормальный inner join на выходе:

SELECT 
[Extent1].[ItemID] AS [ItemID], 
[Extent1].[Name] AS [Name]
FROM  [dbo].[Item] AS [Extent1]
INNER JOIN [dbo].[FirmItem] AS [Extent2] ON [Extent1].[ItemID] = [Extent2].[ItemID]
WHERE 1 = [Extent2].[FirmID]

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

See Also