Выполнение запросов
В прошлый раз мы рассмотрели различные способы создания запросов, однако не акцентировали внимание на том, когда и как они выполняются. Пришло время восполнить это упущение.За выполнение запросов в EF отвечает метод Execute уже знакомого нам класса ObjectQuery. В качестве аргумента метод Execute принимает параметр перечисления MergeOption. MergeOption определяет поведение кэша:
- AppendOnly: Добавлять в кэш только новые записи. Существующие (в кэше) сущности не обновлять;
- OverwriteChanges: Заменять текущие (current) значения существующих сущностей полученными значениями;
- PreserveChanges: Заменять исходные (original) значения существующих сущностей полученными значениями. При этом текущие значения не изменяются, следовательно все изменения, внесенные до сих пор, остаются в силе;
- NoTracking: Полученные значения не записываются в кэш
Рассмотрим небольшое тестовое приложение, которое демонстрирует, как значение MergeOption влияет на результат запроса:
Итак, после выполнения первой строчки у нас есть запись в кэше. Затем мы вручную меняем значение в базе и выполняем последующие запросы в четыре захода (для чистоты эксперимента).
using (FirstModel ctx = new FirstModel())
{
Person person = ctx.Persons.Where(p => p.PersonId == 1).First();
//После выполнения предыдущей строчки измените значение какого-нибудь свойства данной записи в базе вручную
ObjectQuery<person> personQuery = (ObjectQuery<person>)ctx.Persons.Where(p => p.PersonId == 1);
//Раскоментируйте поочередно нижележащие строчки и обратите внимание на возвращаемый результат
//Person person1 = personQuery.Execute(MergeOption.AppendOnly).First();
//person1.Person person2 = personQuery.Execute(MergeOption.OverwriteChanges).First();
//Person person3 = personQuery.Execute(MergeOption.PreserveChanges).First();
//Person person4 = personQuery.Execute(MergeOption.NoTracking).First();
}
- Полученное из базы значение игнорируется - возвращается значение из кэша;
- Текущие значения кэша подменяются значениями, полученными из базы - person2 содержит актуальные значения.
- Самый интересный случай. Как было описано выше, PreserveChanges лишь подменяет исходные значения, и не оказывает никакого влияния на текущие значения. Однако после выполнения теста в person3 будет содержаться актуальная информация из базы. Это связано с тем, что так как мы не производили никаких манипуляций с person, его текущие свойства не заданы. После выполнения запроса с PreserveChanges текущие значения останутся не заданными, поэтому в результате запроса будут возвращены исходные значения (которые к этому моменту уже были подменены на актуальные данные из базы). Если дополнить считывание person модификацией его свойств (например, если Вы планирует модифицировать в базе фамилию, достаточно дописать person.Surname = "AnySurname") - в person3 будут помещены текущие значения из кэша (в данном случае Surname будет равен AnySurname), а PreserveChanges, как ему и полагается, подменит лишь исходные значения.
- В случае NoTracking данные в кэш не заносятся и из кэша не считываются: person4 всегда будет содержать актуальные данные из базы.
Маппинг
Вот мы и добрались до самой "вкусной" возможности любой ORM-системы - до маппинга. К счастью, на эту тему уже написано достаточно статей, и повторяться я не буду. Я в очередной раз сошлюсь на статьи Сергея Розовика (раз, два), а опишу лишь маппинг Table splitting, которого в указанных статьях нет.Table splitting
Представьте, что у Вас есть таблица, которая помимо относительно легких атрибутов содержит и тяжелые, вроде varbinary(max) или filestream (на примере SQL Server), причем в большинстве случаев требуются лишь легкие атрибуты:
- Если мы храним в varchar(max) фотографию товара в высоком разрешении, то она понадобится лишь в тех редких случаях, когда пользователь интернет-магазина кликнет "посмотреть в полном размере";
- Если мы храним в filestream содержимое файла библиотеки, то значение этого атрибута понадобится лишь при скачивании файла; во всех остальных случаях необходимы лишь описывающие файл атрибуты: имя, размер, дата загрузки и т. д.;
- и т. д.
Добавим в таблицу Persons нашей базы данных (бэкап которой можно скачать в прикрепленном ко второй части файле) поле Photo типа varbinary(max). Для того чтобы не скачивать с сервера полуторамегабайтный jpeg каждый раз, когда нам нужна информация о пользователе, реализуем Table Splitting:
- В дизайнере скопируем и вставим сущность Person
- Переименуем полученную сущность в PersonPhoto (а ее Entity Set - в PersonPhotos) и в Mapping Details зададим Maps To Persons.
- Удалим из сущности Person свойство Photo, а из сущности PersonPhoto - свойства Name и Surname
- Добавим связь 1:1 между Person и PersonPhoto (правой кнопкой в окне дизайнера -> Add -> Association); затем выделим связь -> Mapping Details -> выберем Maps to Persons)
, Mapping Details сущности PersonPhoto:
и маппинг созданной связи:
Если сейчас попробовать сбилдить проект, в результате получим ошибку "Each of the following columns in table Persons is mapped to multiple conceptual side properties: Persons.PersonId is mapped to
Для того чтобы избавиться от ошибки, необходимо создать ReferentialConstraint, описывающий связь между PersonId сущностей Person и PersonPhoto. К сожалению, дизайнер в EF v. 1 этой возможности не поддерживает, поэтому закроем дизайнер -> щелкнем правой кнопкой по FirstModel.edmx в Solution Explorer -> Open With... -> XML Editor... -> в разделе CSDL найдем определение ассоциации PersonPersonPhoto:
<Association Name="PersonPersonPhoto">
<End Type="FirstModelModel.Person" Role="Person" Multiplicity="1" />
<End Type="FirstModelModel.PersonPhoto" Role="PersonPhoto" Multiplicity="1" />
</Association>
Вот теперь нас поджидает successful build.
<Association Name="PersonPersonPhoto">
<End Type="FirstModelModel.Person" Role="Person" Multiplicity="1" />
<End Type="FirstModelModel.PersonPhoto" Role="PersonPhoto" Multiplicity="1" />
<ReferentialConstraint>
<Principal Role="Person">
<PropertyRef Name="PersonId" />
</Principal>
<Dependent Role="PersonPhoto">
<PropertyRef Name="PersonId" />
</Dependent>
</ReferentialConstraint>
</Association>
Воспользуемся созданным маппингом и добавим запись в таблицу:
При этом будет сгенерирован один INSERT-запрос, в котором задаются все поля (в том числе и Photo), а это значит, что маппинг работает. Правда, есть один нюанс. Так, если при создании сущности необходимо оставить свойству Photo значение NULL, нельзя просто не задавать его, как это обычно делается со скалярынми свойствами. Загвоздка в том, что таблицы Person и PersonPhoto связаны 1:1, а связать их 1:0..1 невозможно, ибо связывается первичный ключ одной и той же таблицы. Поэтому необходимо явно присвоить свойству Photo экземпляр класса PersonPhoto:
using (FirstModel ctx = new FirstModel())
{
Person person = new Person();
person.Name = "X";
person.Surname = "Y";
PersonPhoto personPhoto = new PersonPhoto();
personPhoto.Photo = new byte[] { 1, 2, 3, 4, 5 };
person.PersonPhoto = personPhoto;
ctx.AddToPersons(person);
ctx.SaveChanges();
}
Считывать значения PersonPhoto можно следующими способами:
using (FirstModel ctx = new FirstModel())
{
Person person = new Person();
person.Name = "X";
person.Surname = "Y";
person.PersonPhoto = new PersonPhoto();
ctx.AddToPersons(person);
ctx.SaveChanges();
}
- можно считать непосредственно PersonPhoto (для него создан свой EntitySet), отобрав записи по PersonId;
- можно считать Person, а затем при помощи defered-loading подгрузить навигационное свойство PersonPhoto;
- а можно, считывая Person, загрузить навигационное свойство PersonPhoto при помощи eager-loading:
Сгенерированный в этом случае запрос радует глаз: вся загрузка сводится к одному простенькому SELECT'у (точно такому, какой бы Вы написали вручную).
using (FirstModel ctx = new FirstModel())
{
List<Person> persons = ctx.Persons.Include("PersonPhoto").ToList();
...
}
Заключение
В мои планы входило описание еще одного редко встречающегося типа маппинга - Table per concrete type (TPC). Однако столкнувшись с рядом ограничений, а также с несовместимостью Table splitting и TPC, отказался от этой идеи. Надеюсь, в следующей версии реализация TPC будет улучшена.
На этом посте данный цикл статей будет временно заморожен. Хотя некоторое время спустя я, наверное, к нему вернусь. В ближайшее же время Вас ждет новый цикл статей, который (уж не знаю, к счастью или к сожалению) опять-таки будет посвящен Entity Framework. До скорых встреч!
На этом посте данный цикл статей будет временно заморожен. Хотя некоторое время спустя я, наверное, к нему вернусь. В ближайшее же время Вас ждет новый цикл статей, который (уж не знаю, к счастью или к сожалению) опять-таки будет посвящен Entity Framework. До скорых встреч!