Thursday, September 18, 2008

Entity Framework: миграция с beta 3 на SP1

В этом посте я бы хотел рассказать про наш опыт миграции приложения с Entity Framework beta 3 на релизную версию, которая вышла около месяца назад. Поэтому он будет прежде всего полезен таким же неудачникам пострадавшим, как и мы, хотя, думаю, содержит полезную информацию и для тех, кто только собирается использовать EF в своих проектах.

Несколько месяцев назад, в ответ на пост Сергея Розовика о EF beta 3 breaking changes я немного самонадеянно в комментариях написал, что у нас с этим особых проблем быть не должно. Так оно и вышло: по списку проблем и не было :) Однако, забегая наперед, скажу, что были проблемы в других местах, где мы их и не ожидали. Ну да где наша не пропадала :) Итак, по порядку.

Удаление старой версии EF

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

Для Entity Framework beta 3 нужно:

1. Удалить ADO.NET Entity Framework Tools из “Add or Remove Programs"

2. Удалить ADO.NET Entity Framework 1.0 (Pre-Release version) из “Add or Remove Programs"

Для Entity Framework SP1 pre-release (или beta):

1. Закачать себе Visual Studio 2008 Service Pack Preparation Tool

2. Запустить его и удалить VS2008 SP1 pre-release

Инсталляция VS 2008 SP1

Инсталляция – процесс очень простой, но в то же время долгий, т.к. инсталлятор построен по принципу «скачай себя сам за 3 часа». Скачать релизную версию можно здесь. Если вам нужен только .NET 3.5 SP1 (ну, для сервера, например), то можно воспользоваться отдельным инсталлятором. По опыту скажу, что проще всего запустить инсталлятор вечером перед уходом домой (перед этим снеся предыдущую версию, конечно же), и утром он вас порадует сообщением, что я уже типа скачался и даже установился, осталось только шмакнуть кнопочку Restart Now.

Непосредственно миграция

Здесь начинается самое интересное. Для начала всем желающим следует ознакомиться со списками breaking changes для beta 3 и pre-release. Прочитайте их внимательно и обращайтесь к ним в первую очередь в случае, если у вас будут проблемы с компиляцией вашего кода или неправильной его работой.

Задача миграции сводится к 3-м большим пунктам:

1. Создать новую версию модели EDMX. Этот пункт, наверно, менее актуален для ребят, которые начали работать с pre-release или уже успели перевести свои модели на него, но я бы все равно рекомендовал его, потому что править XML и потом думать” WTF is going on?” – не самое приятное занятие.

2. Поправить свой код, который обращается непосредственно к контексту EF или его внутренностям согласно изменениям в API (как раз они неплохо описаны в списках breaking changes)

3. Поправить свой код в неявных местах, где он сломался вследствие каких-то других изменений в поведении сущностей или контекста (например, сериализации, как в нашем случае). Если вы достаточно удачливы :), то у вас все будет хорошо и так и этот пункт будет не важен.

Начнем по порядку. Заранее приношу извинения, если что-то будет не очень понятно – не хочу расписывать слишком подробно. Сначала займемся моделью:

1) Отставляем текущую версию файла EDMX в сторону или вообще исключаем из проекта, чтобы не путалась под ногами. Делаем Add -> New Item, выбираем ADO.NET Entity Data Model, потом Generate from database и дальше по накатанной, как обычно: выбираем таблицы, view, хранимые процедуры и т.д.

2) После того, как дизайнер услужливо построит модель, начинаем восстанавливать все наследования, удаляем ненужные ключи и переименовываем навигационные свойства, если нужно. Например, мне больше нравится видеть множественные имена (Orders, Users) для связей 1-to-* и *-to-*. Не забываем удалять ID из наследников и замапливаем их на ID родителя.

3) Заметил неприятный баг – для наследников все связи с другими сущностями почему-то теряются во время установки наследования. Поэтому восстанавливаем все маппинги навигационных свойств для наследников.

4) Добавляем хранимые процедуры в концептуальную модель. Важный шаг – не забудьте о нем.

5) Нажимаем validate для модели и вот здесь начинаются проблемы. Первая из них – это почему-то появляющийся для некоторых случаев <ReferentialConstraint>. Просто избавляемся от него через XML.

6) Следующими падают связи 1-to-1, которые прекрасно работали в beta 3. Возможно, теперь нужно что-то добавить для того, чтобы они заработали, или концепция просто поменялась – в любом случае пофиксили мы это через добавление primary key и изменение типа физичиской связи на 1-to-*, которое просто в концептуальной модели замапливается как 1-to-1. Немного криво, но работает.

7) Следующая проблема оказалась посерьезнее предыдущих и на нее вкупе было потрачено намного больше времени. Ни с того, ни с сего для сущностей, которые имеют несколько связей с какой-то другой сущностью или просто имеют связь саму на себя, начали выскакивать следующие сообщения об ошибках валидации:

"Error 3034: Problem in Mapping Fragments starting at lines 6250, 7344: Two entities with different keys are mapped to the same row. Ensure these two mapping fragments do not map two groups of entities with different keys to two overlapping groups of rows."

"Error 3034: Problem in Mapping Fragments starting at lines 2495, 2504: An entity from one EntitySet is mapped to a row that is also mapped to an entity from another EntitySet with possibly different key. Ensure these two mapping fragments do not map two unrelated EntitySets to two overlapping groups of rows."

Поколупавшись немного в схеме и погуглив, я-таки нашел причину и выход. Оказывается, теперь в таких случаях EF ожидает элемент <Condition> (выход нашелся в одной из веток форума MSDN, можно почитать еще и про вторую ошибку). Зачем это ему нужно, сказать сложно, но лечится это прямо так, как написано на форуме – добавлением этого элемента. Иногда возникает необходимость добавить этот же элемент и в парную связь. Например:

<AssociationSetMapping Name="FK_Entity1_UserCreated" TypeName="SomeNamespace.FK_Entity1_UserCreated" StoreEntitySet="Entity1">
  <EndProperty Name="User">
    <ScalarProperty Name="Id" ColumnName="CreatedBy" />
  </EndProperty>
  <EndProperty Name="Entity1">
    <ScalarProperty Name="Id" ColumnName="Id" />
  </EndProperty>
  <Condition ColumnName="CreatedBy" IsNull="false" />
</AssociationSetMapping>
<AssociationSetMapping Name="FK_Entity1_UserUpdated" TypeName="SomeNamespace.FK_Entity1_UserUpdated" StoreEntitySet="Entity1">
  <EndProperty Name="User">
    <ScalarProperty Name="Id" ColumnName="UpdatedBy" />
  </EndProperty>
  <EndProperty Name="Entity1">
    <ScalarProperty Name="Id" ColumnName="Id" />
  </EndProperty>
  <Condition ColumnName="UpdatedBy" IsNull="false" />
</AssociationSetMapping>

8) Исправляем все остальные возникшие ошибки валидации до тех пор, пока валидатор перестанет ругаться.

Далее начинаем изменять код. Нам пришлось пофиксить всего 4 места:

1) Заменить вызов GetEntityKey() на CreateEntityKey(). Замечу, что эта замена нигде в breaking changes не описана, пришлось действовать наощупь :)

2) Заменить свойство QueryTimeout на CommandTimeout (только для тех, кто мигрирует с beta 3)

3) Поменять connection strings для edmx-файлов. Если раньше для того, чтобы к ним добраться, приходилось извращаться различными способами, то теперь они автоматически билдятся в сборку и доступны из ресурсов через строку подключения наподобие этой:

metadata=res://*/Model.csdl|res://*/Model.ssdl|res://*/Model.msl;provider=System.Data.SqlClient;provider connection string=...

4) С некоторых пор KnownTypeAttribute стал автоматически добавляться в сгенерированные сущности, поэтому больше нет нужды добавлять его самим с помощью указания статического метода, возвращающего массив типов. Более того, теперь компилятор ругается на то, что у нас используется KnownTypeAttribute с указанием метода и другие его вариации для одного и того же класса, поэтому нужно просто удалить атрибут с методом.

5) Продолжаем в том же духе, пока у вас не останется ошибок компиляции

6) Ребилдим custom tool, если таковой был использован для кастомизации кодогенерации модели – очень полезная штука, рекомендую (примеры здесь и здесь). Мы через нее реализовали нормальный lazy load, который, впрочем и сыграл с нами злую шутку :) Подробности этой реализации я опущу до лучших времен, если кому интересно увидеть раньше – отпишите в комментах, я сделаю отдельный пост быстрее.

Итак, мы закончили с кодом. Те же, кто рискнул использовать сериализацию сущностей через DataContractSerializer в своих целях, читаем дальше :) Для меня до сих пор остается загадкой, почему навигационные свойства для EntityCollection и не-Reference навигационные свойства для EntityReference<T> оказались помеченными атрибутом DataMemberAttribute (то есть сериализуемые через DataContractSerializer), хотя они же помечены атрибутами SoapIgnoreAttribute и XmlIgnoreAttribute (то есть остальные сериализаторы их игнорируют), но видимо, так надо. Однозначно одно: в нашем случае сериализация этих свойств приводила к тому, что мы выкачивали ВСЮ базу данных (т.к. нас включен lazy loading на уровне кодогенерации) и пытались ее сериализовать, что приводило к неминуемому OutOfMemoryException. Мой вопрос на форумах MSDN остался без ответа, пришлось разбираться самому. Были разные варианты решения этой проблемы вплоть до того, чтобы отключать lazy load на время сериализации сущностей, но в результате пофиксили мы эту проблему просто и радикально – закомментировав ненужные DataMemberAttribute атрибуты через все ту же кастомную кодогенерацию при помощи гениальной строчки кода:

string formattedCode = code.Replace(
"[global::System.Xml.Serialization.SoapIgnoreAttribute()]\r\n        [global::System.Runtime.Serialization.DataMemberAttribute()]",
"[global::System.Xml.Serialization.SoapIgnoreAttribute()]\r\n        //[global::System.Runtime.Serialization.DataMemberAttribute()]");

Не правда ли, просто? Конечно, сама реализация достаточно тупая, но зачем придумывать что-то более гибкое, если и так работает на 100% эффективно. Вот когда выйдет EF v2 и в нем еще что-то поменяется в генерации – будем смотреть :)

Итого: миграция прошла успешно, осталось лишь установить SP1 на все девелоперские станции и сервера. Удачи и вам. Будут вопросы – пишите в комментарии, постараюсь помочь.

No comments:

Post a Comment