Практикуемся в ADO.NET Entity Framework. Model First.

Вячеслав Гринин, June 6, 2011

Сразу оговорюсь, какое отношение имеет цикл статей про Entity Framework к веб-программированию. В дальнейшем я собираюсь использовать полученную модель для создания веб-приложения на базе технологии ASP.NET MVC 3, которая очень хорошо сочетается с Entity Framework.

Прежде всего – вам нужно иметь установленную Visual Studio 2010 (кажется 2008 тоже подойдет) и установленный пакет ADO.NET Entity Framework 4.1 найдите его по ссылке или в поиске на microsoft.com. Теперь нам становятся доступны все возможности ADO.NET Entity Framework. Замечу, что четвертая версия отличается от более ранних, так что, если у вас установлена более ранняя версия, то не гарантирую, что у вас будет работать тот код, что я привел в статье.

Итак, создаем обычное консольное приложение. Присваиваем ему имя test1. Добавляем в проект модель данных.

Add -> New Item... -> ADO.NET Entity Data Model

, называем ее MyEFModel.edmx.

Среда разработки предлагает нам два варианта создания модели: генерация из базы данных и пустую модель. В этой статье мы собираемся рассмотреть принцип Model First, то есть начинать разработку мы будет с создания модели данных, из которой впоследствии будет сгенерирована схема данных (таблицы и связи в базе данных). А это значит, что мы выберем вариант создания пустой модели (Empty Model). После этого перед нами откроется пустое поле дизайнера модели данных. Если дизайнер не открылся, то сделайте двойной клик на модели MyEFModel.edmx в Solution Explorer’е.

Итак, мы хотим спроектировать модель данных для проверки пользовательских прав на те или иные операции. А значит в модели данных у нас будут присутствовать следующие сущности: Пользователь (User), Группа (Group) и Право (Right). Каждый пользователь обязательно принадлежит какой либо группе, и при этом строго одной. Каждая группа содержит в себе несколько прав, или не содержит ни одного. Все просто – при регистрации нового пользователя создается одна сущность User, у которой есть свойство Group, привязывающее пользователя к конкретной группе пользователей(например, администраторы, модераторы, авторы, читатели, посетители). Группа в нашем случае это набор прав (таких как: “блокирует пользователя”, “редактирует чужую статью”, “создает статью”, “читает статью” и т.д.).

Перед тем как создавать сущности, давайте скажем дизайнеру модели, по каким правилам будут формироваться имена коллекций сущностей. Я вот о чем. Одновременно с сущностью в модели создается также контейнер этих сущностей, то есть по сути коллекция. Аналогия с таблицей в БД прямая: сущность – это строка в таблице, контейнер сущностей – сама таблица. Дизайнер дает имя контейнеру исходя из имени сущности (имя контейнера, впрочем, всегда можно поменять), и для сущности User, контейнер он может назвать UserSet или Users. По мне так название Users гораздо приятнее. А потому для самой модели в свойствах мы выставим Pluralize New Objects = True.

Итак, создаем сущности(сущности создаются правым кликом на диаграмме модели данных и выбором контекстного меню Add -> Entity…):

1)
Entity Name = User
Entity Set = Users
Остальные свойства оставим без изменений. Заметим лишь, что по умолчанию дизайнер модели создает первичный ключ Id целочисленного типа, что для нас вполне приемлемо.
Добавляем в эту сущность свойства(Properties):
Login (Type = String, Max Length = 255)
Registered (Type = DateTime)
Свойства добавляются в контекстном меню самой сущности (Add -> Scalar Property).

2)
Entity Name = Group
Entity Set = Groups
Свойства:
Name (Type = String, Max Length = 255)

3)
Entity Name = Right Entity Set = Rights
Свойства:
Description (Type = String, Max Length = 255)

Итак, теперь у нас есть три сущности, пока еще не связанные друг с другом. Займемся этим вопросом.
Создаем связи (Add -> Association):

1)
Association Name = UserGroup
Начало: Entity = User, Multiplicity = Many
Конец: Entity = Group, Multiplicity = One
Это соотношения “Один ко многим”, то есть каждый пользователь может состоять только в одной группе, но в каждой группе может быть много пользователей. (Если бы у сущности Group мы выбрали Multiplicity = Zero or One, то мы бы получили отношение “Один ко многим”, но дали бы при этом возможность иметь пользователей не принадлежащих ни одной группе, то есть у пользователя свойство Group было бы Nullable).

