Showing posts with label Entity Framework. Show all posts
Showing posts with label Entity Framework. Show all posts

Monday, March 22, 2010

Список интересных подкастов: выпуск #4

Новая подборка интересных подкастов, которые я бы рекомендовал к прослушиванию. Сегодня разбиваем на категории:

.NET 4.0:

Jason Olson проходит по некоторым новым фичам CLR, C# и BCL. Concurrent GC, side-by-side CLR versions, Memory-Map files, co-variants и contra-variants, Parallel Extensions, немного об обновлениях в языках программирования и новых языках. И да, они наконец-то выкинули CAS и заменили его на более простой механизм. Хотя кто его вообще использовал :)

Небольшой обзор изменений в ASP.NET AJAX 4.0. От UpdatePanel до полностью кастомных AJAX-запросов и JavaScript-контролов. jQuery теперь "часть" ASP.NET AJAX, переработанный AJAX Control Toolkit - тоже, более того, AJAX Control Toolkit теперь работает не только с WebForms и MVC, но даже с PHP или любым другим back-end'ом. ASP.NET AJAX поддерживает dual data-binding на клиенте, который работает по аналогии с дата контекстом EF или L2S, на сервере его поддерживает WCF Data Services, которые раньше назывались ADO.NET Data Services. На закуску немного о ScriptLoader, history API, Microsoft CDN, Microsoft AJAX Minifier, Sea Dragon и Deep Zoom.

Честно говоря, еще сам не слушал этот подкаст, но судя по транскрипту - довольно любопытно. Foreign Keys, POCO и поддержка DDD разработки и модульного тестирования, Lazy/Deferred Loading, поддержка N-Tier сценариев при помощи Self-tracking entities, T4 templates, Code Only (в общем, все то, о чем я уже когда-то писал), немного о WCF Data Services, а также история о том, как Julie помогала Oren Eini (Ayende) создавать EFProf.

Обсуждение возможностей первого функционального языка программирования, включенного в .NET 4.0. Немного о том, что такое функциональные языки вообще, зачем нужен функциональный язык в .NET и какую пользу он может принести обычным разработчикам, привыкшим к императивным языкам вроде C#, VB.NET, Java или С++, некоторый ликбез по терминологии функциональных языков. Много интересного контента и шуток благодаря Теду.

Интервью с програм менеджером DLR team. Конечно же, обсуждение DLR, который релизится с .NET 4.0, и его двух основных языков: IronRuby и IronPython. Разговаривают об архитектуре DLR, истории развития, возможностях, отличиях Iron-версий языков Ruby и Python от своих оригиналов, IronRuby on Rails :)

Другое:

И снова непревзойденная Tess. О чем еще с ней можно поговорить, как не об отладке. Что такое Memory Dump, как его создавать и почему об этом важно знать, что такое Allocated и Committed memory, как пользоваться WinDbg и CDB для отладки "утечек памяти" в .NET и проблем с производительностью, а также некоторые улучшения с отладкой многопоточных приложений в VS2010 и в предыдущих версиях, о которых вы, возможно, не слышали.

Интервью с создателем WebFormsMVP - очень интересного MVP-движка для WebForms. Автор рассказывает, что такое MVC и MVP, зачем использовать паттерн MVP в WebForms приложениях, архитектуре движка, особенностях его использования и том, насколько полно он работает с остальной инфраструктурой ASP.NET. Надо будет обязательно попробовать его в боевом проекте - по идее, должен сберечь кучу времени и сил.

Приятно, что в нашем уголке мира появляются хорошие подкасты :) В гостях у питерской группы ALT.NET подкаста Александр Бындю и Виталий Стахов. Разговор идет о вполне понятных вещах: принципах ООП (SOLID), TDD, практиках XP, полезных инструментах, развитии программиста и различных вариантах построения и развития команд. Прикольно, что Александр Бындю точно так же понимает разницу между Scrum и XP, как и я.

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

Обсуждение Domain-Driven Design и CQRS (Command and Query Responsibility Segregation pattern) с Мишей Чалым и frozen_space. Если про DDD многие хотя бы краем уха слышали, то что такое CQRS для меня для самого тайна :) Где помогает DDD и зачем он нужен, что такое CQRS, его преимущества и недостатки. В общем, самое острие программной инженерии - не пропустите! Подкаст только записали, завтра буду слушать сам.

И напоследок, как всегда, тема попроще :)

Скотт общается с Warren Sande и его 10-летним сыном об их опыте написания книги по обучению программированию для детей и других начинающих программистов. Вроде бы ничего особенного, но удивляет этот 10-летний малыш. Мало того, что он помогал отцу в написании книги и примеров для нее на Питоне, но он еще и рассуждает обо всем по-взрослому. Интересно, он просто настолько умен или изучение программирования действительно ускоряет развитие у детей?

Приятного прослушивания!

Sunday, November 15, 2009

Материалы доклада по Entity Framework 4.0

Наконец-то появилось время разобраться с видео доклада по Entity Framework 4.0 :) Так что выкладываю, кому интересно:

Видео находится здесь: http://cid-be683ad8462aaeaf.skydrive.live.com/browse.aspx/.Public/Uneta?uc=1&sa=650758507. Придется качать 5 частей архива, а потом объединять Rar'ом, т.к. больше 50 Мб одним куском заливать нельзя. Можно было бы выложить на RapidShare или другие "условно бесплатные" файл-хостинги, но там тоже есть ограничения (как правило, 100 Мб), да и качать бесплатно не слишком приятно. YouTube/Rutube тоже не подходят - там 10-минутное ограничение, да и не совсем по теме... Если кто знает, куда можно выложить 250 Мб видео (почти час по длительности) одним куском - подскажите, плз.

А презентация живет здесь: http://www.slideshare.net/AlexMerle/new-in-entity-framework-40

PS. Спасибо всем, кто пришел, и отдельное спасибо Андрею Каще за съемку! :)

Upd 16.10.2009: Выложил примеры из доклада туда же, где и видео:

http://cid-be683ad8462aaeaf.skydrive.live.com/self.aspx/.Public/Uneta/EF4TestApp.zip
http://cid-be683ad8462aaeaf.skydrive.live.com/self.aspx/.Public/Uneta/NorthwindPocoSamplePart2.zip

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

Кстати! Как я говорил на докладе, уже вышли обновления Feature CTP для Code Only и Self-tracking entities:

http://blogs.msdn.com/adonet/archive/2009/11/15/updated-feature-ctp-walkthrough-self-tracking-entities-for-the-entity-framework.aspx
http://blogs.msdn.com/adonet/archive/2009/11/12/updated-feature-ctp-walkthrough-code-only-for-entity-framework.aspx

Так что кому интересно - дерзайте :)

Sunday, October 25, 2009

Доклад о нововведениях в Entity Framework 4.0

Готовлю доклад о нововведениях в Entity Framework 4.0. Планирую рассказать о различных проблемах Entity Framework 1.0 и том, какие из них были решены в следующей версии, а также просто про новые фичешки и рюшечки. Планируется даже не столько доклад, сколько workshop – постараюсь показать как можно больше примеров. Для этого скачал себе недавно вышедшую VS2010 beta 2 с обновленной версией EF4 – сижу вот теперь, развлекаюсь... Студия новая очень нравится, а вот Entity Framework, вернее, ADO.NET team подкачал – нигде толком не описаны breaking changes, поэтому застопорился на примерах для Code Only, POCO и Self-tracking entities: для первого не могу найти сборку, которая существовала в beta 1 (CTP1), для вторых – шаблоны T4 в студии. Ну да ничего, как-нибудь прорвемся – уж очень не хочется показывать примеры на beta 1, когда уже вышла beta 2.

Доклад буду делать в пятницу, 30 октября, на очередном харьковском собрании UNETA. Точное место и время пока еще неизвестны, но скорее всего, как обычно, в ХНУРЭ где-то около 18:00-18:30. Владимир Лещинский выложит анонс на dev.net.ua, так что можно будет посмотреть либо там, либо подписаться на комментарии к этой заметке – я чуть позже выложу время и место в комментариях.

