Вопрос «какой ORM мне следует использовать» действительно нацелен на вершину огромного айсберга, когда речь идет об общей стратегии доступа к данным и оптимизации производительности в крупномасштабном приложении.
Проектирование и поддержка баз данных
По большому счету, это один из наиболее важных факторов, определяющих пропускную способность приложения или веб-сайта, управляемого данными, и часто полностью игнорируется программистами.
Если вы не используете правильные методы нормализации, ваш сайт обречен. Если у вас нет первичных ключей, почти каждый запрос будет медленным. Если вы пользуетесь хорошо известными анти-шаблонами, такими как таблицы для пар «ключ-значение» (AKA Entity-Attribute-Value) без уважительной причины, вы увеличите количество физических операций чтения и записи.
Если вы не воспользуетесь возможностями, которые предоставляет вам база данных, такими как сжатие страниц, FILESTREAM
хранение (для двоичных данных), SPARSE
столбцы, hierarchyid
для иерархий и т. Д. (Все примеры SQL Server), то вы не увидите ничего рядом с производительность, которую вы могли видеть.
Вам следует начать беспокоиться о своей стратегии доступа к данным после того, как вы спроектировали свою базу данных и убедились, что она настолько хороша, насколько это возможно, по крайней мере, на данный момент.
Eager vs. Lazy Loading
Большинство ORM использовали технику, называемую отложенной загрузкой для отношений, что означает, что по умолчанию он будет загружать один объект (строку таблицы) за раз, и совершать обход в базу данных каждый раз, когда ему нужно загрузить один или несколько связанных (чужих) ключевые) строки.
Это не хорошо или плохо, скорее это зависит от того, что на самом деле будет сделано с данными, и от того, сколько вы знаете заранее. Иногда ленивая загрузка абсолютно правильная вещь. Например, NHibernate может решить вообще ничего не запрашивать, а просто сгенерировать прокси для определенного идентификатора. Если все, что вам когда-либо нужно, это само удостоверение личности, зачем ему просить больше? С другой стороны, если вы пытаетесь распечатать дерево каждого отдельного элемента в трехуровневой иерархии, отложенная загрузка становится операцией O (N²), что крайне негативно сказывается на производительности.
Одним из интересных преимуществ использования «чистого SQL» (то есть необработанных запросов / хранимых процедур ADO.NET) является то, что оно в основном заставляет вас задуматься о том, какие именно данные необходимы для отображения любого экрана или страницы. ORMs и функция отложенной загрузки не помешать вам делать это, но они действительно дают вам возможность быть ... ну, лениво , и случайно взрываются количество запросов вы исполняете. Таким образом, вы должны понимать свои возможности загрузки ORM и всегда быть бдительными в отношении количества запросов, которые вы отправляете на сервер для любого данного запроса страницы.
Кэширование
Все основные ORM поддерживают кэш первого уровня, AKA «идентификационный кеш», что означает, что если вы дважды запрашиваете один и тот же объект по его идентификатору, это не требует повторного приема, а также (если вы правильно спроектировали свою базу данных ) дает вам возможность использовать оптимистичный параллелизм.
Кэш-память L1 довольно непрозрачна в L2S и EF, вы должны верить, что она работает. NHibernate более четко об этом ( Get
/ Load
vs. Query
/ QueryOver
). Тем не менее, до тех пор, пока вы пытаетесь запросить по идентификатору как можно больше, у вас все будет в порядке. Многие люди забывают о кеше L1 и многократно просматривают одну и ту же сущность с помощью чего-то, кроме ее идентификатора (то есть поля поиска). Если вам нужно сделать это, вы должны сохранить идентификатор или даже весь объект для будущих поисков.
Также есть кэш 2-го уровня («кеш запросов»). NHibernate имеет этот встроенный. Linq to SQL и Entity Framework имеют скомпилированные запросы , которые могут значительно снизить нагрузку на сервер приложений, компилируя само выражение запроса, но оно не кэширует данные. Похоже, что Microsoft считает это проблемой приложения, а не доступа к данным, и это является основным слабым местом как L2S, так и EF. Излишне говорить, что это также слабое место «сырого» SQL. Чтобы получить действительно хорошую производительность с любым другим ORM, кроме NHibernate, вам нужно реализовать свой собственный фасад кэширования.
Есть также «расширение» кеша L2 для EF4, что нормально , но на самом деле не является полной заменой кеша уровня приложения.
Количество запросов
Реляционные базы данных основаны на наборах данных. Они действительно хороши в создании больших объемов данных за короткий промежуток времени, но они не так хороши с точки зрения задержки запросов, потому что в каждой команде есть определенные накладные расходы. Хорошо спроектированное приложение должно играть в сильные стороны этой СУБД и стараться минимизировать количество запросов и максимизировать объем данных в каждом.
Теперь я не говорю, чтобы запрашивать всю базу данных, когда вам нужна только одна строка. То , что я хочу сказать, если вам нужно Customer
, Address
, Phone
, CreditCard
и Order
ряды все в то же время для того , чтобы служить одной страницы, то вы должны задать для них все в то же время, не выполняете каждый запрос по отдельности. Иногда это хуже, вы увидите код, который запрашивает одну и ту же Customer
запись 5 раз подряд, сначала чтобы получить Id
, потом Name
, потом EmailAddress
, потом ... это смехотворно неэффективно.
Даже если вам необходимо выполнить несколько запросов, которые все работают с совершенно разными наборами данных, обычно все же более эффективно отправлять все это в базу данных в виде единого «сценария» и возвращать несколько наборов результатов. Вы беспокоитесь о накладных расходах, а не об общем объеме данных.
Это может звучать как здравый смысл, но часто очень легко потерять все запросы, которые выполняются в различных частях приложения; ваш провайдер членства запрашивает таблицы пользователей / ролей, ваше действие «Заголовок» запрашивает корзину покупок, ваше действие «Меню» запрашивает таблицу карты сайта, ваше действие «Боковая панель» запрашивает список рекомендуемых продуктов, а затем, возможно, ваша страница разделена на несколько отдельных автономных областей, которые запрашивайте таблицы «История заказов», «Недавно просмотренные», «Категория» и «Инвентарь» по отдельности, и, прежде чем вы это узнаете, вы выполняете 20 запросов, прежде чем сможете даже начать обслуживание страницы. Это просто разрушает производительность.
Некоторые фреймворки - и я думаю здесь в основном о NHibernate - невероятно умны в этом отношении и позволяют вам использовать то, что называется фьючерсами, которые объединяют целые запросы и пытаются выполнить их все сразу, в последнюю возможную минуту. AFAIK, ты сам по себе, если хочешь сделать это с помощью любой из технологий Microsoft; Вы должны встроить это в логику своего приложения.
Индексирование, предикаты и прогнозы
По крайней мере, 50% разработчиков, с которыми я общаюсь, и даже некоторые администраторы баз данных имеют проблемы с концепцией покрытия индексов. Они думают: «Ну, Customer.Name
столбец проиндексирован, поэтому каждый поиск, который я делаю по имени, должен быть быстрым». За исключением того, что это не работает таким образом, если Name
индекс не охватывает конкретный столбец, который вы ищете. В SQL Server это делается с INCLUDE
помощью CREATE INDEX
оператора.
Если вы наивно используете SELECT *
везде - и это более или менее то, что будет делать каждый ORM, если вы явно не укажете иное с помощью проекции - тогда СУБД вполне может решить полностью игнорировать ваши индексы, поскольку они содержат непокрытые столбцы. Проекция означает, что, например, вместо этого:
from c in db.Customers where c.Name == "John Doe" select c
Вы делаете это вместо этого:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
И это будет, для большинства современных ORMs, поручить это только пойти и запросить Id
и Name
столбцы , которые предположительно охватываемые индексом (но не Email
, LastActivityDate
или любые другие столбцы случились придерживаться там).
Также очень легко полностью отбросить любые преимущества индексации, используя неподходящие предикаты. Например:
from c in db.Customers where c.Name.Contains("Doe")
... выглядит почти идентично нашему предыдущему запросу, но на самом деле приведет к полному сканированию таблицы или индекса, потому что оно переводится в LIKE '%Doe%'
. Аналогично, другой запрос, который выглядит подозрительно простым:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Предполагая, что у вас есть индекс BirthDate
, у этого предиката есть хороший шанс сделать его полностью бесполезным. Наш гипотетический программист, очевидно, пытался создать своего рода динамический запрос («фильтровать только дату рождения, если этот параметр был указан»), но это неправильный способ сделать это. Вместо этого написано так:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... теперь движок БД знает, как это настроить и выполнить поиск по индексу. Одно незначительное, казалось бы, незначительное изменение в выражении запроса может существенно повлиять на производительность.
К сожалению, LINQ в целом делает слишком легким написание плохих запросов, подобных этому, потому что иногда провайдеры могут угадать, что вы пытались сделать, и оптимизировать запрос, а иногда нет. Таким образом, вы получите разочаровывающе противоречивые результаты, которые были бы ослепительно очевидными (для опытного администратора БД, во всяком случае), если бы вы только что написали простой старый SQL.
По сути, все сводится к тому, что вам действительно нужно внимательно следить как за генерируемым SQL, так и за планами выполнения, к которым они приводят, и если вы не получаете ожидаемых результатов, не бойтесь обойти Слой ORM время от времени и вручную код SQL. Это касается любого ORM, а не только EF.
Транзакции и блокировка
Нужно ли отображать данные с точностью до миллисекунды? Возможно - это зависит - но, вероятно, нет. К сожалению, Entity Framework не дает вамnolock
, вы можете использовать только READ UNCOMMITTED
на уровне транзакции (не на уровне таблицы). На самом деле ни один из ОРМ не является особенно надежным по этому поводу; если вы хотите выполнять грязное чтение, вам нужно перейти на уровень SQL и написать специальные запросы или хранимые процедуры. Итак, все сводится к тому, насколько легко вам сделать это в рамках.
Entity Framework прошел большой путь в этом отношении - версия 1 EF (в .NET 3.5) была ужасна, сделав невероятно трудным прорвать абстракцию «сущностей», но теперь у вас есть ExecuteStoreQuery и Translate , так что это действительно не плохо. Подружитесь с этими парнями, потому что вы будете их часто использовать.
Существует также проблема блокировок записи и взаимоблокировок, а также общая практика удержания блокировок в базе данных как можно меньше времени. В этом отношении большинство ORM (включая Entity Framework) на самом деле имеют тенденцию быть лучше, чем необработанный SQL, поскольку они инкапсулируют шаблон единицы работы , который в EF - SaveChanges . Другими словами, вы можете «вставлять», «обновлять» или «удалять» сущности в свое душевное содержание, когда захотите, и быть уверенными в том, что никакие изменения на самом деле не будут переданы в базу данных, пока вы не передадите единицу работы.
Обратите внимание, что UOW не является аналогом длительной транзакции. UOW все еще использует функции оптимистичного параллелизма ORM и отслеживает все изменения в памяти . Ни один оператор DML не генерируется до окончательной фиксации. Это позволяет максимально сократить время транзакции. Если вы создаете свое приложение, используя сырой SQL, довольно трудно добиться этого отложенного поведения.
Что это конкретно означает для EF: Сделайте ваши единицы работы как можно более грубыми и не передавайте их до тех пор, пока вам это не понадобится. Сделайте это, и вы получите гораздо меньшую конкуренцию за блокировку, чем при использовании отдельных команд ADO.NET в случайное время.
EF полностью подходит для приложений с высоким трафиком / высокой производительностью, точно так же, как любая другая среда подходит для приложений с высоким трафиком / высокой производительностью. Важно то, как вы используете это. Вот быстрое сравнение наиболее популярных фреймворков и того, что они предлагают с точки зрения производительности (легенда: N = не поддерживается, P = частично, Y = да / поддерживается):
Как вы можете видеть, EF4 (текущая версия) выглядит не так уж плохо, но, вероятно, это не лучший вариант, если производительность является вашей главной задачей. NHibernate гораздо более зрелый в этой области, и даже Linq to SQL предоставляет некоторые повышающие производительность функции, которых EF еще не имеет. Необработанный ADO.NET часто будет работать быстрее для очень специфических сценариев доступа к данным, но, когда вы соберете все части воедино, он на самом деле не предлагает много важных преимуществ, которые вы получаете от различных сред.