2)
Association Name = GroupRight
Начало: Entity = Group, Multiplicity = Many
Конец: Entity = Right, Multiplicity = Many
Это соотношение “Многие ко многим”, то есть группа содержит в себе множество прав, при этом каждое право может принадлежать нескольким различным группам.

Вот какая схема данных у нас появилась:

Обратите внимание, что после создания связей, в каждой из сущностей появились дополнительные навигационные свойства. Например, свойство Group сущности User дает нам возможность узнать группу, в которой состоит пользователь, а свойство Rights сущности Group дает нам возможность получить список всех прав группы. Таким образом, обратившись к concreteUser.Group.Rights мы получим список прав пользователя. Как видно из схемы данных, среда разработки создала также и встречные (обратные) навигационные свойства Right.Groups и Group.Users.

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

Правый щелчок мыши на диаграмме, затем в контекстном меню “Generate Database from Model…“. Здесь мы можем выбрать существующее подключение к БД или создать новое. Мы создадим новое подключение (кнопка New Connection…), здесь вы выбираете сервер БД, способ и параметры аутентификации, и имя базы данных (если хотите создать новую, то просто введите имя новой БД в поле Select or enter a database name), жмем OK. (Я создал базу EFUSERS на локальном компьютере). Мастер спросит вас нужно ли создать БД, вы соглашайтесь. После этого мы снова вернемся в мастер генерации БД, убедитесь, что галочка “Save entity connection settings in App.config as” установлена и нажмите Next. После этого во вкладке DDL мастер отобразит вам DDL-скрипт схемы данных. Здесь вы нажмите Finish.

После работы мастера мы получили следующее:
1) скрипт схемы данных в файле MyEFModel.edmx.sql,
2) строка подключения MyEFModelContainer в файле App.config (откройте его и посмотрите в раздел сonnectionStrings)
3) пока еще пустая база данных EFUSERS.

Мастер всего лишь создает, но не выполняет DDL-скрипт и это правильно, потому что скрипт этот при выполнении уничтожает все данные в БД. Понятно, что при первом выполнении скрипта это нормально, ведь данных в БД еще нет. Но вот при последующих изменениях в модели данных и генерации новой схемы данных это может сыграть роковую роль и уничтожить все созданные ранее данные в базе. В последующих статьях мы еще рассмотрим способы внесения изменений в БД без потери существующих данных.

Теперь нам нужно выполнить DDL-крипт, чтобы создать все необходимые объекты в БД. Вы можете выполнить файл скрипта в Query Analizer, а можете сделать это прямо в Visual Studio, открыв скрипт и нажав кнопку Execute SQL в панели инструментов, или нажав комбинацию Ctrl+Shift+E. Студия запросит у вас параметры подключения к БД а затем выполнит скрипт. Открыв теперь базу в Enterprise Manager мы можем увидеть, что там создано 4 таблицы: Users, Rights, Groups, GroupRight. Первые три таблицы хранят в себе сущности трех видов, а вот четвертую Junction-таблицу (GroupRight) Entity Framework создал для того чтобы хранить отношения “Многие ко многим” между сущностями Group и Right. Как видим, при помощи подхода Model First мы полностью ушли от этапа проектирования таблиц в БД, всю работу Entity Framework сделал за нас. Обратим также внимание, что в таблице Users появилась колонка Group_Id ссылающаяся на группу, в которую входит пользователь, колонка эта NOT NULL, и это гарантирует что пользователь обязательно будет принадлежать той или иной группе. Ниже приведена схема БД, на которой отображены поля таблиц, первичные и внешние ключи.

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

Я не буду приводить здесь DDL-скрипт, сгенерированный студией, но он доступен для ознакомления по ссылке MyEFModel.edmx

Ну вот, вся предварительная работа сделана, модель данных нарисована, схема данных создана, пустые таблицы в БД присутствуют. Осталось теперь попробовать поработать со всем этим добром.

Чтобы обратиться к данным в БД, нужно создать экземпляр контейнера модели. Класс контейнера называется MyEFModelContainer, инстанцируем его:

MyEFModelContainer cnt = 
  new MyEFModelContainer("name=MyEFModelContainer");

В качестве аргумента конструктора мы задаем имя строки подключения к БД. Эта строка подключения представляет собой нечто большее, чем обычная ConnectionString для подключения к базе данных, потому что в нашем случае используется провайдер System.Data.EntityClient, которому для инициализации требуется гораздо больше параметров. Строка подключения была создана мастером ранее и хранится в файле App.config, строка подключения выглядит так:

metadata=res://*/MyEFModel.csdl|res://*/MyEFModel.ssdl|res://*/MyEFModel.msl;provider=System.Data.SqlClient;provider connection string="data source=.;initial catalog=EFUSERS;integrated security=True;multipleactiveresultsets=True;App=EntityFramework"

Посмотрите внимательно, строка подключения имеет в себе три обязательных параметра: metadata, provider и provider connection string. Первый параметр задает ссылки на файлы CSDL, SSDL и MSL моделей. Все эти три файла хранятся в папке edmxResourcesToEmbed(найдите ее в дереве каталогов проекта) и представляют собой XML-файлы, описывающие соответственно: CSDL – концептуальную(объектную) модель в терминах бизнес-уровня, SSDL – схема хранения (реляционная схема), MSL – схема сопоставления (то есть связи между элементами CSDL и SSDL). Собственно все эти XML-файлы объединены в один с расширением EDMX.

Параметры provider и provider connection string задают как раз привычную нам строку подключения к БД.

Конструктор класса MyEFModelContainer можно вызвать также совсем без параметров, тогда по умолчанию строка подключения будет взята из App.config и станет равна той строке подключения, для которой собственно генерировался изначально DDL-скрипт.

В качестве аргумента конструктора класса MyEFModelContainer можно подать также экземпляр класса EntityConnection, который есть по сути объектная обертка вокруг строки подключения, мы не будет усложнять код, потому что конструктор класса MyEFModelContainer все равно сделает эту работу за нас.

Итак, теперь у нас есть контейнер данных, который помимо огромного количества прочих полезностей, содержит в себе три коллекции: Users, Groups, Rights – это те самые коллекции, ради которых собственно все и затевалось. Операции, производимые нами над этими коллекциями, автоматически (ну или почти автоматически) отображаются в базу данных, добавляя, удаляя или изменяя строки в соответствующих таблицах. Попытка чтения из коллекции приведет к выполнению SELECT-запроса к соответствующей таблице.

Пока наши таблицы пусты попробуем добавить в них некоторые данные. Пусть мы хотим, чтобы существовали два пользователя: chitatel и pushkin. Первый по статусу положено только чтение статей, второму – и чтение и запись. Понятно, что никаких статей у нас пока нет, важно только само разграничение прав на операции. При этом chitatel принадлежит группе “Читатели”, а pushkin – группе “Писатели”. Таким образом нам нужно создать два права: “Читать статьи” и “Редактировать статьи”; две группы: “Читатели” и “Писатели”; двух пользователей: chitatel и pushkin. А также – создать между ними корректные связи. Приведу сразу кусок кода, который надо выполнить единожды для того, чтобы создать все необходимые записи в таблицах.

// Создаем объекты прав
Right canRead = new Right()
{
  Description="Читать статьи",
};
Right canWrite = new Right()
{
  Description="Редактировать статьи",
};
// Создаем объекты групп
Group groupReaders = new Group()
{
  Name = "Читатели"
};
Group groupWriters = new Group()
{
  Name = "Писатели"
};
// Присваиваем группам соответствующие права
groupReaders.Rights.Add(canRead);  // читатели могут только читать
groupWriters.Rights.Add(canRead);  // писатели могут и читать
groupWriters.Rights.Add(canWrite); // и писать

// Создаем объекты пользователей и заносим их в соответствующую группу
User userChitatel = new User()
{
  Login = "chitatel",
  Registered = DateTime.Now,
  Group = groupReaders
};

User userPushkin = new User()
{
  Login = "pushkin",
  Registered = DateTime.Now,
  Group=groupWriters
};

// Теперь все эти существующие исключительно в памяти объекты
// добавляем в соответствующие коллекции контейнера
using (MyEFModelContainer cnt = 
  new MyEFModelContainer("name=MyEFModelContainer"))
{
  cnt.Rights.AddObject(canRead);
  cnt.Rights.AddObject(canWrite);
  cnt.Groups.AddObject(groupReaders);
  cnt.Groups.AddObject(groupWriters);
  cnt.Users.AddObject(userChitatel);
  cnt.Users.AddObject(userPushkin);
  // И финальный аккорд - сохраняем все изменения в БД
  cnt.SaveChanges();
}

После выполнения приведенного кода мы получим вот такие данные в таблицах:

Рассмотрим теперь алгоритм выборки данных из контейнера. Я для этих целей буду использовать язык встроенных запросов LINQ.Предположим, что некий пользователь вошел в систему, и нам нужно определить, имеет ли он право редактировать статьи. То есть мы должны по имени пользователя получить его группу и посмотреть, имеет ли эта группа соответствующее право.

