ADO.NET Entity Framework. Часть 2.

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

Итак, продолжаем изучать ADO.NET Entity Framework.

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

Предположим, что перед нами теперь возникла необходимость дополнить объект User новыми полями, пусть это будет FIO и BirthDate. Для этого мы снова открываем диаграмму модели *.edmx и командой Right Mouse Click -> Add -> Scalar Property выполенной для сущности User создаем два новых свойства(Properties):

FIO (Type = String, Max Length = 255, Nullable = True)
BirthDate (Type = DateTime, Nullable = True)

Теперь сущность User имеет следующий вид:

Обратите внимание, что Nullable = True, а это значит, что при добавлении новых сущностей мы имеем право не инициализировать эти поля, а значит и в БД они будут иметь значение NULL.

После этого выполняем Right Mouse Click -> Generate Database from Model и получаем в свое распоряжение DDL-скрипт, полностью описывающий схему хранения данных для новой модели. Понятно, что более 90% скрипта будет повторять предыдущий, в таблицу Users добавятся два новых поля BirthDate и FIO:

CREATE TABLE [dbo].[Users] (
    [Id] int IDENTITY(1,1) NOT NULL,
    [Login] nvarchar(255)  NOT NULL,
    [Registered] datetime  NOT NULL,
    [FIO] nvarchar(255)  NULL,
    [BirthDate] datetime  NULL,
    [Group_Id] int  NOT NULL
);

Еще прошу обратить внимание, что DDL-скрипт начинается с вызова DROP CONSTRAINT и DROP TABLE, выполненных для всех таблиц и внешних ключей. То есть, выполнив этот скрипт мы потеряем все накопленные на текущий момент данные. Для нашего учебного примера это не страшно, но что если изменения нужно внести в уже работающий проект?

Мы поступим следующим образом. Сгенерим с помощью этого скрипта ДРУГУЮ базу. Пусть она будет называться [EFUSERS2]. Создадим ее сначала при помощи SQL Managment Studio, после чего выполним в ней весь скрипт MyEFModel.edmx.sql, только не забудьте сначала в скрипте поменять инструкцию USE в самом начале скрипта: USE [EFUSERS2] иначе вы сами того не желая внесете изменения в существущую базу EFUSERS удалив из нее все данные.

Итак, а нашем распоряжении теперь есть две базы, одна имеет старую структуру, но содержит в себе все накопленные данные, другая имеет новую структуру, но при этом пустая. Наша задача так обновить структуру старой базы, чтобы в ней сохранились все существующие данные. Мы воспользуемся для этого утилитой Adept SQLDiff.
После зппуска она предложит выбрать два подключения к БД, Primary Database (здесь мы выберем EFUSERS) и Secondary Database (выберем EFUSERS2).

После этого в графическом представлении утилита покажет нам все изменения в схеме данных, которые она сможет отыскать между двумя базами. Красным цветом будут выделена все различия между двумя БД. Осталось только сгенерировать скрипт разности, то есть тот скрипт, который после выполнения на Secondary Database внесет в нее все необходимые изменения. Скрипт это генерируется нажатием на кнопку “Show Changes to Left-hand DB”, она находится в панели инструментов прямо над деревом структуры данных. При этом скрипт будет сгенерирован только для выделенного узла дерева, а поэтому надо выделить самый верхний уровень иерархии Shema of EFUSERS. Полученный скрипт выглядит так:

alter table Users add
  FIO nvarchar(255),
  BirthDate datetime
go

Этот скрипт мы должны скопировать и выполнить для базы EFUSERS. Теперь в нашем распоряжении в таблице Users появились два новых поля FIO и BirthDate. Попробуем написать фрагмент программы, задающий значения этих полей для уже существующих пользователей нашей БД.

            using (MyEFModelContainer cnt = 
         new MyEFModelContainer("name=MyEFModelContainer"))
            {
                // Привязка к записям идет по ключам Id
                User userChitatel = new User()
                {
                    Id = 1
                };
                User userPushkin = new User()
                {
                    Id = 2
                };
                // Привязываем объекты к записям в таблице
                cnt.Users.Attach(userPushkin);
                cnt.Users.Attach(userChitatel);

                // Изменяем поля
                userPushkin.FIO = "Пушкин Александр Сергеевич";
                userPushkin.BirthDate = new DateTime(1799, 6, 6);

                userChitatel.FIO = "Гринин Вячеслав Николаевич";
                userChitatel.BirthDate = new DateTime(1980, 11, 12);

                // Сохраняем изменения в БД
                cnt.SaveChanges();
            }