Да, а вторым докладчиком будет Майк Чалый. Он будет рассказывать про DDD, причем это будет не просто теория, а рассказ человека, на своей шкуре попробовавшего этот подход. Да и вообще, Миша очень хорошо разбирается в дизайне и архитектуре, поэтому его всегда интересно послушать :)

Так что, приходите и приводите друзей – постараемся сделать доклады полезными и интересными. Если есть пожелания по докладу – прошу в комментарии.

PS. А еще 31 октября мой хороший друг Андрей Каща совместно с Сергеем Лутаем будут делать доклад по Silverlight 3.0 на IT Jam 2009 в Киеве. Я туда, к сожалению, не попадаю, но, может, кому-то другому повезет больше. Список тем можно посмотреть на официальном сайте.

PPS: Встреча будет проходить в Харьковском национальном университете радиоэлектроники (ХНУРЭ), пр. Ленина 14 ауд 301б (м. Научная) 30 октября 2009 в 18-15

PPPS. Внимание! Аудитория поменялась: встреча будет в 329 ауд.

Sunday, August 9, 2009

Entity Framework 4.0: выходим на зрелый уровень

Я уже полгода не работаю с Entity Framework, но по былой памяти интерес все равно сохраняется. Как-то совсем упустил из виду, что вместе с выходом Visual Studio 2010 Beta 1 вышла и первая бета Entity Framework 4.0. Более того, спустя месяц ADO.NET team выпустил еще и Entity Framework 4.0 Feature CTP 1 (скачать можно здесь). В CTP 1 вошли еще три новые фичи, которые не успели довести до выхода VS 2010 Beta 1: Self Tracking Entities (о которых я уже немного рассказывал), POCO Template и Code Only.

Что это значит? Это значит, что в релизной версии 2010-й студии в составе .NET 4.0 этих фич тоже не будет. Daniel Simmons в подкасте .NET Rocks #451 как-то достаточно неясно сказал, что они вроде бы войдут в какой-то следующий релиз после VS 2010, хотя сам EFv4 будет релизиться вместе со студией. Ну, да это не так важно.

А важно вот что: в следующей версии EF разработчики проделали достаточно серьезную работу. Они действительно учли многие комментарии и критику, которые обрушились на первую версию EF, поэтому вторая версия EF с физическим номером 4.0 (Microsoft снова извратила нумерацию с целью упростить жизнь своим sales) будет ощутимо солиднее. Что же мы получим в новой версии:

1) Model First development approach

В EFv1 была лишь возможность сгенерировать модель по базе данных, а EFv4 обладает и обратной возможностью - сгенерировать базу данных по модели. Теперь вы можете сами выбрать, какой подход вам удобнее и работать, используя его. На мой взгляд, может подойти людям, работающим в рамках DDD, где база данных воспринимается как инфраструктура - средство для хранения (persistence) состояния объектов системы.

Подробнее о фиче:
Sneak Preview: Model First in the Entity Framework 4.0

2) Code Only development approach

Конечно, Database First и Model First подходы к разработке покрывают потребности почти всех разработчиков. Однако, также есть люди, которым нравится создавать свою модель прямо в коде. При чем делают они это, как правило, при помощи так называемых fluent-интерфейсов, то есть максимально читабельного кода, который заменяет конфигурирование при помощи XML (то есть EDMX-файлы уходят в прошлое). На данный момент еще не все возможности EDMX-конфигурации обзавелись своими аналогами, да и текущий API сложно назвать fluent, но к релизу, думаю, будет покрыт максимум возможных операций.

Подробнее о фиче:
Code Only design
Code Only enhancements
Feature CTP Walkthrough: Code Only for the Entity Framework
Next version of EF Code Only Design laid out by MS

3) Persistence Ignorance (POCO) support

То о чем так долго говорили и писали, наконец свершилось: ADO.NET team добавил в EFv4 поддержку Persistence Ignorance (PI). В EFv1 все сущности были унаследованы от EntityObject и таким образом "знали" о том, что где-то есть EF контекст, а EF контекст, в свою очередь, мог отслеживать изменения в этих объектах и использовать их в других целях. Такая привязка сущностей к инфраструктуре создавала много проблем с тестируемостью и реализацией сценариев, где нужны чистые POCO-объекты. Теперь же эта проблема решена: вы можете создавать POCO-объекты и при этом у вас сохраняться все инфраструктурные возможности EF: отслеживание и сохранение изменений, lazy/deferred loading, и др. Кроме того, у вас есть выбор, какой способ отслеживания изменений выбрать: snapshot-based или proxy-based, также вы можете создать свои собственные шаблоны для генерации POCO. Фича очень серьезная, возможностей много.

Подробнее о фиче:
Sneak Preview: Persistence Ignorance and POCO in Entity Framework 4.0
POCO in the Entity Framework: Part 1 - The Experience
POCO in the Entity Framework : Part 2 – Complex Types, Deferred Loading and Explicit Loading
POCO in the Entity Framework : Part 3 – Change Tracking with POCO
Feature CTP Walkthrough: POCO Templates for Entity Framework

4) Customization of Code Generation

После описания POCO и Code Only, разумно рассказать немного о механизме, который они частично (или полностью) используют. Если вы помните, первая версия EF генерировала код при помощи такой штуки, как CodeDOM. Надо сказать, что хотя это и мощная технология кодогенерации, кастомизировать ее невероятно сложно. Мы пробовали вклиниваться в процесс генерации EF сущностей на одном из проектов, но возможности там достаточно поверхностные. EFv4 генерирует сущности и класс контекста при помощи T4 (Text Template Transformation Toolkit), что позволяет просто отредактировать существующие текстовые шаблоны или написать и подключить свои - и все готово. Кроме того, переход на более простой способ генерации позволил реализовать и некоторые другие новые возможности, например, POCO или Code Only. Да и вообще, теперь вы можете генерировать все что угодно на основании своей модели - различные адаптеры, DTO, POCO-объекты, возможности кодогенерации почти безграничны.

Подробнее о фиче:
Sneak Peek – Using Code Generation Templates with the Entity Framework 4.0

Как кастомизировать шаблоны T4 в EF:
Customizing T4 Templates
Customizing EDM Code Gen in EF4
Feature CTP Walkthrough: POCO Templates for Entity Framework

5) Testability Improvements

Здесь, в принципе, все понятно. Все вышеназванные улучшения и еще пара новых наконец-то дают возможность писать нормальный код с поддержкой тестирования. Да и нормальная поддержка различных паттернов вроде Repository тоже не помешает.

Подробнее о фиче:
Sneak Preview: Entity Framework 4.0 Testability Improvements

6) Self-tracking entities

Идея проста - появились новые шаблоны T4, которые позволяют генерировать специальные сущности, не зависящие от ObjectContext и EF, но зато умеющие сами трекать изменения внутри себя. Когда это может пригодится? Например, когда вы хотите сериализовать свои сущности, отправить их куда-то в другое место (на SL-клиент или в другое приложение при помощи WCF-сервиса), сделать там в них изменения (добавить новые, изменить, удалить), вернуть назад и сохранить изменения в базу. Описанный здесь сценарий было очень сложно реализовать в EFv1, теперь это вопрос намного более тривиальный.

Подробнее о фиче:
Self-Tracking Entities in the Entity Framework
Feature CTP Walkthrough: Self Tracking Entities for Entity Framework

7) Deferred loading

В EFv1 в отношении lazy/eager loading было две опции: eager loading, доступный через Include(), и explicit (явный) lazy loading, доступный через метод Load() классов EntitySet и EntityReference. При этом если вам хотелось реализовать неявную поддержку lazy loading, вам приходилось достаточно серьезно извращаться (мы, например, хакали кодогенерацию EF), хотя в LINQ to SQL эта возможность была с самого начала. Теперь же все стало на свои места - все три сценария поддерживаются, причем третий сценарий для отличия от уже существующего lazy loading назвали deferred loading. Зачем - не знаю.

