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, February 15, 2009

След Шерлока Холмса в программировании или Немного об agile для программистов

Задумывались ли вы, чем отличаются между собой waterfall и agile подходы к разработке программного обеспечения? Для меня, человека, который раньше работал либо по ad hoc, либо по waterfall, либо по итеративным процессам вроде UP, agile-подходы вроде XP, Scrum и др. (прошу не бить сильно ногами, что буду все причесывать под одну гребенку) раньше казались чем-то сродни ad hoc'а, только с дополнительными правилами, чтобы совершенно не скатиться в анархию. У нас даже шутка ходит о том, на проекте часто бывает не просто agile, а полный agile :) Однако после прочтения книжки Книберга я изменил свое мнение.

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

Примечание. Кстати, лично мне кажется, что Шерлок Холмс пользовался все-таки индуктивным методом, а не дедуктивным, т.к. он, как и любой другой детектив, строил общую версию исходя из более мелких, частных фактов и улик, но это мое личное мнение и оно к делу не относится :).

Так к чему я это все? Да к тому, что waterfall - это как раз способ разработки методом сверху-вниз. То есть мы сначала получаем от клиента по возможности все требования (при помощи бизнес-анализа), анализируем их, оцениваем, строим архитектуру, документируем это все - и вперед, арбайтен. От общего к частному или, иными словами, посредством дедукции. Именно на этом подходе работает большинство нормальных человеческих проектов, начиная от строительства дома и заканчивая запуском спутника в космос. Но разработка ПО - это даже близко не строительство дома, хотя определенные аналогии провести можно. Все-таки поменять в последний день логику работы приложения намного легче, чем соорудить автостоянку под уже построенным зданием. То есть в разработке ПО есть такое понятие как гибкость (agility). Ко всему прочему, заказчики (а это может быть кто угодно) - люди зачастую не очень понимающие то, что они хотят получить в результате, а если и понимающие, то склонные менять свое мнение, иногда очень кардинально. Так что же, заставлять их выдавать на-гора все функциональные и нефункциональные требования к приложению ДО начала разработки?

Вот собственно из этого всего и родились различные agile-подходы к разработке ПО. А потом появился небезызвестный манифест - и пошло-поехало. Так что же такое agile и чем он отличается от обычной водопадной или итеративной практики разработки? Ну, отличий, конечно, много, но я бы в первую очередь сказать, что это - процесс разработки ПО снизу-вверх, то есть индуктивный процесс. С самого начала мы очень мало знаем о том, что мы вообще строим, иногда даже так же мало, как и заказчик. Но нам по барабану. Мы берем то, что есть, тратим не очень много времени на планирование - и стартуем. Сделали что-нибудь стоящее, показали заказчику, определили следующие задачи - и поехали дальше. И так до тех пор, пока заказчик не получит то, что хотел. При этом необходимо отметить, что гибкие методологии, в отличие от своих "жестких" товарищей, все же имеют свою жесткость - жесткость правил, по которым ведется разработка. Если у вас нет unit-тестов - вы обломаетесь на первых же серьезных рефакторингах системы. Если у вас нет code review или pair programming - вам будет сложно организовать коллективное владение кодом, что можем повлечь серьезные проблемы, если кто-то из ведущих разработчиков вдруг заболеет. Если вы забиваете на product backlog - у вас в скором времени возникнет каша с требованиями (впрочем, каша с требованиями возникает и тогда, когда они есть, но на их обновление точно так же забивают). Если вы не проводите ежедневные митинги - ваши разработчики будут дублировать действия друг друга и изобретать велосипеды сотнями, а код превратится в мусорку. А все потому что работа ведется в условиях самоорганизовывающейся команды (self-management team) и нет никого, кто бы мог сверху спустить план, в котором будет четко сказано, кто, что и когда должен делать.