Запросы, которые в результате выполнятся в БД кратки, в них нет ничего лишнего, что не может не радовать:

exec sp_executesql N'update [dbo].[Users]
set [FIO] = @0, [BirthDate] = @1
where ([Id] = @2)
',N'@0 nvarchar(255),@1 datetime,@2 int',
@0=N'Гринин Вячеслав Николаевич',
@1=''1980-11-12 00:00:00:000'',@2=1
go
exec sp_executesql N'update [dbo].[Users]
set [FIO] = @0, [BirthDate] = @1
where ([Id] = @2)
',N'@0 nvarchar(255),@1 datetime,@2 int',
@0=N'Пушкин Александр Сергеевич',
@1=''1799-06-06 00:00:00:000'',@2=2
go

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

using (MyEFModelContainer cnt = 
  new MyEFModelContainer("name=MyEFModelContainer"))
{
    // Создаем объект-группу и "присоединяем" ее к БД
    Group groupReader = new Group()
    {
        Id = 1 // читатели
    };
    cnt.Groups.Attach(groupReader);

    // Добавляем пользователей (пока просто создаем объекты в памяти)
    for (int i = 0; i < 5; i++)
    {
        User user = new User()
        {
            BirthDate = DateTime.Now,
            FIO = "Пользователь " + i,
            Login = "user" + i,
            Registered = DateTime.Now,
            Group = groupReader // Задаем группу
        };
        cnt.Users.AddObject(user);
    }
    // здесь произойдет добавление новых пользователей в БД
    cnt.SaveChanges();

    // Выбираем всех недавно добавленных пользователей(по их логинам, 
    // начинающимся с "user"). Здесь произойдет запрос 
    // SELECT LIKE к БД, так как мы явно вызвали метод ToList()
    List<User> users = cnt.Users.Where(
      u => u.Login.StartsWith("user")).ToList();

    // Пробегаем  в цикле по всем выбранным пользователям и удаляем их 
    // пока только из модели
    users.ForEach(u =>
    {
        cnt.Users.DeleteObject(u);
    });

    // Здесь произойдет физическое удаление DELETE из БД
    cnt.SaveChanges();
}

Итак, после выполнения первой инструкции SaveChanges() выполнится следующий запрос (я оставил лишь один из пяти одинаковых запросов вставки, и немного отформатировал его для удобочитаемости):

exec sp_executesql '
insert [dbo].[Users]
(
  [Login], 
  [Registered], 
  [FIO], 
  [BirthDate], 
  [Group_Id])
values 
(
  @0, 
  @1,
  @2, 
  @3, 
  @4
)
select 
  [Id]
from 
  [dbo].[Users]
where 
  @@ROWCOUNT > 0 
  and [Id] = scope_identity()'
,
'@0 nvarchar(255), @1 datetime, @2 nvarchar(255), @3 datetime, @4 int',
@0='user0',
@1=''2011-06-17 17:30:25:977'',
@2='Пользователь 0',
@3=''2011-06-17 17:30:25:977'',
@4=1
go

Достаточно своеобразная конструкция, давайте попробуем разобраться, что здесь произошло. Прежде всего выполняется системная процедура sp_executesql, первым аргументом для которой идет сам текст запроса, вторым аргументом – список и типы аргументов запроса, все остальные аргументы процедуры – собственно значения аргументов запроса. Надеюсь, я вас не запутал. Самое интересное здесь – это текст выполняемого запроса на вставку, он расположен со 2 по 21 строку листинга включительно. Как видно, первая часть – это сама вставка, а вторая часть – это по сути выборка scope_identity(), причем только в том случае, если вставка записи прошла успешно, о чем сигнализирует @@ROWCOUNT > 0 и тот факт, что scope_identity() присутствует среди набора Id таблицы Users.

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