Подробнее о фиче:
Sneak Preview: Deferred Loading in Entity Framework 4.0
A Look at Lazy Loading in EF4

8) Model Defined Functions

MDF - это специальные функции, добавляемые к сущностям, которые описываются на уровне CSDL, то есть концептуальной модели данных. Эти функции могут принимать параметры, выполнять любой eSQL-код внутри себя, и потом возвращать результат. Благодаря такой возможности их можно использовать в eSQL-запросах, а также в LINQ-запросах (если вывести их наверх). В каких случаях они будут полезны в жизни? Сложно сказать, не могу подобрать реальный пример. Разработчики говорят что-то про Reporting Services, Web Services и т.д. - они необходимы там. Поэтому это больше внутренняя фича, которая будет полезна, если вы начнете делать шаги в сторону от стандартных сценариев. Поживем - увидим.

Подробнее о фиче:
Model Defined Functions
Sneak Preview: Model Defined Functions
EF4: Model-Defined Functions Level 1 & 2

Немного больше об истории и причинах их появления:
Update on Computed Properties

9) Foreign Keys support

EFv4 поддерживает новый тип ассоциации, т.н. FK Association. Старый тип ассоциации теперь принято называть Independent Association. FK Association будет полезен в тех случаях, когда вам удобнее работать со связями через первичные ключи, а не с коллекциями и ссылками на реальные объекты - все-таки это иногда намного более производильные операции. При этом надо сказать, что эта фича далась разработчикам очень нелегко, так как она должна поддерживаться не только на уровне EntityObject, но также на уровне proxy-based POCO, snapshot-based POCO, IPOCO и других способов создания модели.

Подробнее о фиче:
Foreign Keys in the Entity Framework

10) SQL Generation Improvements: readability and performance

Здесь все достаточно просто: генерируемый SQL стал более шустрым и более читабельным, сам процесс генерации тоже ускорился.

Подробнее о фиче:
Improvements to the Generated SQL in .NET 4.0 Beta1
Checking for EF to TSQL Query Compilation Changes in VS2010 Beta1

Об остальных фичах более кратко:

Checking out one of the new stored procedure features in EF4
A big step for Stored Procedures in EF4
Pluralization support
The much improved EDM Wizard Pluralization in VS2010
Complex Types in the EDM Designer in EF4 and a look at updating complex types in code
More Designer Improvements - Deleting Entities from the model and from the store schema

Также ADO.NET team выпустил пару статей по "правильному" написанию кода для поддержки Repository, Unit of Work и даже N-Tier applications. Почитать можно, но все же, на мой взгляд, там лучше почитать комментарии и ссылку, которые в них:

Using Repository and Unit of Work patterns with Entity Framework 4.0
Sneak Preview: N-Tier development with Entity Framework 4.0

Ну, а утрамбовать это все стоит отличным подкастом .NET Rocks #451, где происходит интервью с уже упомянутым Daniel Simmons, который более детально рассказывает о реализации некоторых вышеуказанных фич.

Надеюсь, эта информация кому-нибудь поможет быстрее перестроиться на новую версию EF, а может, просто поподробнее взглянуть на него. В конце-концов, правило о том, что продуктами Microsoft стоит пользоваться после выхода SP1, действует и здесь: первая версия была несколько сырой, но зато во второй добавились многие плюшки, которых так не хватало профессиональным разработчикам.

Удачи!

Sunday, April 12, 2009

Список интересных подкастов: выпуск #1

Недавно прослушал несколько интересных подкастов. Хотел бы поделиться некоторыми наиболее интересными на мой взгляд:

Carl, Richard и Ward Bell обсуждают вопросы использования ORM в целом и Entity Framework в частности. Послушав этот подкаст, вы узнаете какие проблемы есть у Entity Framework v1.0 (дизайнер, POCO, etc.), еще раз про ситуацию с LINQ to SQL и Entity Framework, о которой я уже писал, почему NHibernate, несмотря на свои функциональные преимущества, не очень популярен в широких кругах программистов, когда стоит использовать ORM, когда не стоит, а также какие есть проблемы разработки RIA приложений, в частности проблема единственности модели данных, когда и на сервере, и на клиенте приходится делать одинаковую модель, чем тут может быть полезен DevForce .NET Silverlight-разработчикам (хотя после выхода RIA Services часть проблемы с моделью вроде бы как уходит).

Гость программы, Doug Crockford рассказывает про свой профессиональный опыт, а также про создание формата данных JSON, автором которого он является (!). Также довольно много обсуждений того, почему DOM - ахиллесова пята клиентских скриптов, различных стандартов, на которых строится JavaScript, а также его будущего. Весьма познавательно.

Robert C. Martin (Uncle Bob) в гостях у Скотта рассказывает последнему, что такое Single Responsibility Principle, Open Closed Principle, Liskov Substitution Principle, Interface Segregation и Dependency Inversion Principle (SOLID). Очень детально и на примерах. Рекомендую всем, кто начинает понимать, что кроме внешнего качества продукта, есть еще его внутреннее качество - качество кода.

Просто отличный подкаст о людях, которые разрабатывают софт не только для крупных организаций, и не только за деньги. Приложение, которое вносит свою лепту в дело борьбы с раком, разработанное в Scripps Institute, в котором поучаствовало много программистов со всего мира, объединенных одной целью. WPF, Sharepoint, Scrum, и многое другое. Если вы хотите изменить мир к лучшему, то послушайте как это делают другие люди: возможно, у вас появится руководство к действию.

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

А какие подкасты нравятся вам?

Sunday, March 29, 2009

Entity Framework vNext: сущности, сами отслеживающие свои изменения

Команда ADO.NET продолжает свой эксперимент по публичному проектированию следующей версии Entity Framework. Надо сказать, что результаты пока что весьма спорны: с одной стороны, они действительно публикуют все идеи и предлагают их обсуждать, с другой - очень часто возникает впечатление, что все решения уже приняты, и максимум, на что можно рассчитывать - это какие-то незначительные изменения в деталях.

Вот, например, одно из последних нововведений - это концепция сущности, которая сама отслеживает свои изменения. Я поначалу не до конца понял идею и до середины статьи думал, что ADO.NET team решил реализовать сделать свою версию Castle ActiveRecord, воспользовавшись одноименным паттерном. Однако потом я-таки въехал в идею - никакого Active Record там нет. Смысл в том, что теперь у разработчиков появится возможность сгенерировать достаточно "умные" сущности (при помощи T4), которые будут сами отслеживать свое состояние и фиксировать изменения. Сохранять изменения в таких сущностях вы сможете просто вызвав метод ApplyChanges у ObjectContext, которому вы можете передать подобный объект (да, наверно, и коллекцию). Кроме того, если вы просто создадите эту сущность, она будет жить с состоянием Added до тех пор, пока кто-то не решит записать ее в базу. Фактически, теперь вы можете создать сущность при помощи одного контекста, а потом сохранить при помощи другого, то есть сущность становится намного более независимой от контекста, который ее породил. В статье подобная сущность даже называется POCO-объектом, хотя все-таки до true POCO там еще далеко.

Что же тут такого полезного? Ведь и раньше вроде бы можно было отсоединять сущности от одного контекста, и присоединять их потом к другому. Можно было, но только вот все изменения сущности при этом терялись как при отсоединении, так и при присоединения, т.к. сущности первой версии не обладают способностью отслеживать свои изменения, эта функция доступна лишь контексту. И это как раз и порождало кучу проблем, связанных с тем, что для обеспечения нормального взаимодействия между несколькими сущностями, было необходимо, чтобы они были созданы в одном и том же контексте. А это не очень приятно для сценариев, в которых сущности живут в приложении немножко дольше, чем время вызова одного-двух методов. Теперь же эта проблема решена.

Однако, лично у меня возникает один вопрос: как быть с ассоциациями (навигационными свойствами)? Теперь, когда сущность отделена от контекста, она не может им воспользоваться для того, чтобы заполнить данными незагруженное навигационное свойство. Как быть? Задал вопрос разработчикам там же в комментариях - может, ответят.

