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

17

Таким образом, ситуация, с которой я сталкиваюсь достаточно часто, - это ситуация, когда мои модели начинают либо:

  • Вырасти в монстров с тоннами и тоннами методов

ИЛИ

  • Позволяют вам передавать кусочки SQL им, чтобы они были достаточно гибкими и не требовали миллиона различных методов.

Например, скажем, у нас есть модель «виджет». Мы начнем с некоторых основных методов:

  • получить ($ ID)
  • вставить ($ запись)
  • обновление ($ id, $ record)
  • удаление ($ ID)
  • getList () // получить список виджетов

Это все хорошо и здорово, но тогда нам нужно немного отчетности:

  • listCreatedBetween ($ start_date, $ end_date)
  • listPurchasedBetween ($ start_date, $ end_date)
  • listOfPending ()

И тогда отчетность начинает усложняться:

  • listPendingCreatedBetween ($ start_date, $ end_date)
  • listForCustomer ($ customer_id)
  • listPendingCreatedBetweenForCustomer ($ customer_id, $ start_date, $ end_date)

Вы можете видеть, где это растет ... в конце концов, у нас так много особых требований к запросу, что мне нужно либо реализовать тонны и тонны методов, либо какой-то объект "запроса", который я могу передать в один запрос -> запрос ( метод $ query) ...

... или просто прикусить пулю и начать делать что-то вроде этого:

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

Есть определенный призыв просто иметь один такой метод вместо 50 миллионов других более специфических методов ... но иногда кажется "неправильным" вставлять кучу того, что в основном является SQL, в контроллер.

Есть ли «правильный» способ справиться с такими ситуациями? Кажется ли приемлемым вставлять подобные запросы в общий метод -> query ()?

Есть ли лучшие стратегии?

Кит Палмер младший
источник
Я сейчас решаю эту проблему в проекте не из MVC. Возникает вопрос: должен ли уровень доступа к данным абстрагироваться от каждой хранимой процедуры и оставить базу данных уровня бизнес-логики независимой, или уровень доступа к данным должен быть общим, за счет того, что бизнес-уровень знает что-то о базовой базе данных? Возможно, промежуточное решение состоит в том, чтобы иметь что-то вроде ExecuteSP (строка spName, параметры params object []), а затем включить все имена SP в файл конфигурации для чтения бизнес-уровнем. У меня не очень хороший ответ, хотя.
Грег Джексон

Ответы:

10

В паттернах корпоративной архитектуры приложений Мартина Фаулера описан ряд паттернов, связанных с ORM, включая использование объекта Query, что я и предложил бы.

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

У тебя их будет много? Конечно. Можно ли сгруппировать некоторые в общие запросы? Да еще раз.

Можете ли вы использовать внедрение зависимостей для создания объектов из метаданных? Это то, что делают большинство инструментов 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 для "Там нет правильного способа сделать это". Есть несколько правильных способов. Удвоение количества методов при добавлении функции, как это делал OP, является немасштабируемым подходом, и предлагаемая здесь альтернатива одинаково не масштабируется, просто с точки зрения размера базы данных, а не количества функций запроса. Масштабируемые подходы существуют, см. Другие ответы.
Теодор Мердок
4

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

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

совершенно правильный запрос в Django ORM .

Идея состоит в том, что у вас есть какой-то конструктор запросов (в данном случае Purchase.objects), чей внутренний статус представляет информацию о запросе. Такие методы , как get, filter,exclude , order_byявляются действительными и возвращают новый построитель запросов с обновленным статусом. Эти объекты реализуют итеративный интерфейс, поэтому при выполнении итерации по ним выполняется запрос, и вы получаете результаты построенного до сих пор запроса. Хотя этот пример взят из Django, вы увидите ту же структуру во многих других ORM.

