воскресенье, 27 апреля 2008 г.

Разбираемся с ADO.NET Entity Framework

Итак, в этом проекте мы не будем использовать никаких посторонных систем ORM (про ORM здесь), а попробуем построить слой DAL на основе новой технологии ADO.NET Entity Framework, предложенной Microsoft. Говорят, что это не просто ORM, а нечто большее. Что ж, попробуем понять, что именно. Честно говоря, не совсем понимаю, почему вокруг этой ORM столько шума. Мне, например, очень нравится ORM LLBLGen, я думаю, что эта система ни в чем не уступает майкрософтовской, за исключением, разве что тем, что в ней нельзя использовать LINQ (хотя может быть, и можно).

Entity Data Model
В основе ADO.NET EF лежит модель сущностей, т.е. Entity Data Model. Как и предполагалось, основные объекты модели - это сущности и связи. Все объекты и их связи представлены в виде XML-схемы - в файле с расширением .edmx. Таблицы представлены объектами EntitySet, поля - свойствами Property (обычные поля) и NavigationProperty (поля, участвующие в ассоциациях - ключи, foreign keys).

Служебные объекты (Object Services)
В ADO.NET EF уществует несколько полезных объектов, с помощью которых можно получать доступ к соединению с БД или, например, к выборке из базы, представленной в виде типизированной коллекции.
ObjectContext позволяет получить доступ к соединению, метаданным и еще кое-каким службам.
ObjectQuery - это, по сути, запрос, который возвращает коллекцию типизированных сущностей.
ObjectStateManager кэширует запросы, а также отслеживает изменения в сущностях (с целью дальнейшей синхронизацией с БД).

Mapping
Это то, как полученные сущности связаны с реальными объектами базы данных.

Как все это выглядит
Добавим в наш солюшн еще один проект, который назовем DataCore типа ClassLibrary. Затем в проект добавим новый айтем типа ADO.NET Entity Data model. Сразу же на экране возникает визард, предлагающий нам выбрать, будет ли наша модель построена на основе базы данных, или она будет пустой. Конечно, выбираем Generate from database. Дальше все просто. Визард автоматически подключается к серверу, вернее, сразу к самой базе, после чего мы выбираем таблицы и другие объекты, которые хотим включить в модель. В нашем проекте мы включим следующие таблицы:

  • aspnet_Applications

  • aspnet_Membership

  • aspnet_Users

  • Category

  • Comment

  • Question

  • UserDetails


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


Если теперь выбрать файл DataModel.edmx в SolutionExplorer, а затем Open with... XML Editor, то мы увидим, как в действительности выглядит этот файл. Это обычный XML, точнее схема, где описаны все элементы: сущности идут здесь как EntitySet, связи между таблицами (ассоциации) как AssociationSet, а отдельные поля - это Properties.

Помимо этой схемы, студия генерирует еще три файла DataModel.csdl, DataModel.msl и DataModel.ssdl. Файл CSDL представляет собой концептуальную схему - это XML файл, в котором представлены объекты на уровне сущностей. Файл SSDL - схема объектов (таблиц и пр.), как они представлены в базе данных.Файл MSL - схема сопоставления - содержит данные о том, как связаны между собой концептуальная схема и схема объектов БД.

Теперь вернемся в Solution Explorer и рассмотрим файл DataModel.Designer.cs. Это самый важный для нас файл, потому что здесь сосредоточены все сгенерированные объекты. Если мы откроем наш первый проект (MVC), то в любом месте сможем, например, написать (не забудьте добавить ссылку на проект):

using (DataCoreConnection context = new DataCoreConnection())
{
Category cat = new FAQEngineModel.Category();
cat.Name = "Web";
context.AddObject("Category",cat);
context.SaveChanges();
}

Итак, что же делает этот код?
В первую очередь он создает объект контекста (DataCoreConnection), который будет уничтожен после выхода за пределы фигурных скобок. Этот объект идет первым в нашем файле DataModel.Designer.cs и является по сути связующим звеном между нашим кодом (объектами) и базой данных. С его помощью выполняются все стандартные операции с данными - выборка, вставка, обновление и удаление.

Затем мы создаем новый объект категории, присваиваем ей имя Web, добавляем объект в контекст и сохраняем изменения. Это, конечно, всего лишь пример кода, поскольку настоящий код должен обязательно содержать конструкцию try-catch.

Однако в этом коротком примере кроется одна проблема, которая, как я надеюсь, будет в дальнейшем решена разработчиками студии. Дело в том, что все проходит гладко, когда мы имеем один проект, из которого вызываем наш контекст. Но как только мы выделяем обработку с данными в отдельный проект, а вызываем код из другого проекта, начинаются проблемы. Во-первых, поскольку вызывающий проект (у нас это веб-проект) имеет свой файл конфигурации, в котором содержатся свои строки подключения к БД, он в упор не видит файла конфигурации App.config проекта DataCore, и следовательно, не может подключиться к базе. Как результат, мы видим на экране сообщение EntityConnection error: The specified named connection is either not found in the configuration. Это исправляется легко - достаточно скопировать строку подключения из файла App.config в файл Web.config нашего веб-проекта.

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