Другие нововведения:

1) Новый тип ассоциаций (foreign key)
2) Упрощенная настройка шаблонов генерации сущностей (T4)
3) Функции, определяемые в модели
4) Общие улучшения для N-tier архитектур
5) Концепция подхода Model First
6) и многое другое (там действительно куча информации)

PS. Надо сказать, нашумевший Vote of no confidence, о котором я уже писал, сделал свое дело, и вторая версия EF будет намного более солидной и зрелой. Еще бы немножко улучшить производительность - и будет достаточно интересная альтернатива NHibernate.

Sunday, February 22, 2009

Немного о проектировании: паттерны из мира ORM

Наверняка, каждый программист слышал о паттернах (шаблонах) проектирования, читал классику, и с той или иной периодичностью использует паттерны в повседневной работе для того, чтобы сделать дизайн своего приложения более понятным и масштабируемым. Если вы когда-нибудь пробовали использовать Linq to SQL, Entity Framework, NHibernate или другие ORM, то вы, конечно же, заметили, что у них есть много общего. L2S и EF с некоторого расстояния вообще выглядят, как близнецы-братья, да и остальные ORM тоже недалеко ушли. Вы думаете, это совпадение? Вовсе нет. Просто все они строятся с использованием одних и тех же паттернов проектирования.

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

Начнем с того, что каждый ORM имеет в своем составе некий "менеджер" (например, контекст в L2S и EF, или сессию в NH), который является неким логическим фасадом (Facade) к базе данных. Этот фасад, как правило, "понимает" определенный маппинг объектов приложения на таблицы базы данных, "умеет" принимать на вход простые и сложные запросы на получение объектов, знает, "какие" изменения объектов и "как" сохранить в базе данных, и многое другое. Рассмотрим, какие паттерны работают внутри и рядом с ним.

Unit of Work

Unit of Work - наверно, самый популярный паттерн модификации данных. Он описывает следующую стратегию работы с данными:

  1. Получили объекты из ORM
  2. Изменили какое угодно их количество
  3. Сказали ORM сохранить данные в базу (или откатиться)
  4. ORM сохранил все изменения одним махом (или откатился)

Даже если вы никогда в жизни в глаза не видели ORM, то знакомо, не правда, ли? Конечно, именно по такому же принципу работает старая-добрая пара DataAdapter - DataSet в plain ADO.NET. И точно так же работают как контексты L2S и EF, так и сессия NH. Unit of Work работает благодаря тому, что он хранит в себе ссылки на все объекты, которые были созданы/получены через него. Именно поэтому без дополнительных телодвижений вам не удастся заставить контекст EF сохранить объект, который был изменен в рамках другого контекста, а потом приаттачен к текущему. Контекст просто не знает, какие изменения были сделаны в объекте, т.к. это не забота объекта "знать" о своих изменениях, а забота окружения, коим и является для объекта контекст/сессия.

Паттерн Unit of Work полезно использовать и в других случаях. Например, когда вам необходимо дать пользователю возможность изменить определенные данные приложения в памяти, а потом нажатием на одну кнопку сохранить их в базу данных. Или не сохранить.

Active Record

В противовес предыдущему паттерну, паттерн Active Record определяет, что объект сам знает, как себя извлечь из базы данных и сохранить изменения туда же. То есть объекту больше не нужны внешние "менеджеры", он сам в состоянии за себя постоять.

В целом, использование этого паттерна остается достаточно спорным, т.к. объект в таком случае "знает" о способе сохранения себя в базе, и поэтому крепко связан с ней, нарушая при этом Persistence Ignorance. Кроме того, если подобный объект содержит бизнес-логику, то он еще и нарушает Single Responsibility Principle. С другой стороны, у Active Record есть свои преимущества и сторонники. Из широко распространенных ORM паттерн ActiveRecord используется как минимум в Castle ActiveRecord (кто бы мог подумать :)). Да и Ruby-on-Rails, насколько я слышал, его уважает.

Identity Map

Identity Map - это паттерн, который дает возможность загрузить лишь один экземпляр объекта в память. То есть, если при первом обращении к определенному объекту ORM "поднимает" экземпляр этого объекта из базы, но все последующие запросы, которые будут приводить к получению этого объекта (даже если этот объект будет частью списка других объектов), будут всего лишь получать ссылку на уже созданный объект, а не его копию. Работает это благодаря тому, что внутри ядра ORM есть реализация этого паттерна. Контекст/сессия, перед тем, как материализовать (создать) объект с определенным Id, сначала просматривают это хранилище на предмет существования объекта, и если он не найден, регистрируют новый материализованный объект в хранилище.

Что это нам дает, как программистам. Прежде всего, если мы работаем с одним и тем же контекстом/сессией, то мы можем легко сравнивать объекты по ссылке (==). Кроме того, и что более важно, если мы уже начали изменять объект, а потом вдруг он появился в результате какой-то вспомогательной выборки, то мы можем быть уверены, что это один и тот же измененный объект (конечно, если вы напрямую не сказали при выборке, что вам необходим "чистый" объект). Ну, и напоследок, как side effect, мы получаем небольшое кеширование в рамках контекста/сессии, что позволяет уменьшить затраты на повторную материализацию объекта (весьма немаленькие). Естесственно, объекты, полученные из разных конекстов/сессий, не обладают подобными свойствами, что также необходимо понимать и учитывать.

Identity Field

Упомянув Identity Map, нельзя обойти стороной Identity Field. Этот небольшой паттерн, по сути, всего лишь говорит о том, что каждый объект содержит в себе уникальный ключ, который позволяет ORM правильно идентифицировать объекты у себя внутри. В частности, в EF такую функцию выполняет класс EntityKey, который состоит из названия сущности и значений первичного ключа.

Как вы уже догадались, Identity Field интенсивно используется для решения задач, описанных предыдущими паттернами.

Query Object

Паттерн Query Object реализует в себе запрос к данным. У Фаулера написано, что этот объект должен сам знать, как превратить себя в SQL-запрос, но очень часто паттерн рассматривается более широко, описывая прежде всего способ составления сложных запросов к ORM.

В NH есть механизм запросов Criteria, который можно отнести к реализации этого паттерна. В L2S и EF эту функцию выполняет язык запросов LINQ, который также можно назвать его реализацией (хотя и с большой натяжкой).

Lazy Load

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

Lazy Load в том или ином виде реализован практически во всех известных мне ORM для улучшения производительности. Ведь не всегда ваш объект Заказ должен загружать сразу и список Продуктов - они могут просто не понадобится в данной транзакции, так зачем тратить на это время и ресурсы?

Repository

Я не мог пройти мимо этого паттерна, хотя на самом деле он не реализован ни в L2S, ни в EF, ни в NH, ни в AR. (Возможно, реализован в других ORM, подскажите, если знаете.) Не мог пройти, потому что паттерн Repository очень часто используется при работе с NH (1 и 2), да и примеры для других ORM уже подоспели, например, для L2S (1 и 2).

С простой точки зрения, этот паттерн определяет фасад, который предназначен для того, чтобы быть промежуточным слоем между доменной моделью (бизнес-логикой) и источником данных. Весь код приложения за пределами репозитория работает с базой данных (или другим источником данных) через репозиторий. Однако, если смотреть на него с точки зрения пуристов DDD, то репозиторий - это уже нечто другое. Всех желающих погрузиться в нирвану DDD отправляю к следующему источнику, который достаточно неплохо описывает Repository с точки зрения DDD. Не заблудитесь только там :)

Заключение

Предлагаю пока что ограничится данными паттернами, т.к. они являются основными паттернами поведения ORM. Тех же, кто хочет узнать больше, отправляю к Фаулеру, в каталоге которого описаны еще с десяток других паттернов, связанных с другими аспектами работы Data Access в целом, и ORM в частности.

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

Sunday, January 25, 2009

Методы оптимизации производительности Linq to SQL и Entity Framework

