Pull to refresh

Entity Framework глазами постороннего

Reading time12 min
Views22K

Предыстория


А ведь много, много пишут на Entity Framework. Фактически, это ORM «по умолчанию» для .NET и среды Microsoft Visual Studio…

Так, однажды, не очень давно, попал в мои руки один немаленький проект. Ко времени моего появления, проекту было примерно три года. В техническом отношении он представлял собой душераздирающее зрелище. Грубый замер показал, что веб-приложение легко и совершенно неоправданно умудрялось потреблять около 1ГБ серверной памяти на одновременного пользователя. Пришлось быстро-быстро разбираться, чего же там такого интересного написано. Хороший урок, как делать не надо.

В этом проекте для доступа к данным в реляционной БД применялся Entity Framework. Кроме очевидных архитектурных и программистских глупостей, навроде ToList() по любому поводу, Include чему попало, наложения ограничений на множества объектов не на уровне БД, а на уровне приложения с использованием LINQ To Objects, были и проблемы, связанные только с Entity Framework.

Сознаюсь, не могу причислить себя к активным пользователям и знатокам Entity Framework, т.к. в практической жизни много лет пользуюсь другим ORM. Но это и хорошо, ибо я могу рассказать вам, уважаемые читатели, о странностях Entity Framework и сопутствующего инструментария, замеченных глазом постороннего наблюдателя. Всего не охватить, но расскажу, что вижу, раз уж я попробовал этот самый Entity Framework.

Семь раз отмерь или ужасы проектирования


CodeFirst и DBFirst не предлагать. И вот почему. Ответ здесь и философский, и практический.

CodeFirst. Безусловно, он даёт самые продвинутые возможности в Entity Framework и полный контроль. Некоторые готовы спорить за этот подход до хрипоты и до драки. Мой аргумент прост: пока у вас 5-10-15 сущностей, вы можете держать структуру системы в голове. Пользуйтесь на здоровье! Когда счёт идёт на сотни, вы этого уже не можете. У меня действительно легко идёт на сотни и в мою голову столько не помещается. В головы товарищей тоже, особенно когда вводишь в проект нового человека. Поэтому мне нужны картинки. Компактные, наглядные, удобные. Причем модель должна быть не на одной картинке, а на нескольких, по-частям, выбранным как-то предметно. Ну, и далее, с картинок что-то как-то должно генерироваться, не буду же я писать однотипный код сотнями раз, да ещё и не делать при этом ошибок.

Может, DBFirst? Не-ет, это тоже не для меня. Наследование я как буду делать? И потом, как я буду думать про прикладное через специфику базы? Там и типы данных свои. Если я написал, скажем, VARBINARY, мне придётся в голове помнить, что где-то это картинка, а где-то какой-нибудь хеш. Ну и документировать это отдельно, либо разводить фольклор с товарищами. Помножим это на 300 с лишком сущностей. Нет, это неудобно. Нет, хочу думать в прикладных типах. И потом, что останется после меня?

Остаётся ModelFirst. Здесь я хотя бы могу рисовать сущности приближенно к тому, что я потом получу в C#, и использовать .NET-ные типы для полей. Но и тут заботливо расставлены развесистые грабли:
  1. Дизайнер. Почему связи на нём скачут как попало? И проводятся по диаграмме, как нравится Visual Studio, а не как мне, причём в отдельных случаях завязываются узлами? Я хочу, чтобы они были прибиты к листу гвоздями и не двигались никуда без моего ведома, а то я их найти не могу после очередного открытия диаграммы. Кажется, я понимаю, почему так популярен CodeFirst.
  2. Как просто разрезать модель на несколько диаграмм? Ну, неудобно мне 300 сущностей на одной диаграмме и, кроме того, я замучиваюсь тыркать мышкою в ползунки. Да и очень тормозит дизайнер при таких количествах сущностей. Конечно, есть функция «Move to new diagram», но как же потом, если понадобится, обратно объединить диаграммы, может передвинуть часть сущностей, использовать одни и те же сущности на нескольких диаграммах?
    Гуру говорят, есть какой-то способ, есть. Он из области ручного редактирования .diagram файла. Но это нужно познать Высший замысел. Но я ж не гуру, мне просветляться некогда, мне прикладной функционал надо набирать.
  3. Вот, к примеру, нарисовал я поле типа string. В базу оно генерируется по умолчанию как nvarchar(max). Отвесить бы придумавшему это умолчание инженеру щедрый щелбан. Только за это умолчание. От души. В свойствах поля EntitySet изменить тип нельзя даже для одного случая. А я вообще хочу изменить это умолчание, чтобы все строковые поля генерировались другим типом БД. Что делать? Править в текстовом редакторе edmx для всех, а затем разгребать последующие глюки?
  4. Я по-прежнему не могу рисовать, используя свои прикладные типы, выбор типов ограничен в дизайнере. Что делать, если я придумал в процессе проектирования свой прикладной тип (или уже имею готовый) и просто хочу объяснить ORM, как он отображается в базу, а как — в код на C#? Ну, разумеется, мне при реализации типа придётся соблюсти некие правила, например, наличие implicit-преобразований к нативному типу, или реализация древнючего IConvertible во что-то.
  5. Что мне делать, если я хочу, чтобы код на C#, соответствующий сущностям, содержал что-нибудь особенное? А предлагается в таком случае вернуться к CodeFirst. Либо пожалуйте в дивный, новый мир T4.
  6. Надизайненное в редакторе наследование не развязывается в «человеческий» TPC (отдельная таблица под каждый класс со всеми полями), а только как:
    — TPT (отдельная таблица под каждый класс только со своими полями + связь к предку). Потом при выборке будет вам: JOINы под каждую связь наследования — беда с производительностью и головная боль при попытке сформировать ограничение на голимом SQL;
    — TPH (все уровни положить в одну таблицу, наплевав на нормализацию и другие чистые идеалы). Интересно, как вообще можно до такого додуматься.
    Если вы всё-таки хотите TPC – опять см. CodeFirst + ModelBinder. Пользуясь случаем, передаю привет тому самому инженеру. Отсыпьте ему жирных щелбанов по числу прикладных сущностей.

Чтобы понять, как «удобно» проектировать, советую поговорить с кем-то, кто пытается приспособить средства проектирования Entity Framework, скажем, под Oracle. Особенно внесение изменений в имеющуюся модель. Рекомендую найти для этой цели лицо противоположного пола. Контраст мировосприятий придаст остроту вашей дискуссии.

Где ключи от танка или «занимательное» программирование


Ну, давайте посмотрим, как же это программируется.
Начнём с простого: создадим и сохраним что-нибудь:
CDLIBEntities dbctx = new CDLIBEntities();            
 
//Country ctr1 = dbctx.Country.Create();
Country ctr1 = new Country();
ctr1.primaryKey = Guid.NewGuid(); //Ах, да! Надо не забыть вручную сгенерировать первичный ключ.
ctr1.Name = "Greece";
 
//Publisher pblshr1 = dbctx.Publisher.Create();
Publisher pblshr1 = new Publisher();
pblshr1.primaryKey = Guid.NewGuid(); //Ах, да! Опять сгенерировать ключ...
pblshr1.Name = "First Publisher";
pblshr1.Country = ctr1.primaryKey; //Чёрт, я опять забыл, надо, надо помнить о ключах и везде расставлять вручную, иначе EF подумает, что нарушилась ссылочная целостность
pblshr1.Country1 = ctr1;
 
//dbctx.Publisher.Attach(pblshr1); //АУ! Почему я вообще должен здесь заботиться о том, какого типа у меня сущность??? Ведь итак понятно, какого она типа.
dbctx.Publisher.Add(pblshr1); // Ах, Attach не вправляет сущностям статус на создание, мне тоже нужно помнить, что новым объектам надо делать Add, а старым Attach, иначе всё сломается
                              // А может, статус сущности будет как-нибудь сам распознаваться?
 
dbctx.SaveChanges();


Здесь удивительно:
  1. Надо вручную инициализировать первичные ключи. Не лучше ли иметь какую-то отдельно прописанную закономерность по генерации первичного ключа? Ну, и возможности по управлению;
  2. Для связывания сущностей надо не только установить navigation property, но и проставить правильно внешний ключ, иначе Entity Framework заругается на нарушение ссылочной целостности. Непонятно: если каждая сущность идентифицируется ключом, то связка через публичное поле автоматически должна означать реляционное отношение. Это так легко, просто означить публичное поле, но нееет, давай-ка, дорогой программист, помни, что у нас есть первичные ключи и внешние ключи, означивай сиди, будто делать больше нечего;
  3. Да, есть подход, когда ключи для объектов создаются сами в БД. С одной стороны, почему бы и нет, особенно, если ключ целочисленный, но с другой стороны, пока я не выполню запрос по сохранению, я не имею возможности понять, какой будет ключ, и использовать его дальше. Особенно это забавно обрабатывать, когда сконструированный объект нужен в памяти и с ключом, но, требуется ли его сохранение в БД, определяется в самом конце.
  4. Прицепление или добавление объектов в контекст производится только вызовом метода у коллекции соответствующего типа. Причем используются разные методы: для добавления — Add, а для прицепления существующего — Attach. Что так «удобно»? Может в вопросе что, куда и как добавить, Entity Framework совместно с .Net как-нибудь без меня станут разбираться, какого типа эта сущность и что с ней произошло?
  5. Зачем существует контекст? Чтобы ещё подкинуть мозгам программиста разных «интересных» загадок? Например, подсовывать ему из контекста экземпляры каких-нибудь заглушек, а не самих сущностных классов. Или, например, что будет, если попробовать связать объекты, взятые из разных контекстов? То-то, наверное, будет весело. Значит, надо помнить, что из какого контекста я достал.