Надо сказать, это сложно, т.к. получается, что каждый член команды должен не просто ответственно работать, но еще и быть достаточно подкованным с технической точки зрения. Так что перед тем, как с криками "ура, я познал истину и теперь все мы будем жить счастливо" бежать к своей команде, подумайте сначала, а сможет ли она работать в таких условиях? И, что еще более важно, подумайте, а надо ли оно вам? Если у вас есть нормальные требования и клиент знает, чего он хочет, то зачем городить огород? Возьмите какой-нибудь UP - и будет вам счастье. Если же этого нет, то добро пожаловать в мир agile :)

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

Отзыв о книжках: DDD Нильссона и Scrum из окопов Книберга

Последние несколько недель были плодотворными в плане чтения книжек. Дочитал "Применение DDD и шаблонов проектирования: проблемно-ориентированное проектирование приложений с примерами на C# и .NET" Джимми Нильссона и прочитал "Scrum и XP: заметки с передовой" Хенрика Книберга. Спешу поделиться отзывами, так как уже давно ничего толкового не читал.

ddd 

Про первую книгу я уже писал относительно недавно. Рекомендую всем, кто чувствует, что ему стало тесно в рамках стандартных технологий и подходов, а архитектура и дизайн его приложений хромают на обе ноги. Domain Driven Design - это радикально другой подход к разработке ПО, поэтому книжка отлично освежает голову, дает совершенно другую перспективу и точку зрения на привычные вещи. Нильссон дает общее представление о DDD, а также на примерах показывает, как можно разрабатывать реальные приложения с использованием DDD, TDD, и различных паттернов проектирования. Автор начинает с простых вещей, вводит рабочий пример и потихоньку продвигается по пути его реализации, задевая вопросы архитектуры, модели предметной области, инфраструктуры для сохраняемости, архитектурных паттернов, unit-тестов, правил, аспектно-ориентированного программирования, пользовательского интерфейса, а также немного проходится по NHibernate. В общем и целом, после прочтения книжки в голове появляется намного более серьезное понимание всех вышеперечисленных вопросов, и даже если DDD вам не подходит, вы вынесете из книги много новых знаний. Сразу хочу предупредить, что это не Эванс, здесь нет серьезной теоретической базы, лишь практика, практика, и еще раз практика. Ну, и ложка дегтя: русский перевод этой книги просто ужасен. Не понимаю, как можно было перевести кучу названий паттернов (чего стоят только прецеденты использования и неведение сохраняемости), а также стандартных терминов (DDD - ППО, проектирование предметной области, TDD - РПТ, разработка посредством тестирования). Да и ладно бы, но там не только это перевели, поэтому постоянно приходится думать, как эта фраза звучит по-английски и что это за очередной термин такой. По всей видимости, после перевода книжку не проверил эксперт в данной предметной области. Поэтому читайте оригинал :)

scrum

Вторая книжка была переведена сообществом Agile Ukraine, за что ребятам огромное человеческое спасибо! Причем, больше спасибо за то, что они разрекламировали ее, потому что в реальности, как и предыдущая книга, эта читается на английском легко и непринужденно. Книжка переведена просто замечательно, в отличие от перевода того же Нильссона. Сама же книга повествует об опыте Хенрика Книберга в постановке и использовании Scrum'а в нескольких командах. Хенрик последовательно проходится по вопросам планирования спринта, работы с product backlog'ом, ролям команды, ведения burndown-диаграммы, проведения scrum-митингов, демо, ретроспектив, а также, что более важно, объясняет, как работать по Scrum-у в условиях fixed-cost проектов, показывает родство Scrum и XP, и делится опытом постановки процесса QA и работы с несколькими командами. В общем и целом, для человека, который знаком с другими процессами разработки ПО, и который имеет не только положительный, но и отрицательный опыт, эта книга может стать хорошим стимулом попробовать поднять Scrum у себя на проекте. Тем более, если у вас ничего другого нет :) В общем, рекомендую не только PM'ам.

Friday, February 6, 2009

Летаргический сон украинского .NET-сообщества

С сожалением вынужден констатировать, что украинское .NET-сообщество либо мертво, либо находится в глубокой и беспробудной спячке. Откуда такие неутешительные выводы? Да так, личные наблюдения. Скажите, пожалуйста, проходила ли когда-нибудь в Украине нормальная полноценная .NET-конференция? Нет, я не про DevDays, которые, надо признаться, в этом году в Киеве уже были похожи хотя бы на что-то более-менее взрослое, а не на обычную рекламу последних разработок Microsoft. Я про что-то более реальное, более осязаемое, с серьезными темами и докладами, с серьезными докладчиками. А давно ли к нам с докладами наведывались реальные девелоперы из Microsoft или хотя бы из западного сообщества? Говорите, что им тут делать, они по таким мероприятиям не ездят? А если я вам скажу, что многие из них ездят не только в Европу или там Австралию, но и в Южную Африку, Египет, Пакистан, Турцию, Болгарию, Ливан? А потом делятся в подкастах, что, мол, вот какая в Софии была классная конференция, а в Пакистане у нас аж 3-тысячная аудитория была. И в Россию приезжают, что делает честь нашим северным соседям не только потому, что они их приглашают, но и потому что они способны организовать что-то серьезное, вроде той же Платформы, РИТ или других конференций. Да, это еще не PDC, но это уже хоть что-то, вы не находите? Хорошо, а много ли у нас других конференций или встреч? Не знаю, как в Киеве или Львове, а вот в Харькове есть еще IT Talk'и, которые проводятся под чутким руководством Жени Устименкова, встречи UNETA, которые были бы невозможны без Вовы Лещинского и еще нескольких человек, за что им честь и хвала, и все. Да, еще иногда заезжают с приветом из Киева QA Club и Agile Gathering. Вот теперь точно все! А теперь еще одно задание: скажите, пожалуйста, сколько вы знаете украинских .NET-блоггеров, которых интересно читать. Много насчитали? Угу, вот мне тоже как-то пальцев на двух руках хватило...

С чего это я так разошелся? Да, кто его знает, наболело, наверно. Вчера в Харькове на чем-то наподобие DevDays были доклады по Windows Azure. Знаете, сколько было людей? Человек 50, не больше. Из них человек 30 - знакомые лица со встреч UNETA, то есть костяк, который ходит на все подобные мероприятия. И это в городе, где по самым скромным подсчетам больше 2 тысяч .NET-разработчиков. А почему? А потому что: а) реклама мероприятия была дана меньше чем за неделю до его проведения и прошла, по-моему, только по рассылке dev.net.ua, многие мои друзья даже не знали о том, что что-то будет, б) в Харькове у людей неоднозначное мнение об уровне подобных мероприятий: серьезные разработчики предпочитают их игнорировать, как и встречи UNETA, которые в последние несколько лет превратились в собрания студентов. Ну что за организация, в самом деле?! Почему подобные мероприятия не анонсируются как-то более серьезно, почему не рассылается реклама по разным фирмам, почему GlobalLogic может собрать пару тысяч человек на Программанию (пусть большинство из них студенты - не важно), а Microsoft Украина - нет? Ну да ладно, проблему с рекламой поправить не сложно. А вот как доказать сильным разработчикам и их работодателям, что этот день (или вечер) пройдет для них не зря? Что они услышат интересные доклады, из которых смогут узнать что-то новое для себя, что-то, что им поможет в их работе. Что они смогут встретиться с интересными людьми, пообщаться, поделиться опытом и приобрести его. Что они смогут сами поучаствовать в докладах, если видят в себе силы и имеют желание! И не говорите мне, что у нас нет ребят, способных на это. Только в одном Харькове среди моих близких и дальних знакомых я знаю десятки подобных людей, которые просто не хотят ходить на наши “местные” конференции из-за их слабого уровня.

Как, в конце-концов, сформировать в Украине или отдельно взятом городе настоящее сообщество, которое сможет не просто вместе двигаться вперед, но и двигать вперед всю отечественную разработку? Может, стоит обратиться за опытом к профессионалам хотя бы того же Microsoft, которые уже собаку съели на этих вопросах и построили у себя в Штатах или Европе реально сильные сообщества. Ведь все реально, было бы только желание. Существуют же online-сообщества типа того же RSDN уже в течение многих лет, и держаться как-то. Так чем мы хуже-то?

Ведь сообщество - это не только "тусовка", это еще и место, где можно подойти и посоветоваться с экспертом в определенной области, услышать про новинки, в конце-концов, самому подготовить доклад о чем-то, если есть желание. А потом пойти и внедрить это на практике у себя в работе. Ведь это, возможно, один из самых эффективных способов повышения своего уровня и уровня окружающих тебя людей. Не говоря уже о том, что это фан. Без подобного роста над самими собой, без обмена опытом, без проб и ошибок мы так и останемся страной третьего мира в разработке ПО. И так и будем заниматься аутсорсингом саппортных проектов или банальных веб-сайтиков и ERP-систем, которыми уже никто не хочется заниматься на Западе. И не будет у нас своих разработок, своих исследований, своих продуктов и своих достижений.

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

Москва

Совсем недавно наткнулся на фотографии из Москвы, которые мы сделали этим летом, будучи там проездом по дороге на Байкал. Когда я выкладывал байкальские, хамар-дабанские и иркутские фотографии, то решил, что Москве уже один фотоальбом был посвящен в 2007 году, поэтому второй можно и отложить. А между тем в фотографиях образца 2008 года тоже было немало хороших, поэтому решил выложить и их на пикасе.

Можно по-разному относится к Москве, как к городу. Кто-то скажет, что Москва - очень красивый город, особенно летом, особенно возле Кремля :) Город с развитой инфраструктурой, большими возможностями, богатой историей. Кто-то возразит, что сейчас город заметно сдал, везде развешана реклама, идет снос старинных зданий, и что вообще ему далеко до Питера. Да и вообще, понаехали тут... Я же скажу, что для меня Москва - это прежде всего один из древнейших на Руси городов, и бывшая столица нашей некогда необъятной родины. Москва всегда была коммерческим, торговым городом, а потом ей еще и не повезло, когда она стала столицей сначала Российской империи, а потом - СССР и России. Это во-многом определило судьбу города, его застройку и дух. Тому же Питеру в этом плане, на мой взгляд, повезло чуть больше. Питер намного моложе, но его расцвет пришелся на расцвет Российской империи, а потом он не попал под жернова столичной жизни. Наверно, поэтому и сохранился как исторический и культурный памятник намного лучше. Ну да хватит сравнивать - сегодня мы поем оду Москве :)

Я никогда не был в Москве долго. Зато уже 4 раза был там проездом :) И скажу честно, 2 года назад город мне не очень понравился. Не смотрелся он по сравнению с Питером, да и с Киевом тем же, откровенно говоря, тоже. Но с каждым следующим приездом я все больше убеждался, что в Москве есть что-то особенное, какая-то своя неповторимая изюминка. Москва поражает не только размахом и шиком, но и своим внутренним духом, который еще не успел исчезнуть под давлением свалившихся буржуазно-демократических отношений. Да, самые красивые для глаза места - это стандартный набор: Кремль, Красная площадь, храм Христа Спасителя, Воробьевы горы, Новодевичий монастырь, Арбат, Царицыно, и др. Но там вы не почувствуете атмосферу города. Это все в основном для туристов. Лучше отойти в сторону от всего этого, убежать в обычные московские кварталы, погулять там. Вот там можно прочувствовать, как и чем живут обычные москвичи, понять их. А может, даже соприкоснуться с духом этого великого во всех отношениях города.

А пока несколько фотографий Москвы образца 2008 года:

 Picture 509 Picture 523 Picture 522

  IMG_8971Picture 558 Picture 554

Picture 563 Picture 529

Доклад на харьковской 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.

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

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