Я хотел бы закончить начатое в предыдущих нескольких заметках (часть 1, часть 2) и докладе дело и привести набор методов по оптимизации производительности L2S и EF. В какой-то степени эту заметку можно считать текстом к презентации. Наверняка, это не все, что можно сделать с этой точки зрения, но это как минимум основные способы, которые могут стать отправной точкой для ваших собственных действий.

Зачем нам все это нужно?

Первым делом давайте обсудим, а зачем вообще нужно оптимизировать производительность этих инструментов? Неужели она настолько плоха, что ими нельзя пользоваться без этого? Вовсе нет, можно и даже нужно! Просто нужно понимать, что за огромное ускорение времени разработки, исчезновение нудного процесса создания Data Access Layer вручную и снижение затрат на дальнейшие изменения в базе данных и модели нужно чем-то платить. А платить приходится двумя вещами: временем на изучение нового инструмента (однажды) и некоторой потерей гибкости и производительности. В разных ORM потери производительности разные, но все же вы должны отдавать себе отчет в том, где и когда использовать ORM, а где и когда - нет.

За год работы с Entity Framework на достаточно крупном проекте я использовал в лучшем случае пару способов из приведенных ниже. И это несмотря на то, что как оказалось для нашего приложения использование ORM было, возможно, не самым лучшим выбором. У нас было несколько бизнес правил, которые заставляли нас вытаскивать чуть ли не полбазы в память, чтобы подсчитать некоторые данные для всего лишь одной сущности! При этом из-за иерархичности данных считать их в базе было еще большим злом, чем считать в памяти (поверьте, мы пробовали и меряли производительность). Поэтому, как вы сами понимаете, за dotTrace я брался довольно часто. Но, как правило, обычно все заканчивалось тем, что я находил либо ляп в коде, либо добавлял локальный кеш, чтобы не вытаскивать одни и те же данные из EF сотни, а то и тысячи раз. Надо сказать, что почти все проблемы с производительностью мы побороли через кеширование данных и правильное использование контекста EF. Но ближе к релизу приложения я начал замечать, что мы уже оптимизировали почти все, что можно и что bottleneck медленно, но уверенно переместился к границе EF, и что дальше придется оптимизировать уже его. Следствием этого и стало исследование, результаты которого я привожу сейчас.

Основные места потери производительности

Сначала я хотел бы остановиться на основных местах, где теряется производительность, т.к. от этого зависят и методы оптимизации:

1. Инициализация инфраструктуры. При "холодном старте" и L2S и EF теряют здесь какое-то время. Это происходит всего лишь один раз для апп-домена, но тем не менее может стать неприятной неожиданностью. L2S справляется с этим быстрее, но в EF можно пре-генерировать его внутренние views еще до запуска приложения и таким образом сократить время инициализации в разы. То есть для EF эту потерю (иногда очень значительную при первом запросе) оптимизировать можно.

2. Накладные расходы, связанные с маппингом. И L2S, и EF имеют эти расходы, но в EF они больше, т.к. в нем намного больше вариантов маппинга: наследование, тип на таблицу, несколько типов на таблицу, несколько таблиц на тип, сложные типы. К сожалению, тут особо не разбежишься, т.к. все происходит внутри ORM. Возможно, есть какие-то хаки, но я о них на данный момент не знаю. Поэтому будем считать, что оптимизация этой задачи нам недоступна.

3. Анализ запросов. Это могут быть linq-запросы (Linq to SQL, Linq to Entities) или EntitySQL для EF. Здесь ситуация выглядит получше. Разобранные EntitySQL запросы кешируются на уровне апп-домена, что позволяет не заниматься этим несколько раз. Ключом выступает как раз само текстовое представление запроса, так что будьте внимательнее и используйте параметры. С linq-запросами сложнее. Так как linq - это дерево объектов, которое представляет собой запрос, использовать его в качестве ключа довольно сложно. Но в таком случае запрос можно "откомпилировать" один раз (то есть, грубо говоря, закешировать его разобранный вид и команду, которая будет построена на нем) и потом выполнять его многократно. Тоже помогает.

4. Генерация SQL-запросов. Как и маппинг, происходит глубоко внутри, поэтому добраться туда и как-то повлиять сложно. Но нужно сказать, что почти все провайдеры реализуют внутри себя различные способы оптимизации, такие как кеширование планов запросов. В то же время во всех ORM есть одна и та же проблема: генерируемый SQL-результат не всегда оптимален (хотя как правило это так). Причем, что интересно, EF генерирует запросы в большинстве своем более оптимально, чем L2S. Один из основных советов здесь - в случае получения на выходе неоптимального запроса просто перестройте запрос в коде: поменяйте джойны, разбейте на несколько более мелких. Это помогает.

5. Материализация. Это процесс создания объектов по полученным реляционным данным. Занимает львиную долю времени от времени выполнения самого запроса. Однако оптимизирован за счет того, что контекст как L2S, так и EF (да и сессия NHibernate тоже) сохраняет у себя внутри материализованные объекты и при повторном обращении к этому же объекту уже не создает его повторно (если вы сами не скажете). Конечно, это сделано не для оптимизации, а для того, чтобы в рамках одного контекста у вас был только один объект Заказ с ID=7 в независимости от количества запросов. Ну, и чтобы трекать изменения в этих самых объектах. Но подобное "кеширование" еще и увеличивает производительность, что тоже является хорошей новостью. В защиту процесса материализации нужно добавить еще и то, что даже если вы пишете свой Data Access Layer, то вы все равно в том или ином виде занимаетесь материализацией. Вам же тоже нужно превратить записи из DataReader в какие-то объекты доменной модели. Да, ручной код более оптимален, т.к. заточен для решения лишь одной задачи, но, поверьте, ORM тоже умные люди пишут :)

Методы оптимизации производительности

Итак, какие же методы оптимизации производительности можно предложить. За конкретными цифрами и сравнениями вы можете обратиться к презентации.

1. Пре-генерация views в Entity Framework. По шагам расписана в MSDN и частично здесь, поэтому подробно останавливаться не буду.

2. Отключение трекинга объектов (tracking). Дает возможность немного улучшить производительность за счет того, что материализованные объекты не отслеживаются контекстом. Обратная сторона - сделанные в объектах изменения не могут быть сохранены в базу. Плюс (здесь я могу ошибиться, но вряд ли) отключается кеширование материализованных объектов в контексте, что в большинстве своем намного сильнее ухудшает производительность для большого количества запросов. Так что решайте сами, но мы у себя не использовали.

В L2S трекингом можно управлять через свойство контекста:

context.ObjectTrackingEnabled = false;

В EF - через ObjectQuery<T>, что не так удобно:

entities.Orders.MergeOption = MergeOption.NoTracking;
entities.Customers.MergeOption = MergeOption.NoTracking;

Подробнее про трекинг в EF можно почитать в MSDN.

3. Кеширование материализованных объектов. Происходит автоматически в контексте в том случае, если включен трекинг. В интернете иногда можно прочитать советы по использованию одного контекста на запрос. Да, можно, если вы пишете тестовое приложение или вам совершенно не важна производительность. Однако, в то же время и слишком долго держать контекст в памяти - неправильно. В конце-концов он вытянет в память всю вашу базу данных, не говоря уже о том, что придется каким-то образом вручную управлять конкуренцией, т.к. данные в базе могут обновиться, а в памяти - еще нет. В любом случае, судя по цифрам, использование одного контекста на несколько запросов улучшает производительность в несколько раз.

В нашем веб-приложении мы использовали интересную стратегию: создавали один контекст на http request и сохраняли его в Request. Все запросы в рамках одного http request работали через этот контекст, таким образом обеспечивая определенное кеширование. Плюс к этому у нас не было проблем, связанных с тем, что одна сущность была вытянута из одного контекста, а другая - из второго. Ну, и наконец, т.к. обработка http request занимает от силы несколько секунд, то проблемы с конкуренцией были сведены до минимума. В случае же, когда нам нужно было сохранять состояние объектов на протяжении нескольких http request'ов (например, для реализации многостраничных диалогов или многостраничных таблиц с поддержкой сохранения изменений), мы временно переносили контекст в сессию, а когда нужно было сохранять данные, вызывали context.ApplyPendingChanges(), и убирали контекст из сессии. Таким образом, на следующем запросе он снова создавался в рамках http request и все продолжало замечательно работать.

Но не переборщите с этим методом. Настоятельно не рекомендуется использовать один и тот же контекст из нескольких сессий или запросов (например, поместив его в Application). Мало того, что вы получите себе на голову все вышеупомянутые проблемы, так еще и в случае параллельного выполнения нескольких запросов (а каждый http request - это отдельный поток, если вы не знали) вы получите красивый exception :)

4. Компиляция linq-запросов. Как я уже говорил, linq-запросы, в отличие от EntitySQL, не кешируются автоматически. Для того, чтобы скомпилировать запрос, нужно выполнить очень простой вызов:

protected static Func<NorthwindClassesDataContext, IQueryable<Order>> compiledGetOrders =
    CompiledQuery.Compile(
        (NorthwindClassesDataContext ne) =>
            (from o in ne.Orders
             select o));

При этом не имеет особого значения, используете вы Linq to SQL или Linq to Entities. Вы просто передаете туда либо контекст L2S, либо контекст EF.

Однако, хотел бы предостеречь тех людей, которые уже побежали переписывать свой код. Дело в том, что компиляция запроса - процесс довольно длительный, поэтому он имеет смысл лишь в том случае, если вы делаете несколько вызовов одного и того же запроса (пусть и с разными параметрами). И еще лишь в том случае, если у вас действительно сложный linq-запрос. Для случая, приведенного выше, компиляция запросов не дает существенной выгоды. Для случая с одним запросом откомпилированный запрос может работать дольше неоткомпилированного. Так что it depends.

Подробнее про компиляцию запросов можно прочитать все в том же MSDN. Также советую глянуть статью о потоке выполнения запроса в EF.

5. Использование EntitySQL. Во всех источниках написано, что самый быстрый способ работы с EF - это EntitySQL. И, как показывают тесты, это действительно так. В то же время, EntitySQL не так удобен, как linq, не поддерживает code completion (это же простая строка), не типобезопасен (у вас нет проверки уровня компиляции) и в целом выглядит как обычный SQL-код в вашем C#-коде, что не придает ему эстетичности. Да и работает не намного быстрее компилированного linq. Так что я бы советовал использовать его лишь в самых серьезных случаях. Хотя в таких случаях еще лучше написать хранимую процедуру.

Выглядит это где-то так:

string query = "SELECT VALUE p FROM NorthwindEntities.Products AS p"
    + " JOIN NorthwindEntities.Order_Details AS od ON od.ProductID == p.ProductID"
    + " JOIN NorthwindEntities.Orders AS o ON o.OrderID == od.OrderID"
    + " WHERE o.Customers.CustomerID == @customerId";
var products = 
    entities.CreateQuery<Product>(query, new[] { new ObjectParameter("customerId", customerId) }).ToList();

Подробнее почитать про EntitySQL можно в одной из заметок на ADO.NET blogs и в MSDN.

6. Использование жадной загрузки (eager loading). Во-первых, нужно сказать, что и L2S, и EF по умолчанию реализуют ленивую загрузку (lazy loading). Только L2S делает это неявно, то есть сразу при обращении к навигационному свойству или коллекции, а в EF нужно явно вызвать метод Load(). Что лучше, что хуже - можно спорить долго. Жаль, что в EF не сделали включение/выключение явного/неявного режима через какое-нибудь свойство. Поэтому нам вот пришлось самим сделать неявную ленивую загрузку через кодогенерацию (я как-нибудь расскажу об этом подробнее). Но разговор сейчас не об этом. По тестам жадная загрузка, конечно же, выигрывает у ленивой, причем иногда весьма существенно. Однако если вы загрузите кучу данных, а потом не будете ее использовать - какой вам от нее прок? В этом случае, ленивая загрузка оказывается на высоте. Как же написать запрос в "жадном" стиле? В Entity Framework это делается через ObjectQuery.Include():

(from o in entities.Orders.Include("Order_Details")
select o).ToList();

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

var allProductsData = 
    (from cust in entities.Customers
    where cust.CustomerID == customerId
    select new
        {Orders = 
            from ord in cust.Orders
            select new
                {Products = 
                    from det in ord.Order_Details
                    select det.Product}}).ToList();

List<Product> products = new List<Product>();
var productsData = allProductsData.FirstOrDefault();
if (productsData != null)
    foreach (var ordersData in productsData.Orders)
    {
        foreach (Product product in ordersData.Products)
        {
            products.Add(product);
        }
    }

Хоть это и не eager loading в чистом виде, но в то же время этот способ обходит вариант прохождения от Customer до его Products через навигационные свойства, которые бы подняли данные через lazy loading.

7. Оптимизация обновления данных. Здесь в первую очередь хочется отметить, что по тестам EF значительно опережает L2S в этом аспекте. Причем дело не в SQL-запросах, так как их производительность, за некоторыми исключениями, почти одинакова. Проблема кроется где-то внутри L2S, поэтому учитывайте и этот аспект при выборе способа доступа к базе данных. Для оптимизации выполнения сгенерированных SQL-запросов в L2S советую глянуть еще на атрибут UpdateCheck, который определяет будет ли сгенерирован where по всем полям сущности или только по ее Id (это делается для реализации оптимистической конкуренции). В EF такая проверка делается только по измененным колонкам, а в L2S - по всем. Также стоит подумать об реализации колонки Version для этой самой конкуренции.

8. Оптимизация SQL-запросов. Как я уже говорил, единственный способ - следить за ними и менять входные linq или EntitySQL запросы. Также по результатам осмотра можно наставить индексов в базе данных. Ну, и никто не отменял хранимые процедуры, куда можно поместить наиболее серьезные запросы. Так что основной совет здесь - не бояться браться за SQL профайлер в случае необходимости :)

Еще интересен тот факт, что EF в целом генерирует более оптимальные запросы, чем L2S. Так что имейте и это в виду.

Выводы

В целом, я описал все основные средства оптимизации производительности. Если вам интересны подробности, для EF существует целая статья в MSDN, посвященная этому вопросу. Также советую интересную серию заметок в блогах ADO.NET. Для Linq to SQL советую почитать следующую серию заметок, хотя она немного устарела.

В целом, из своего опыта могу сказать, что производительность Entity Framework уже достаточна для реализации кучи приложений. Думаю, в следующей версии она станет еще лучше. Если же вы знаете, что этого вам будет мало, то можете посмотреть на Linq to SQL. И пусть вас не пугает, что MS решила не развивать L2S в той же мере, что и EF - уже есть примеры высоконагруженных приложений, успешно использующих L2S, не говоря о том, что это просто замечательная замена всяким типизированным и прочим датасетам (брррр). Ну, а в особо критичных к производительности случаях нам все равно никуда не деться от старого-доброго ADO.NET :)

Если сравнивать Entity Framework с другими полноценными ORM, такими как NHibernate, LLBLGen Pro и др, то на данный момент я не могу ничего толком сказать. Надо попробовать, благо тестовое приложение, которое я написал, с легкостью расширяется другими тестовыми провайдерами. Могу лишь кое-что сказать об NHibernate, т.к. я успел его попробовать и почитал дополнительные материалы о нем. NH по производительности занимает место где-то в промежутке между L2S и EF. Плюс этот прекрасный ORM уже давно на рынке и обладает множеством других интересных способностей, например, поддержкой Persistence ignorance и кешом второго уровня, которых пока нет в Entity Framework. Так что при выборе ORM я бы однозначно смотрел и в его сторону.

На этой замечательной ноте я бы хотел завершить цикл постов про производительность L2S и EF :) Вполне возможно, в ближайшем будущем у нас появятся дополнительные данные по производительности NHibernate и еще нескольких интересных ORM. Если у вас есть желание помочь в этом - можете скачать тестовое приложение, написать свой тестовый провайдер (это делается абсолютно несложно, поверьте мне), и расшарить результаты.