Вернемся к рассмотрению листинга алгоритма манипуляции с данными. После выполнения операции ToList() (найдите ее в листинге), в БД отправится следующий запрос:

SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Login] AS [Login], 
[Extent1].[Registered] AS [Registered], 
[Extent1].[FIO] AS [FIO], 
[Extent1].[BirthDate] AS [BirthDate], 
[Extent1].[Group_Id] AS [Group_Id]
FROM [dbo].[Users] AS [Extent1]
WHERE [Extent1].[Login] LIKE N'user%'

Как видим, фреймворк преобразовал вызов метода StartsWith(“user”) в конструкцию WHERE [Login] LIKE N’user%’, что не может нас не порадовать.

Выполнение последней инструкции SaveChanges() вызовет выполнение пяти почти одинаковых запросов, похожих на этот:

exec sp_executesql N'delete [dbo].[Users]
where (([Id] = @0) and ([Group_Id] = @1))',N'@0 int,@1 int',@0=8,@1=1
go

Здесь мы можем заметить еще одну интересную особенность – удаление происходит не по одному критерию ([Id] = @0) как мы ожидали, а по двум ([Id] = @0) and ([Group_Id] = @1). То есть в критерий WHERE зачем-то была добавлена еще и группа Group_Id. Судя по всему, Entity Framework в критерий WHERE добавляет все первичные и внешние ключи таблицы. На всякий случай.

Проверка показала, что вот такой код вообще не хочет выполняться:

                User user = new User()
                {
                    Id = 29
                };

                cnt.Users.Attach(user);
                cnt.Users.DeleteObject(user);
                cnt.SaveChanges();

и на операции SaveChanges() выбрасывает вот такое исключение: Entities in ‘MyEFModelContainer.Users’ participate in the ‘UserGroup’ relationship. 0 related ‘Group’ were found. 1 ‘Group’ is expected. Что как бы намекает нам о необходимости явно указать еще и группу пользователя, что вызывает у меня некоторое недоумение. Как же мне удалить пользователя, заданного исключительно по Id? К сожалению, ничего умнее вот этого я не придумал:

cnt.Users.DeleteObject(cnt.Users.Where(u => u.Id == 30).FirstOrDefault());
cnt.SaveChanges();

Ну здесь все понятно: нашли первую (и единственную) запись, удовлетворяющую критерию Id=30, и удалили ее из контекста. При этом в БД ушел сначала SELECT, а затем его результаты были использованы для DELETE, при этом мы снова видим, что критерий по Group_Id так и остался.

SELECT TOP (1) 
[Extent1].[Id] AS [Id], 
[Extent1].[Login] AS [Login], 
[Extent1].[Registered] AS [Registered], 
[Extent1].[FIO] AS [FIO], 
[Extent1].[BirthDate] AS [BirthDate], 
[Extent1].[Group_Id] AS [Group_Id]
FROM [dbo].[Users] AS [Extent1]
WHERE 30 = [Extent1].[Id]

exec sp_executesql N'delete [dbo].[Users]
where (([Id] = @0) and ([Group_Id] = @1))',N'@0 int,@1 int',@0=30,@1=1

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

В тему:

2комментария

Спасибо за статью!)

Сергей, September 8, 2011 3:40 pm Reply

Обе статьи отличные! Я по 1й быстро перепрыгнул с LightSpeed для оракла yf sql server. Только один недостаток, как ж так, майкрософт пишут и движок базы данных, и среду разработки, а вот миграции изменений напрямую нету( Вот в лайтиспиде было так: можно обнговит ьструктуру как из базы, так и в саму базу, он лишь спрашивал – вот это удалить а это добавить? и расставив галочки на изменения схемы было достаточно. А тут – надо дублировать базы, эхх)

Alex, February 13, 2013 5:34 pm Reply
Ваше имя
Ваш email*
Ваш сайт
Текст вашего комментария:

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