Для этого мы выполним следующий код:

            using (MyEFModelContainer cnt = 
                     new MyEFModelContainer("name=MyEFModelContainer"))
            {
                var check = from u in cnt.Users
                            where u.Login == "pushkin" && 
               u.Group.Rights.Any(f => f.Description == "Редактировать статьи")
                            select u;
                Console.WriteLine(check.Count());
            }

Дословно, LINQ-запрос выбирает из БД всех пользователей, имеющих заданный логин(то есть единственного пользователя), и в правах группы в поле Description имеющих содержимое “Редактировать статьи”. Если такой пользователь существует, то check.Count() == 1, а значит пользователь имеет требуемое право.
Текст SQL-запроса, выполненного в БД, выглядит так:

SELECT 
[GroupBy1].[A1] AS [C1]
FROM ( SELECT 
	COUNT(1) AS [A1]
	FROM [dbo].[Users] AS [Extent1]
	WHERE (N'pushkin' = [Extent1].[Login]) AND ( EXISTS (SELECT 
		1 AS [C1]
		FROM  [dbo].[GroupRight] AS [Extent2]
		INNER JOIN [dbo].[Rights] AS [Extent3] ON [Extent2].[Rights_Id] = [Extent3].[Id]
		WHERE (N'Редактировать статьи' = [Extent3].[Description]) AND ([Extent1].[Group_Id] = [Extent2].[Groups_Id])
	))
)  AS [GroupBy1]

Здесь мы видим безобразный SELECT FROM SELECT отягощенный EXISTS-ом и INNER JOIN-ом. На мой взгляд, запрос далеко не оптимален. Немного упростить запрос, убрав из него INNER JOIN можно, если искать права не по названию (“Редактировать статьи”), что в любом случае являет плохим тоном, потому что зависит от способа написания названия права, а искать по Id права, ведь согласитесь сопоставление ID и DESCRIPTION в нашей базе всегда будет сохраняться. Таким образом, упрощаем LINQ-запрос:

                var check = from u in cnt.Users
                            where u.Login == "pushkin" && 
         u.Group.Rights.Any(f => f.Id == 2)
                            select u;

И получаем слегка укороченный SQL-запрос:

SELECT 
[GroupBy1].[A1] AS [C1]
FROM ( SELECT 
	COUNT(1) AS [A1]
	FROM [dbo].[Users] AS [Extent1]
	WHERE (N'pushkin' = [Extent1].[Login]) AND ( EXISTS (SELECT 
		1 AS [C1]
		FROM [dbo].[GroupRight] AS [Extent2]
		WHERE (2 = [Extent2].[Rights_Id]) AND ([Extent1].[Group_Id] = [Extent2].[Groups_Id])
	))
)  AS [GroupBy1]

Я думаю, что на начальном этапе создания проекта, когда таблицы будут еще не слишком большими, подойдет и этот запрос, в дальнейшем же мы просто будем выносить все часто используемые не слишком оптимальные запросы в отдельные оптимизированные хранимые функции и процедуры. К счастью в Entity Framework 4 заложена возможность вызова хранимых процедур и функций, и не просто их вызова, а даже использования их в LINQ-запросах, что согласитесь очень хорошо.

Итак мы научились создавать модель данных, генерировать из нее схему данных, создавать сущности и делать из них выборку.

В следующей статье я планирую коснуться изменения (UPDATE) сущностей и изменения схемы данных (ALTER), создания и использования хранимых функций и процедур.

Проект можно скачать здесь test1

В тему:

21комментарий

Спасибо за статью. Очень помогло.

Антон, October 29, 2011 5:07 pm Reply