Всем спасибо!

Saturday, January 24, 2009

Доклад на харьковской UNETA. Материалы

Вчера прошел мой первый доклад на харьковской встрече UNETA. Я попытался разобраться с производительностью Linq to SQL и Entity Framework, взяв в качестве бенчмарка производительность аналогичных запросов в чистом ADO.NET, а также дать какие-то советы по их улучшению. Надеюсь, тем, кто пришел, было не очень скучно :)

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

Как я и обещал, выкладываю материалы доклада:

Скачать тестовое приложение и презентацию можно отсюда:

Тестовое приложение: http://data-access-orm-comparison.googlecode.com/files/DataAccessPerformanceTest_v2.zip

Презентация: http://data-access-orm-comparison.googlecode.com/files/L2S%20and%20EF%20performance%20analysis.zip

Постоянная ссылка проекта: http://code.google.com/p/data-access-orm-comparison/

Для запуска тестового приложения вам понадобится лишь поставить базу данных Northwind и изменить строки подключения. Приложение достаточно расширяемо, так что вы можете добавить туда свои собственные тестовые провайдеры, например, для работы с NHibernate или LLBLGen Pro.

Список полезных ссылок есть в конце доклада, так что здесь я его не привожу.

Удачи в ваших исследованиях!

Monday, December 22, 2008

Доклад на харьковской UNETA

Судя по всему, я буду одним из докладчиков на следующей харьковской UNETA, которая ориентировочно будет в начале-середине января. Тема у меня будет не совсем обычная, она будет связана с производительностью ORM. Точно будут L2S и EF, возможно, добавлю что-то еще, если хватит времени. Так как тема не связана с какой-то конкретной технологией, скорее, это разбор и сравнение, то я могу немного варьировать ее содержание. В связи с этим у меня возник вопрос, что бы вам хотелось услышать? В принципе, скелет доклада я уже продумал, но все равно есть место для различных деталей, тестов, которые я могу провести и т.д. Я постараюсь учесть все пожелания и осветить их в докладе по-максимуму.

В докладе я постараюсь рассказать о том, как работают рассматриваемые ORM внутри, что именно влияет на их производительность, проведу некоторые наиболее интересные и показательные тесты для их сравнения между собой и с чистым ADO.NET, и постараюсь рассмотреть генерируемые каждой ORM запросы.

Итак, у меня есть несколько вопросов к вам:

  1. Стоит ли включать в доклад NHibernate? Или достаточно будет L2S и EF?
  2. Какие тесты вы бы хотели увидеть?
  3. Какие сопутствующие темы вам были бы интересны? Что из неописанного выше можно осветить еще?

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

Спасибо за помощь :)

Thursday, November 13, 2008

Сравнение производительности .NET ORM: Часть 2. Генерация SQL-запросов

И снова здравствуйте. В первой части серии я постарался рассмотреть производительность L2S, EF и AR/NH по сравнению с классическим ADO (тестовое приложение можно найти здесь). NET и друг с другом. Для этого были написаны специальные тесты, но замеры мы производили на достаточно высоком уровне, не выделяя в результатах базу данных. Как я и обещал, во второй части я бы хотел опуститься ниже, в базу, и сравнить запросы, генерируемые нашими подопытными. Должен сказать, что я был несколько удивлен полученными результатами, но обо всем по порядку.

Способ сравнения

Способ сравнения я выбрал до предела простой. У нас уже есть написанный код для получения продуктов по заказчику. Это не ахти какой сложный запрос, конечно, но тем не менее что-то мы из него получим. При этом у нас есть два варианта получения данных: одним запросом с джойнами (тест CustomerProducts) или при помощи простых операций вроде получения сначала заказчика, потом его заказов, потом деталей заказа и уж в самом конце – продуктов, то есть при помощи пачки запросов (тест CustomerProductsComplex).

Значит, заряжаем SQL Server Profiler, запускаем наши тесты, вытаскиваем пойманные SQL-запросы, садим их в Management Studio, включаем “Include Actual Execution Plan”. Я сделал 4 теста. В первом сравниваются запросы из теста CustomerProducts для всех четырех способов доступа к данным. В последующих трех я сравнивал попарно запросы для теста CustomerProducts и для теста CustomerProductsComplex для каждого из ORM отдельно (для классического ADO.NET я не делал этот тест).

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

Итак, поехали.

Сюрпризы

Уже на проведении первого же теста (того, где должен быть один результирующий запрос) меня ждало несколько интересных открытий:

  1. L2S зачем-то сгенерировал два запроса вместо одного. Первый запрос у них вытаскивает заказчика, а второй уже выполняет реальную работу. Я проверил свой код – вроде бы там простой LINQ-запрос, который должен превратиться в один SQL-запрос. Более того, такой же LINQ-запрос в EF действительно превращается в один SQL-запрос. Что ж, сами напросились, L2S будет тестироваться с двумя запросами, тем более что первый запрос по сравнению со вторым выполняется почти моментально, поэтому какая разница.
  2. AR/NH пошел еще дальше, чем L2S. Я ожидал, что написанный мною HQL-запрос выльется в один SQL-запрос. Не срослось. NH сгенерировал и выполнил 23 запроса. Но, присмотревшись к природе этих запросов, я увидел, что это запросы к таблице Supplier, которая в тесте никак не участвует, но зато есть в моем маппинге. Я понял, что, по всей видимости, не отключил lazy loading, поэтому просто в очередной раз отметил свое незнание AR/NH, исправил свойство Supplier класса Product на SupplierID, а атрибут [BelongsTo] поменял на [Property], после чего просто перезапустил тесты и получил корректные SQL-запросы. Замечание: Да, вы правильно подумали, результаты общих тестов для AR из предыдущей части тоже учитывают эти дополнительные запросы, но исходя из того, что я увидел в результатах выполнения тестов, мы не получили здесь особого прироста производительности.
  3. Я наконец-то убедился, что L2S и EF писали разные люди :) Даже несмотря на то, что я специально сделал LINQ-запросы в L2S и EF идентичными, генерируемые SQL-запросы отличаются. Более того, они отличаются не только текстом, но и производительностью, что вы увидите в результатах.

Анализ результатов

Результаты тестов несколько необычны. Для простоты я запустил все запросы каждого теста в одном наборе и посмотрел на Query cost каждого запроса, который считается относительно всего набора.

В первом тесте я получил следующие результаты:

Классический ADO.NET – 14%
LINQ 2 SQL – 2 + 48 = 50%
Entity Framework – 22 %
Active Record (NHibernate) – 14%

Вот и еще один сюрприз: запрос L2S оказался существенно медленнее запроса EF, не говоря уже про победителей – NH и наш контрольный запрос. Зато NH превзошел все ожидания. HQL очень чисто трансформировался в SQL, его план выполнения в точности совпал с планом выполнения контрольного запроса. План выполнения EF тоже неплох, а вот L2S намудрил. Да, здесь есть важный момент: это всего лишь ОДИН тест, на основании которого не стоит судить об ORM в целом. Вполне возможно, есть запросы, где намудрит EF или даже NH. В любом случае, среднее время выполнения этих запросов намного меньше результирующего времени, учитывающего дополнительные расходы на построение запроса и материализацию результата (в среднем где-то 50-150 мс согласно профайлеру). И тут уже L2S несомненно опережает EF и AR/NH с большим отрывом.

Во втором тесте я сравнил сложный запрос с джойнами и набор простых запросов для L2S:

Сложный запрос с джойнами – 25%
Набор простых запросов – 75%

Как видите, один запрос выиграл, что неудивительно. Однако, в целом, можно сказать, что с точки зрения базы данных потери на lazy loading не такие уж и большие.

Третий тест был посвящен EF:

Сложный запрос с джойнами – 18%
Набор простых запросов – 72%

Как видите, результаты похожи до безобразия.