Что-то многовато телодвижений для такой простой задачи, как конструирование и связывание сущностей друг с другом.

Как спрашивают СУБД или «недо-include»


Давайте-ка посмотрим, что происходит, когда Entity Framework читает что-либо из БД. Это тоже очень интересно. Как и когда он строит и выполняет запросы.

Поглядите на edmx (кусочек), чтобы немного представить задачку:

Кстати говоря, функция экспорта модели в картинку реализована так, чтобы не только доставить разработчику немало веселых и приятных минут, но и скрасить ему досуг.
Если вы экспортируете относительно большую модель, то разрешающая способность картинки будет примерно такая:

В принципе, я допускаю, что найдутся люди, которые способны это разглядеть. Вот в заголовке сущности написано «BlueRay».

И потом, вы заметили, что мощности связей мне пришлось указать, «накарябав» на картинке «от руки», т.к. они не видны? Я сделал это нарочно «пострашнее». Вы думаете, в процедуре экспорта содержится ошибка и мощность просто не попала в диаграмму? Нет, всё проще, ошибок нет, мощность честно экспортирована светло-светло серыми символами на белом фоне (поглядите внимательно под концы связей). Сделано это, вероятно, с целью проверки цветоразрешающей способности мониторов, а также тренировки цветовосприятия разработчиков.

Мы хотим вывести название диска, название издателя и название страны. Есть соблазн решить задачу в лоб. А что, удобно. Берём коллекцию из контекста, проходим по связям. Все данные подкачиваются из базы как-то сами. Да можно и не задумываться об этом, всё работает, цель достигнута, чего ещё надо.
Реализовав нехитрую конструкцию, подглядим, какие при этом запросы выполняет EF.
CDLIBEntities dbctx = new CDLIBEntities();
            
dbctx.Database.Log = (s => { textBox1.AppendText(string.Format("{0}{1}", s, Environment.NewLine)); }); //Так будем подглядывать, какие запросы выполняет EF
 
DVD[] dvds = dbctx.DVD.ToArray(); // Посмотрим, как много запросов будет в таком варианте
//DVD[] dvds = dbctx.DVD.Include("Publisher1").Include(@"Publisher1.Country1").ToList().ToArray(); //В таком варианте будет один запрос с joins, однако нужно ли читать ВСЕ поля из связанных таблиц? На практике это кошмар с ужасом
 
for (int i = 0; i < dvds.Length; i++ )
{ 
    textBox1.AppendText(string.Format("{0} {1} {2}", dvds[i].Name, dvds[i].Publisher1.Name, dvds[i].Publisher1.Country1.Name) );
    textBox1.AppendText(Environment.NewLine);
}
 
textBox1.AppendText("ОК" + Environment.NewLine);


Сначала проверим «в лоб», запускаем: смотрите, Entity Framework выполняет целых 5 запросов, причем каждый ещё и внутри своей коннекции.
Вывод текстбокса:
Opened connection at 17.04.2015 11:01:19 +05:00

SELECT 
    [Extent1].[primaryKey] AS [primaryKey], 
    [Extent1].[Version] AS [Version], 
    [Extent1].[Capacity] AS [Capacity], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Publisher] AS [Publisher]
    FROM [dbo].[DVD] AS [Extent1]


-- Executing at 17.04.2015 11:01:20 +05:00

-- Completed in 11 ms with result: SqlDataReader



Closed connection at 17.04.2015 11:01:20 +05:00

Opened connection at 17.04.2015 11:01:20 +05:00

SELECT 
    [Extent1].[primaryKey] AS [primaryKey], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Country] AS [Country]
    FROM [dbo].[Publisher] AS [Extent1]
    WHERE [Extent1].[primaryKey] = @EntityKeyValue1