<add name="DataCoreConnection" connectionString="metadata=.\FAQEngineModel.csdl|.\FAQEngineModel.ssdl|.\FAQEngineModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=.\SQLEXPRESS;Initial Catalog=FAQEngine;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />


Обратите внимание, на какие файлы метаданных она ссылается. Все эти файлы лежат в каталоге bin/Debug проекта DataCore (.\) - т.е. в каталоге, куда помещается dll. Но поскольку мы вызываем эту dll из другого проекта, то получается, что мы ищем эти файлы там, где их нет, и получаем новую ошибку: "The specified metadata path is not valid. A valid path must be either an existing directory, an existing file with extension '.csdl', '.ssdl', or '.msl', or a URI that identifies an embedded resource.". Нам остается два выхода - либо исправить строку подключения ,либо каждый раз копировать нужные файлы в какой-либо каталог веб-проекта.

Попробуем сначала первый способ. Лучше всего, если бы можно было указать относительный путь, например, с использованием какого-нибудь макроса. Единственное, что мне удается найти - это DataDirectory. Но, поскольку мы вызываем файлы из веб-приложения, этот макрос будет заменен каталогом App_Data, который нам вовсе ни к чему. Поэтому можно просто указать абсолютный путь к файлам на диске. Но потом, после деплоймента, эту строку придется менять. Поэтому этот способ мне не нравится.

Второй способ - копирование файлов - тоже не слишком хорош. Получается, что каждый раз, как только я изменяю модель, мне надо куда-то еще копировать сгенерированные файлы? Получается, что так... ладно, пусть это делает за меня студия. Открываем свойства проекта DataCore, идем в Post-Build event и пишем следующее:


copy $(TargetDir)FAQEngineModel.csdl $(SolutionDir)FAQEngine\App_Data\
copy $(TargetDir)FAQEngineModel.msl $(SolutionDir)FAQEngine\App_Data\
copy $(TargetDir)FAQEngineModel.ssdl $(SolutionDir)FAQEngine\App_Data\


Здесь мы просто копируем нужные файлы в каталог App_Data нашего веб-приложения. Теперь остается чуть подправить строку подключения (в файле Web.config):

<add name="DataCoreConnection" connectionString="metadata=|DataDirectory|\FAQEngineModel.csdl||DataDirectory|\FAQEngineModel.ssdl||DataDirectory|\FAQEngineModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=.\SQLEXPRESS;Initial Catalog=FAQEngine;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />

Все работает!
Решение данной проблемы описано на форуме ADO.NET Entity Framework.


Ссылки по теме:
Обзор ADO.NET Entity Framework
Блог Sergey Rozovik - посты, посвященные EF
Работа с макросами в Pre- и Post-Build events

10 комментариев:

i-am-realist комментирует...

Спасибо за такие статьи. Разбираю твой блог уже второй день. Оч помог с пониманием MVC.

Electric Cat комментирует...

Спасибо!
Значит, мои усилия были не напрасны.

samvexler комментирует...

Цікаво!

Анонимный комментирует...

Спасибо, очень инересная и понятная! статья.

Анонимный комментирует...
Этот комментарий был удален администратором блога.
Petya комментирует...
Этот комментарий был удален администратором блога.
123 комментирует...

It is certainly interesting for me to read that article. Thanx for it. I like such themes and anything connected to this matter. I definitely want to read a bit more on that blog soon.
Alex
Phone jammers

MrSena комментирует...

Очень понятно написано. Спасибо.

Konstantin Goroxov комментирует...

Хм, попытался проделать все операции, но столкнулся с проблемой:

После автоматического создания Entity классов из БД , при переходе к классу Designer.cs -получаем пустой класс и ошибку в логах:

System.ApplicationException: Unable to remove the collapsed region from outlining manager, which means there is an internal consistency issue. at Microsoft.VisualStudio.Text.Outlining.OutliningManager.ExpandInternal(ICollapsed collapsed) at Microsoft.VisualStudio.Text.Outlining.OutliningManager.UpdateAfterChange(NormalizedSnapshotSpanCollection changedSpans) at Microsoft.VisualStudio.Text.Outlining.OutliningManager.SourceTextChanged(Object sender, TextContentChangedEventArgs e) at Microsoft.VisualStudio.Text.Utilities.GuardedOperations.RaiseEvent[TArgs](Object sender, EventHandler`1 eventHandlers, TArgs args)

Что с этим делать?

Анонимный комментирует...

Константин, какая у вас версия, студии, фреймворка?