И, наконец, в четвертом тесте я сравнил запросы для NH:

Сложный запрос с джойнами – 6%
Набор простых запросов – 94%

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

Выводы

Какие выводы мы можем сделать по результатам этих тестов?

  1. Прежде всего, если у вас есть возможность выполнить один сложный запрос – выполняйте, не бегайте по коллекциям и не инициализируйте их отдельно, это немного сэкономит вам драгоценное время. Особенно это касается ситуаций, где bottleneck находится в базе данных, а не внутри ORM.
  2. Похоже, оптимизация запросов в L2S сделана немного хуже, чем в EF. Я слышал, что в EF team привлекли очень толковых специалистов по оптимизации запросов, так что, возможно, это результат их работы.
  3. Как показывает практика, генерируемые запросы достаточно неплохи по сравнению с контрольным, так что основная проблема в производительности ORM находится все-таки за пределами базы данных, а именно внутри ORM, в алгоритмах генерации SQL-запроса, работы с контекстом и материализации данных.
  4. Я протестировал LINQ-запросы для L2S и EF. Было бы еще интересно посмотреть, как с заданием справится eSQL (Entity SQL) для EF. Если у кого-то есть опыт – поделитесь.
  5. Аналогично я хотел бы узнать у специалистов насчет HQL в NH. Насколько я понял, в NH есть еще и альтернативный способ создавать сложные запросы через джойны – при помощи критериев. Какие у них плюсы и минусы?

Бонус

Ну, и напоследок бонус тем, кто дочитал до этого места :) Собственно, сгенерированные сложные запросы с джойнами для NH, EF и L2S. Не пугайтесь их, они не такие страшные, как кажутся на первый взгляд :)

NHibernate:

exec sp_executesql N'select product2_.ProductID as ProductID4_, product2_.ProductName as ProductN2_4_, product2_.UnitPrice as UnitPrice4_, product2_.SupplierId as SupplierId4_ from [Order Details] orderdetai0_ inner join Orders order1_ 
on orderdetai0_.OrderID=order1_.OrderID inner join Products product2_ on orderdetai0_.ProductID=product2_.ProductID where (product2_.ProductID=product2_.ProductID )AND(order1_.OrderID=order1_.OrderID )AND(order1_.CustomerID=@p0 
)',N'@p0 nvarchar(5)',@p0=N'BERGS'

Entity Framework:

exec sp_executesql N'SELECT 
[Project3].[CustomerID] AS [CustomerID], 
[Project3].[C1] AS [C1], 
[Project3].[C3] AS [C2], 
[Project3].[OrderID] AS [OrderID], 
[Project3].[C2] AS [C3], 
[Project3].[ProductID] AS [ProductID], 
[Project3].[ProductName] AS [ProductName], 
[Project3].[QuantityPerUnit] AS [QuantityPerUnit], 
[Project3].[UnitPrice] AS [UnitPrice], 
[Project3].[UnitsInStock] AS [UnitsInStock], 
[Project3].[UnitsOnOrder] AS [UnitsOnOrder], 
[Project3].[ReorderLevel] AS [ReorderLevel], 
[Project3].[Discontinued] AS [Discontinued], 
[Project3].[CategoryID] AS [CategoryID], 
[Project3].[SupplierID] AS [SupplierID]
FROM ( SELECT 
    [Extent1].[CustomerID] AS [CustomerID], 
    1 AS [C1], 
    [Project2].[OrderID] AS [OrderID], 
    [Project2].[ProductID] AS [ProductID], 
    [Project2].[ProductName] AS [ProductName], 
    [Project2].[SupplierID] AS [SupplierID], 
    [Project2].[CategoryID] AS [CategoryID], 
    [Project2].[QuantityPerUnit] AS [QuantityPerUnit], 
    [Project2].[UnitPrice] AS [UnitPrice], 
    [Project2].[UnitsInStock] AS [UnitsInStock], 
    [Project2].[UnitsOnOrder] AS [UnitsOnOrder], 
    [Project2].[ReorderLevel] AS [ReorderLevel], 
    [Project2].[Discontinued] AS [Discontinued], 
    [Project2].[C1] AS [C2], 
    [Project2].[C2] AS [C3]
    FROM  [dbo].[Customers] AS [Extent1]
    LEFT OUTER JOIN  (SELECT 
        [Extent2].[OrderID] AS [OrderID], 
        [Extent2].[CustomerID] AS [CustomerID], 
        [Project1].[ProductID] AS [ProductID], 
        [Project1].[ProductName] AS [ProductName], 
        [Project1].[SupplierID] AS [SupplierID], 
        [Project1].[CategoryID] AS [CategoryID], 
        [Project1].[QuantityPerUnit] AS [QuantityPerUnit], 
        [Project1].[UnitPrice] AS [UnitPrice], 
        [Project1].[UnitsInStock] AS [UnitsInStock], 
        [Project1].[UnitsOnOrder] AS [UnitsOnOrder], 
        [Project1].[ReorderLevel] AS [ReorderLevel], 
        [Project1].[Discontinued] AS [Discontinued], 
        [Project1].[C1] AS [C1], 
        1 AS [C2]
        FROM  [dbo].[Orders] AS [Extent2]
        LEFT OUTER JOIN  (SELECT 
            [Extent3].[OrderID] AS [OrderID], 
            [Extent4].[ProductID] AS [ProductID], 
            [Extent4].[ProductName] AS [ProductName], 
            [Extent4].[SupplierID] AS [SupplierID], 
            [Extent4].[CategoryID] AS [CategoryID], 
            [Extent4].[QuantityPerUnit] AS [QuantityPerUnit], 
            [Extent4].[UnitPrice] AS [UnitPrice], 
            [Extent4].[UnitsInStock] AS [UnitsInStock], 
            [Extent4].[UnitsOnOrder] AS [UnitsOnOrder], 
            [Extent4].[ReorderLevel] AS [ReorderLevel], 
            [Extent4].[Discontinued] AS [Discontinued], 
            1 AS [C1]
            FROM  [dbo].[Order Details] AS [Extent3]
            LEFT OUTER JOIN [dbo].[Products] AS [Extent4] ON [Extent3].[ProductID] = [Extent4].[ProductID] ) AS [Project1] ON [Extent2].[OrderID] = [Project1].[OrderID] ) AS [Project2] ON [Extent1].[CustomerID] = 
[Project2].[CustomerID]
    WHERE [Extent1].[CustomerID] = @p__linq__1
)  AS [Project3]
ORDER BY [Project3].[CustomerID] ASC, [Project3].[C3] ASC, [Project3].[OrderID] ASC, [Project3].[C2] ASC',N'@p__linq__1 nvarchar(5)',@p__linq__1=N'BERGS'

LINQ 2 SQL:

exec sp_executesql N'SELECT [t0].[CustomerID]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[CustomerID] = @p0',N'@p0 nvarchar(5)',@p0=N'BERGS'

exec sp_executesql N'SELECT [t2].[ProductID], [t2].[ProductName], [t2].[SupplierID], [t2].[CategoryID], [t2].[QuantityPerUnit], [t2].[UnitPrice], [t2].[UnitsInStock], [t2].[UnitsOnOrder], [t2].[ReorderLevel], [t2].[Discontinued], (
    SELECT COUNT(*)
    FROM [dbo].[Order Details] AS [t3]
    INNER JOIN [dbo].[Products] AS [t4] ON [t4].[ProductID] = [t3].[ProductID]
    WHERE [t3].[OrderID] = [t0].[OrderID]
    ) AS [value]
FROM [dbo].[Orders] AS [t0]
LEFT OUTER JOIN ([dbo].[Order Details] AS [t1]
    INNER JOIN [dbo].[Products] AS [t2] ON [t2].[ProductID] = [t1].[ProductID]) ON [t1].[OrderID] = [t0].[OrderID]
WHERE [t0].[CustomerID] = @x1
ORDER BY [t0].[OrderID], [t1].[ProductID]',N'@x1 nchar(5)',@x1=N'BERGS'