-- EntityKeyValue1: '5cba87e2-2809-4437-9ff3-5abfe0d21536' (Type = Guid, IsNullable = false)

-- Executing at 17.04.2015 11:01:20 +05:00

-- Completed in 6 ms with result: SqlDataReader



Closed connection at 17.04.2015 11:01:20 +05:00

Opened connection at 17.04.2015 11:01:20 +05:00

SELECT 
    [Extent1].[primaryKey] AS [primaryKey], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Country] AS [Extent1]
    WHERE [Extent1].[primaryKey] = @EntityKeyValue1


-- EntityKeyValue1: '888c8fd2-1a12-4ec4-90fa-9742c29cae9e' (Type = Guid, IsNullable = false)

-- Executing at 17.04.2015 11:01:21 +05:00

-- Completed in 2 ms with result: SqlDataReader



Closed connection at 17.04.2015 11:01:21 +05:00

Movie 3 Second Publisher USA
Opened connection at 17.04.2015 11:01:21 +05:00

SELECT 
    [Extent1].[primaryKey] AS [primaryKey], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Country] AS [Country]
    FROM [dbo].[Publisher] AS [Extent1]
    WHERE [Extent1].[primaryKey] = @EntityKeyValue1


-- EntityKeyValue1: '65e0fb16-15aa-4591-8c8b-286e498d1203' (Type = Guid, IsNullable = false)

-- Executing at 17.04.2015 11:01:21 +05:00

-- Completed in 0 ms with result: SqlDataReader



Closed connection at 17.04.2015 11:01:21 +05:00

Opened connection at 17.04.2015 11:01:21 +05:00

SELECT 
    [Extent1].[primaryKey] AS [primaryKey], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Country] AS [Extent1]
    WHERE [Extent1].[primaryKey] = @EntityKeyValue1


-- EntityKeyValue1: '658e951e-b56a-4423-b16a-b1dd2c7c293a' (Type = Guid, IsNullable = false)

-- Executing at 17.04.2015 11:01:21 +05:00

-- Completed in 0 ms with result: SqlDataReader



Closed connection at 17.04.2015 11:01:21 +05:00

Movie 0 First Publisher Greece
Movie 1 Second Publisher USA
Movie 4 First Publisher Greece
Movie 2 First Publisher Greece
ОК


Это от того, что EF работает по принципу «отдай то, за что хватают», имея в виду навигационные свойства. За это удобство разработки приходит жесткая расплата: в больших и сложных системах так делать нельзя, т.к. это приводит к расточительному расходованию ресурсов как СУБД, так и приложения.

Хорошо, что EF имеет кое-какие средства для управления загрузкой. Раскомментируем Include. Хвала мудрости EF! Вот теперь-то JOIN под каждый Navigation и всего один запрос.
Вывод текстбокса:
  Opened connection at 17.04.2015 11:03:44 +05:00

SELECT 
    1 AS [C1], 
    [Extent1].[primaryKey] AS [primaryKey], 
    [Extent1].[Version] AS [Version], 
    [Extent1].[Capacity] AS [Capacity], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Publisher] AS [Publisher], 
    [Extent2].[primaryKey] AS [primaryKey1], 
    [Extent2].[Name] AS [Name1], 
    [Extent2].[Country] AS [Country], 
    [Extent3].[primaryKey] AS [primaryKey2], 
    [Extent3].[Name] AS [Name2]
    FROM   [dbo].[DVD] AS [Extent1]
    INNER JOIN [dbo].[Publisher] AS [Extent2] ON [Extent1].[Publisher] = [Extent2].[primaryKey]
    INNER JOIN [dbo].[Country] AS [Extent3] ON [Extent2].[Country] = [Extent3].[primaryKey]


-- Executing at 17.04.2015 11:03:45 +05:00

-- Completed in 16 ms with result: SqlDataReader



Closed connection at 17.04.2015 11:03:45 +05:00

Movie 0 First Publisher Greece
Movie 4 First Publisher Greece
Movie 2 First Publisher Greece
Movie 1 Second Publisher USA
ОК


Всё или ничего


НО вот ведь беда: если мы просто пишем Include, читаются ВСЕ свойства как своей, так и связанных сущностей. Кажется, это ключ к пониманию того, куда уходит память и скорость. «Пере-include», так сказать. Вызов Include в лоб чему попало — это страшно, особенно ветвистый на много navigation.

