Питання "який ORM я повинен використовувати" насправді орієнтований на вершину величезного айсберга, коли мова йде про загальну стратегію доступу до даних та оптимізацію продуктивності в масштабному застосуванні.
Дизайн та обслуговування баз даних
Це, з великим запасом, єдиний найважливіший визначальний коефіцієнт пропускної здатності керованого даними програми або веб-сайту, який часто повністю ігнорується програмістами.
Якщо ви не використовуєте належних методів нормалізації, ваш сайт приречений. Якщо у вас немає первинних ключів, майже кожен запит буде повільним. Якщо ви використовуєте відомі анти-шаблони, такі як використання таблиць для пар ключових значень (AKA Entity-Attribute-Value) без поважних причин, ви збільшите кількість фізичних зчитувань і записів.
Якщо ви не скористаєтесь функціями, які надає вам база даних, наприклад стиснення сторінок, FILESTREAM
зберігання (для двійкових даних), SPARSE
стовпці, hierarchyid
ієрархії тощо (усі приклади SQL Server), ви не побачите ніде поблизу вистава, яку ви могли бачити.
Вам слід почати турбуватися про свою стратегію доступу до даних після того, як ви створили свою базу даних і переконаєтеся, що вона настільки ж хороша, як це можливо, принаймні на даний момент.
Ігр проти ледачого завантаження
Більшість ORM використовували техніку, яку називають ледачим завантаженням для відносин, а це означає, що за замовчуванням вона завантажує по одному об'єкту (рядок таблиці) за один раз і здійснює зворотну подорож до бази даних кожного разу, коли потрібно завантажити одне чи багато пов'язаних (закордонних) ключ) рядки.
Це не є хорошою чи поганою річчю, скоріше залежить від того, що насправді буде зроблено з даними та наскільки ви знаєте наперед. Іноді ледачий навантаження - це абсолютно правильна річ. Наприклад, NHibernate може вирішити взагалі нічого не запитувати і просто генерувати проксі для певного ідентифікатора. Якщо все, що вам коли-небудь потрібно, - це саме ідентифікатор, чому він повинен просити більше? З іншого боку, якщо ви намагаєтеся надрукувати дерево кожного окремого елемента в 3-х рівневій ієрархії, ліниве завантаження стає операцією O (N²), що вкрай погано для продуктивності.
Одним із цікавих переваг використання "чистого SQL" (тобто необроблених запитів ADO.NET / збережених процедур) є те, що воно в основному змушує задуматися про те, які саме дані потрібні для відображення будь-якого екрана чи сторінки. ORMs і функція відкладеної завантаження не перешкодити вам робити це, але вони дійсно дають вам можливість бути ... ну, ліниво , і випадково вибухають кількість запитів ви виконуєте. Таким чином, вам потрібно зрозуміти свої функції, які бажають завантажувати ORM, і будьте завжди пильні щодо кількості запитів, які ви надсилаєте на сервер для будь-якого запиту на сторінку.
Кешування
Усі основні ORM підтримують кеш першого рівня, "кеш ідентичності" AKA, що означає, що якщо ви двічі запитаєте одне і те ж об'єкт за його ідентифікатором, воно не потребує другого зворотного шляху, а також (якщо ви правильно створили свою базу даних ) дає можливість використовувати оптимістичну одночасність.
Кеш L1 досить непрозорий у L2S та EF, вам належить вірити, що він працює. NHibernate про це явніше ( Get
/ Load
проти 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% розмов, з якими я розмовляю, і навіть деякі DBA, здається, мають проблеми з концепцією покриття індексів. Вони думають: "ну 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 в цілому робить занадто легко писати такі погані запити, тому що іноді постачальники можуть вгадати, що ви намагалися зробити, та оптимізувати запит, а іноді вони не так. Таким чином, ви закінчуєте розчаровуючи непослідовні результати, які були б сліпуче очевидними (як би це не було для досвідченого DBA), якби ви щойно написали звичайний старий SQL.
В основному все зводиться до того, що вам справді слід уважно стежити як за згенерованим SQL, так і за планами виконання, які вони ведуть, і якщо ви не отримуєте очікуваних результатів, не бійтеся обійти Рівень ORM час від часу і вручну кодує SQL. Це стосується будь-яких ОРМ, а не лише 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 для SQL надає деякі функції для підвищення продуктивності, яких EF досі не має. Сирий ADO.NET часто буде швидше для дуже конкретних сценаріїв доступу до даних, але, коли ви складете всі частини разом, це дійсно не пропонує багато важливих переваг, які ви отримуєте від різних рамок.