Andrea
источник
Я не вижу, какое это имеет преимущество перед чем-то вроде old_purchases = Buyases.query ("date> date.today () И type = Purchase.PRESENT AND status! = Purchase.REJECTED"); Вы не уменьшаете сложность и не абстрагируете что-либо, просто превращая SQL AND и OR в метод AND и OR - вы просто меняете представление AND и OR, верно?
Кит Палмер младший
4
Вообще-то нет. Вы абстрагируете SQL, который дает вам много преимуществ. Во-первых, вы избегаете инъекций. Затем вы можете изменить базовую базу данных, не беспокоясь о слегка отличающихся версиях диалекта SQL, поскольку ORM справится с этим за вас. Во многих случаях вы также можете поставить NoSQL бэкэнд, не замечая этого. В-третьих, эти конструкторы запросов - это объекты, которые вы можете передавать, как и все остальное. Это означает, что ваша модель может создать половину запроса (например, у вас может быть несколько методов для наиболее распространенных случаев), а затем ее можно уточнить в контроллере для обработки ..
Andrea
2
... наиболее конкретные случаи. Типичным примером является определение порядка по умолчанию для моделей в Django. Все результаты запроса будут следовать этому порядку, если не указано иное. В-четвертых, если вам когда-либо понадобится денормализовать ваши данные по соображениям производительности, вам нужно только настроить ORM, а не переписывать все ваши запросы.
Андреа
+1 Для динамических языков запросов, таких как упомянутый, и LINQ.
Эван Плейс
2

Есть третий подход.

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

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

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

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

... и где параметры добавляются в запрос:

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

Этот подход обеспечивает линейный рост кода при добавлении функций без необходимости произвольных непараметрических запросов.

Теодор Мердок
источник
0

Я думаю, что общее мнение заключается в том, чтобы максимально сохранить доступ к данным в ваших моделях в MVC. Один из других принципов разработки заключается в том, чтобы переместить некоторые из ваших более общих запросов (те, которые не имеют прямого отношения к вашей модели) на более высокий, более абстрактный уровень, где вы можете разрешить использовать его и в других моделях. (В RoR у нас есть что-то, что называется framework). Есть еще одна вещь, которую вы должны рассмотреть, и это - ремонтопригодность вашего кода. По мере роста вашего проекта, если у вас есть доступ к данным в контроллерах, его будет все труднее отслеживать (в настоящее время мы сталкиваемся с этой проблемой в огромном проекте). Модели, хотя и перегружены методами, обеспечивают единую точку контакта для любого контроллера, который может закончить тем, что запрашивает из таблиц. (Это также может привести к повторному использованию кода, что, в свою очередь, полезно)

Ricketyship
источник
1
Пример того, о чем ты говоришь ...?
Кит Палмер мл.
0

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

База данных имеет 4 основных операции

  • Вставить
  • Обновить
  • удалять
  • запрос

Другим необязательным методом может быть выполнение некоторой операции с базой данных, которая не подпадает под основные операции с БД. Давайте назовем это Execute.

Вставка и обновления могут быть объединены в одну операцию, которая называется Сохранить.

Многие из ваших методов являются запросами. Таким образом, вы можете создать общий интерфейс для удовлетворения большинства насущных потребностей. Вот пример универсального интерфейса:

 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 не передается между слоями. Обычно это то, что делает ORM, но вы можете свернуть свое собственное и иметь свое собственное отображение.

Итак, в вашем случае у вас есть виджеты. Виджеты будут реализовывать интерфейс IPOCO.

Таким образом, в вашей модели уровня сервиса будет иметь getList().

Потребовался бы слой отображения для обработки getListпреобразования в

Search<Widget>(DataTransferObject<Widget> Dto)

и наоборот. Как уже упоминалось, иногда это делается с помощью ORM, но в конечном итоге вы получаете много типового кода, особенно если у вас есть сотни таблиц. ORM волшебным образом создает параметризованный SQL и запускает его для базы данных. При развертывании собственного, дополнительно в самом слое данных, мапперы понадобятся для настройки SP, linq и т. Д. (В основном, sql идет в базу данных).

Как упоминалось ранее, DTO - это объект, составленный композицией. Возможно, одним из объектов, содержащихся в нем, является объект с именем QueryParameters. Это будут все параметры запроса, которые будут установлены и использованы запросом. Другим объектом будет список возвращаемых объектов из запросов, обновлений, доп. Это полезная нагрузка. В этом случае полезной нагрузкой будет список виджетов List.

Итак, основная стратегия:

  • Вызовы уровня обслуживания
  • Преобразование вызова уровня сервиса в базу данных с использованием своего рода репозитория / отображения
  • Вызов базы данных

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

Джон Рейнор
источник