будет ли это работать в visual studio 2010 express
кнопки Execute SQL на панели инструментов я не нашел(

Константин, January 16, 2012 8:14 am Reply

В этом случае придется выполнять sql-код в других продуктах, например, sql management studio

Вячеслав Гринин, January 17, 2012 8:47 am Reply

Установил полную студию, но установить соединение с sql сервером не удается, создал бд sql compact, т.е. расширение скрипта ddl созданного по модели не .sql а .sqlce выполняю скрипт-успешно, но при работе кода C# в строке SaveChanges() вылезает ошибка. причину понять не могу

Константин, January 17, 2012 10:35 am Reply

Нужно смотреть какая ошибка.

Вячеслав Гринин, January 17, 2012 3:19 pm Reply

ADO.NET Entity Framework – это сложная система. Для того чтобы ее быстро изучить надо иметь некии базовые знания и для их получения нужно потратись время. В стыкался с ADO.NET при програмировании в Делфи, но пост нужный – Спасибо !

collaps, February 11, 2012 3:53 pm Reply

При попытке открыть соединение вываливается исключение “Не удалось загрузить указанный ресурс метаданных”. В чем может быть дело? Уже голову сломала…

FireFly, March 28, 2012 4:51 pm Reply

Проверьте, существуют ли в вашем проекте файлы MyEFModel.csdl, MyEFModel.ssdl, MyEFModel.msl

Вячеслав Гринин, March 28, 2012 7:55 pm Reply

на сколько я понимаю, эти файлы объединены в один файл Model.edmx, этот файл есть

FireFly, March 29, 2012 7:03 am Reply

вот кусок кода
{
SqlConnectionStringBuilder sql = new SqlConnectionStringBuilder();
sql.DataSource = “smirnova\\sqlexpress”;
sql.InitialCatalog = “model_db”;
sql.IntegratedSecurity = true;
string providerString = sql.ToString();
EntityConnectionStringBuilder conn = new EntityConnectionStringBuilder();
conn.Provider = “System.Data.SqlClient”;
conn.ProviderConnectionString = providerString;
conn.Metadata = @”res://*/Model.csdl|res://*/Model.ssdl|res://*/Model.msl”;
using (connect = new EntityConnection(conn.ToString()))
{
connect.Open();
ModelContainer container = new ModelContainer(connect);
dataGridView1.DataSource = container.Users;
connect.Close();
}
}

FireFly, March 29, 2012 7:31 am Reply

Сложно так сразу сказать в чем ошибка. Как видите, я примере я подключался несколько иначе – прописал в App.config строку подключения и обращался к ней по имени ModelContainer cnt = new ModelContainer(“name=MyEFModelContainer”);

Вячеслав Гринин, March 29, 2012 8:36 am Reply

Файл Model.edmx лежит в папке ресурсов проекта?

Вячеслав Гринин, March 29, 2012 8:37 am Reply

Файл Model.edmx находится в корне проекта. Подключение через App.config не подойдет, поскольку пользователь в приложении должен указать адрес сервера, на котором будет БД.

FireFly, March 29, 2012 10:54 am Reply

Попытка подключения через строку в App.config приводит к этому же исключению.

FireFly, March 29, 2012 10:55 am Reply

Оно возникает в строке connect.Open();

FireFly, March 29, 2012 10:56 am Reply

В общем, причину так и не нашла, но проблема возникает только при проектировании по методу “Model First”, при использовании метода “Database First” все отлично (по крайней мере пока :) ). Буду при определении контейнера указывать строку из App.config, изменяя имя сервера с БД.
P.S. Спасибо за помощь)

FireFly, March 29, 2012 3:03 pm Reply

Попробуйте создать два одинаковых проекта нор разными методами: сначала modelfirst, а потом сделайте новый проект из этой же базы по методу dbfirst. И сравните структуру каталогов, и вид строки подключения, может натолкнет на мысль.

Вячеслав Гринин, March 30, 2012 8:26 am Reply

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

FireFly, March 30, 2012 8:41 am Reply

Подскажите как быть. Я разрабатываю веб-сайт. БД я создавал в Mysql 5.5.2. Подключение бд к вижуал студио я делал через Mysq Entity Connector и потом создавал модель ADO.NET.EDM, в которой затем сущности связал ассоциациями. Но дело в том, что я не знаю будет ли открываться бд на другом ПК. Если сделать бд в sql server, то эта бд будет хранится в самом проекте в папке App Data в формате mdf, и ее можно будет потом без проблем открыть на любом ПК. Но у меня в проекте эта папка пуста и получается, что бд физически не существует в проекте и соответственно на другом ПК я не смогу открыть БД.
Что мне делать и как быть в этой ситуации? (если что, то я не особо сильно шарю во всем этом. Я только начинающий) Не охото заново создавать бд в sql server, да и не смогу я просто.

Дмитрий, May 9, 2012 1:49 am Reply

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

Дмитрий, May 9, 2012 1:51 am Reply

Доброго времени суток. Я только начал изучать Entity Framework. У меня возник вопрос. При выполнении кода … cnt.SaveChanges(); выдаёт ошибку “UpdateException не обработано: При обновлении записей произошла ошибка. Подробные сведения см. во внутреннем исключении.” Не могли бы Вы подсказать, как её исправить?

Сергей, February 5, 2015 10:46 am Reply
Ваше имя
Ваш email*
Ваш сайт
Текст вашего комментария:

Поиск по блогу:
Подписаться:
Популярные:
Облако тегов:
Разное:
Счетчик: