Стратегії уникнення SQL у ваших контролерах ... або скільки методів я повинен мати у своїх моделях?


17

Тож ситуація, з якою я стикаюся досить часто, - це така ситуація, коли мої моделі починають чинити:

  • Виростають у монстрів тоннами і тонами методів

АБО

  • Дозволяють передавати їм шматочки SQL, щоб вони були досить гнучкими, щоб не потрібно мільйон різних методів

Наприклад, скажімо, у нас є модель "віджет". Почнемо з деяких основних методів:

  • отримати ($ ідентифікатор)
  • вставка ($ запис)
  • оновлення ($ id, $ запис)
  • видалити ($ id)
  • getList () // отримати список віджетів

Це все чудово і безглуздо, але тоді нам потрібна деяка звітність:

  • listCreateBet between ($ start_date, $ end_date)
  • list PurchasedBet between ($ start_date, $ end_date)
  • listOfPending ()

І тоді звіт починає набирати складності:

  • listPendingCreateBet Between ($ start_date, $ end_date)
  • listForCustomer ($ customer_id)
  • listPendingCreateBet BetweenForCustomer ($ customer_id, $ start_date, $ end_date)

Ви можете бачити, де це зростає ... врешті-решт у нас є стільки специфічних вимог до запитів, що мені або потрібно реалізувати тонни і тонни методів, або якийсь об’єкт "запиту", який я можу передати на один запит -> (запит $ запит) метод ...

... або просто кусайте кулю і починайте робити щось подібне:

  • list = MyModel-> query ("start_date> X AND end_date <Y AND pending = 1 AND customer_id = Z")

Існує певний заклик просто мати один подібний метод замість 50 мільйонів інших більш конкретних методів ... але відчуває себе "неправильним" іноді вкладати купу контролера, що в основному є SQL.

Чи існує "правильний" спосіб вирішення подібних ситуацій? Чи здається прийнятним введення таких запитів у загальний метод -> query ()?

Чи є кращі стратегії?


Я зараз переживаю цю проблему в проекті, що не стосується MVC. Постає питання, чи повинен рівень доступу до даних абстрагувати кожну збережену процедуру та залишити агностику бази даних рівня бізнес-логіки, або чи повинен рівень доступу до даних бути загальним, ціною бізнес-рівня відомо щось про базову базу даних? Можливо, проміжним рішенням є мати щось на кшталт ExecuteSP (рядок spName, параметри об’єкта [] параметрів), а потім включити всі імена SP у файл конфігурації для бізнес-шару для читання. Я насправді не дуже добре відповів на це, хоча.
Грег Джексон

Відповіді:


10

" Шаблони архітектури корпоративних додатків" Мартіна Фаулера описує ряд шаблонів, пов'язаних з ORM, включаючи використання об'єкта запитів, що я пропоную.

Об'єкти запитів дозволяють вам слідувати принципу єдиної відповідальності, розділяючи логіку кожного запиту на об'єкти стратегії, що керуються окремо, та керовані ними. Або ваш контролер може керувати їх використанням безпосередньо, або делегувати це вторинному контролеру або допоміжному об'єкту.

У вас їх буде багато? Звичайно. Чи можна згрупувати їх у загальні запити? Так знову.

Чи можете ви використовувати введення залежності для створення об'єктів з метаданих? Саме так роблять більшість інструментів ORM.


4

Немає правильного способу зробити це. Багато людей використовують ORM, щоб відмовитись від усієї складності. Деякі з більш досконалих ORM перекладають вирази коду у складні оператори SQL. ORM також мають свої недоліки, однак для багатьох застосувань переваги перевищують витрати.

Якщо ви не працюєте з масивним набором даних, найпростіше зробити це вибрати всю таблицю в пам'ять і фільтрувати в коді.

//pseudocode
List<Person> people = Sql.GetList<Person>("select * from people");
List<Person> over21 = people.Where(x => x.Age >= 21);

Для внутрішніх додатків звітності такий підхід, мабуть, чудовий. Якщо набір даних дійсно великий, вам почнуть потрібні безліч спеціальних методів, а також відповідних індексів на вашому столі.


1
+ 1 для "Немає правильного способу цього зробити"
ozz

1
На жаль, фільтрація поза набором даних насправді не є варіантом навіть з найменшими наборами даних, з якими ми працюємо - це занадто повільно. :-( Приємно чути, що інші люди натрапляють на мою ж проблему. :-)
Кіт Палмер-молодший

@KeithPalmer з цікавості, наскільки великі ваші столи?
дан

Сотні тисяч рядів, якщо не більше. Забагато фільтрів з прийнятною продуктивністю за межами бази даних, ОСОБЛИВО з розподіленою архітектурою, де бази даних не є на тій же машині, що і додаток.
Кіт Палмер-молодший

-1 для "Немає правильного способу зробити це". Існує кілька правильних способів. Подвоєння кількості методів, коли ви додаєте функцію, як це робив ОП, - це нерозбірливий підхід, а альтернатива, запропонована тут, не менш масштабована, тільки що стосується розміру бази даних, а не кількості запитів. Масштабовані підходи існують, дивіться інші відповіді.
Теодор Мердок

4

Деякі ORM дозволяють будувати складні запити, починаючи з основних методів. Наприклад

old_purchases = (Purchase.objects
    .filter(date__lt=date.today(),type=Purchase.PRESENT).
    .excude(status=Purchase.REJECTED)
    .order_by('customer'))

- ідеально правильний запит в ORM Django .

Ідея полягає в тому, що у вас є якийсь конструктор запитів (у цьому випадку Purchase.objects), внутрішній статус якого представляє інформацію про запит. Такі методи , як get, filter, exclude, order_byє дійсними і повертають новий будівник запитів з оновленим статусом. Ці об’єкти реалізують ітерабельний інтерфейс, так що коли ви повторюєте їх, запит виконується, і ви отримуєте результати побудованого на даний момент запиту. Хоча цей приклад взятий з Django, ви побачите таку ж структуру в багатьох інших ORM.


Я не бачу, яку перевагу це має над чимось на кшталт old_purchases = Purchases.query ("дата> дата.today () І введіть = Покупка. ПРЕДСТАВНИЙ І статус! = Покупка. ЗВЕРНЕНО"); Ви не зменшуєте складність або абстрагуєте щось, просто вводячи SQL AND і ORs у метод AND та ORs - ви просто змінюєте представлення AND і ORs, правда?
Кіт Палмер-молодший

4
Насправді ні. Ви абстрагуєтесь від SQL, що купує вам безліч переваг. По-перше, ви уникаєте ін’єкцій. Потім ви можете змінити базову базу даних, не турбуючись про дещо інші версії діалекту SQL, оскільки ORM обробляє це за вас. У багатьох випадках ви також можете помістити бекенд NoSQL, не помічаючи. По-третє, ці конструктори запитів - це об'єкти, які можна обходити, як і все інше. Це означає, що ваша модель може побудувати половину запиту (наприклад, у вас можуть бути деякі методи для найпоширеніших випадків), а потім її можна вдосконалити в контролері, щоб обробити ..
Andrea

2
... найбільш конкретні випадки. Типовим прикладом є визначення замовлення за замовчуванням для моделей у Django. Усі результати запитів дотримуватимуться цього замовлення, якщо не вказано інше. По-четверте, якщо вам коли-небудь потрібно денормалізувати ваші дані з міркувань продуктивності, вам доведеться лише налаштувати ORM, а не переписувати всі запити.
Андреа

+1 Для таких динамічних мов запитів, як згадувана, та LINQ.
Еван Плейс

2

Є третій підхід.

Ваш конкретний приклад демонструє експоненціальне зростання кількості необхідних методів у міру збільшення кількості необхідних функцій: ми хочемо можливість пропонувати розширені запити, поєднуючи кожну функцію запиту ... якщо ми це робимо, додаючи методи, у нас є один метод для основний запит, два, якщо ми додаємо одну необов'язковість, чотири, якщо ми додаємо два, вісім, якщо додаємо три, 2 ^ n, якщо додаємо n функцій.

Це, очевидно, неможливо досягти понад трьох-чотирьох функцій, і є неприємний запах багатьох тісно пов'язаних кодів, які майже копіюються між методами.

Ви можете уникнути цього, додавши об’єкт даних для утримання параметрів і створити єдиний метод, який будує запит на основі набору наданих (або не наданих) параметрів. У цьому випадку додати нову функцію, таку як діапазон дат, настільки ж просто, як додати в ваш об'єкт даних сеттер і getters для діапазону дат, а потім додати трохи коду, де будується параметризований запит:

if (dataObject.getStartDate() != null) {
    query += " AND (date BETWEEN ? AND ?) "
}

... і де параметри додаються до запиту:

if (dataObject.getStartDate() != null) {
    preparedStatement.setTime(dataObject.getStartDate());
    preparedStatement.setTime(dataObject.getEndDate());
}

Цей підхід дозволяє лінійний ріст коду, оскільки функції додаються, без необхідності допускати довільні, не параметризовані запити.


0

Я думаю, що загальний консенсус полягає в тому, щоб якомога більше доступу до даних у ваших моделях у MVC. Одним з інших принципів дизайну є переміщення деяких ваших більш загальних запитів (тих, які не пов'язані безпосередньо з вашою моделлю) на вищий, більш абстрактний рівень, де ви можете дозволити його використовувати і для інших моделей. (У RoR у нас є щось, що називається фреймворк) Є ще інша річ, яку ви повинні врахувати, і це справність вашого коду. Коли ваш проект зростає, якщо у вас є доступ до даних в контролерах, відстежувати його стає все важче (ми зараз стикаємося з цим питанням у величезному проекті) Моделі, хоча затиснуті методами, забезпечують єдину контактну точку для будь-якого контролера, який може запитати з таблиць. (Це також може призвести до повторного використання коду, що, у свою чергу, вигідно)


1
Приклад того, про що ви говорите ...?
Кіт Палмер-молодший

0

В інтерфейсі вашого сервісного рівня може бути багато методів, але виклик до бази даних може мати лише один.

У базі даних 4 основні операції

  • Вставити
  • Оновлення
  • Видалити
  • Запит

Іншим необов'язковим методом може бути виконання деякої операції з базою даних, яка не підпадає під основні операції з БД. Назвемо це Виконати.

Вставлення та оновлення можна об'єднати в одну операцію під назвою Зберегти.

Багато ваших методів - це запити. Таким чином, ви можете створити загальний інтерфейс для задоволення більшості негайних потреб. Ось приклад загального інтерфейсу:

 public interface IDALService
    {
        DataTransferObject<T> Save<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Search<T>(DataTransferObject<T> Dto) where T: IPOCO;
        DataTransferObject<T> Delete<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Execute<T>(DataTransferObject<T> Dto) where T : IPOCO;
    }

Об'єкт передачі даних є загальним і міститиме всі ваші фільтри, параметри, сортування тощо. Шар даних буде нести відповідальність за аналіз та вилучення цього і налаштування операції з базою даних через збережені процедури, параметризовані sql, linq тощо. Отже, SQL не передається між шарами. Як правило, це робить ОРМ, але ви можете розгорнути своє власне і мати власне відображення.

Отже, у вашому випадку у вас є віджети. Віджети реалізують інтерфейс IPOCO.

Отже, у вашому сервісному шарі модель була б getList().

Вам потрібен шар відображення для обробки getListперетворення в

Search<Widget>(DataTransferObject<Widget> Dto)

і навпаки. Як уже згадували інші, колись це робиться через ORM, але в кінцевому підсумку ви отримуєте безліч типів кодового типу, особливо якщо у вас 100 таблиць. ORM магічно створює параметизований SQL і запускає його проти бази даних. Якщо ви прокатуєте своє власне, додатково в самому шарі даних, потрібні будуть картографи для встановлення SP, linq тощо (в основному sql, що йде до бази даних).

Як було сказано раніше, DTO - це об'єкт, складений композицією. Можливо, одним із об'єктів, що містяться в ньому, є об’єкт, який називається QueryParameters. Це були б усі параметри запиту, які були б встановлені та використані запитом. Іншим об'єктом буде Список повернених об'єктів із запитів, оновлень, доб. Це корисне навантаження. У такому випадку корисним навантаженням буде Список Список віджетів.

Отже, основна стратегія:

  • Виклики рівня обслуговування
  • Трансформація рівня обслуговування виклику до бази даних за допомогою якогось сховища / картографування
  • Виклик бази даних

У вашому випадку я думаю, що модель може мати багато методів, але оптимально ви хочете, щоб виклик бази даних був загальним. Ви все ще маєте багато коду картографічного відображення (особливо з SP) або магічного коду ORM, який динамічно створює параметризований SQL для вас.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.