Попробуем сделать лучше. Решение: выбрать множество только нужных полей у сущностей и зачитать только их. Такое множество можно назвать проекцией или представлением, кому как удобно и угодно будет. Хорошо, что Entity Framework имеет такую возможность, правда, кривую.
Чтение в проекции:
CDLIBEntities dbctx = new CDLIBEntities();            
dbctx.Database.Log = (s => { textBox1.AppendText(s); });
 
// 1. Связка анонимных типов (связка потому, что придётся для навигационных свойств тоже сделать анонимные типы)
var anon = dbctx.DVD.Select(x => new { primaryKey = x.primaryKey, Name = x.Name, Publisher1 = new {primaryKey = x.Publisher1.primaryKey, Name = x.Publisher1.Name } }).ToArray();
 
// 2. Объявленные типы (аналогично анонимному, только типы явно объявим, пример опустим, он очевиден)
 
// 3. Взять и сделать наследников от нужных сущностей (хотя бы явно типы писать не надо)
DVD_D[] dvd_derived = dbctx.DVD.Select(x => new DVD_D { primaryKey = x.primaryKey, Name = x.Name, Publisher1 = new Publisher_D { primaryKey = x.Publisher1.primaryKey, Name = x.Publisher1.Name } }).ToArray();


Есть 3 варианта:
  1. Объявляем анонимный тип и выбираем в него только то, что нужно;
  2. Пишем специальный класс только с нужными свойствами и используем его;
  3. Делаем наследников от нужных сущностей и заполняем в них выборку.

Что тут неудобно: Неужели так будем писать под каждую выборку? Это продуктивно?

И потом: цель-то наша не читать, а с сущностями дальше РАБОТАТЬ, т.е. писать логику, которая, в результате приводит к изменению значений полей сущностей. Должно быть, сделать это просто: изменить значение свойства у сущности, а затем вызвать у контекста сохранение, когда надо будет, вот так:
// Реально, очень хочется написать что-то навроде
dvd_derived[0].Name = "Зелёная сосиска";
dbctx.SaveChanges();

Не тут-то было: объект этот не создан из контекста и тип его EFу неизвестен. И как нам теперь сохранить изменения?

Кривизна состоит в том, что совершенно непонятно, как сохранить изменения, не взяв объект из контекста ПОЛНОСТЬЮ, со всеми полями.

Что же делать? Изобретают люди! Например, пишут классы под каждый случай проекции и маппируют как-то затем в сущностные классы (понятные контексту) и обратно. Можно также иметь отдельные наборы сущностных классов с наборами свойств, соответствующим выборкам и настройкой на те же таблицы. Можно, можно как-то извернуться. Должно быть, захватывающее занятие. Но суть этого занятия никак не стыкуется с правильной философией, которая состоит в том, что ORM на то и ORM, что конструкции в языке программирования однозначно представляют предметную сущность (ОБЪЕКТЫ). А не так, что часть там, часть тут. Тут так читаем, тут эдак, тут маппируем, тут разрезаем, тут клеим, тут рыбу заворачиваем. Если я взял какую-то сущность в коде, я должен железобетонно быть уверен, что тут всё, что мне нужно, и больше в коде нет ничего, что отвечает за ту же сущность. Разделяй и властвуй, нечего размазывать функциональность. А иначе, никакой это не ORM, а так — классический набор костылей.

Вместо заключения


Цель использования ORM — сосредоточить программиста на решении предметных задач, не теряя в производительности как программиста, так и разрабатываемого им ПО. Производительность труда вообще должна повышаться. На дворе 21 век, а вопрос доступа к данным так и не закрыт. В таком виде это всё красиво для школьных поделок на 10-20 сущностей и уж никак не годится в сколько-то серьёзной задаче.

Короче говоря, я не понимаю. Ну ладно, для меня Entity Framework — диковинка от унаследованных проектов, сами пользуемся другим продуктом, совсем не массовым и не раскрученным, но как же живёт большинство? Ведь во всём мире работают и радуются жизни люди, как-то пишут и выдают продукты? Ну и вообще спокойно спят себе ночами. Ведь я только взялся, а прямо всё рассыпается в руках. Либо серьёзные, большие проекты пишут на чем-то другом (интересно, на чём), либо там пишут всё вручную, во что вообще верится с трудом. Люди, как вы живёте, я не понимаю…

У меня есть ещё вагончик вопросов про запас. Но лучше, наверное, я в следующей статье расскажу вам, какой ORM кажется удобным мне, «лучшие практики», как сейчас модно выражаться. А?
Tags:
Hubs:
-1
Comments120

Articles

Change theme settings