Статьи

Антишаблон Grails: локально оптимизированные динамические искатели повсюду

Контекст

Grails позволяет очень легко сохраняться и находить вещи, используя доменные классы . Он использует GORM (Grails ‘Object Relational Mapping) под капотом, который по умолчанию использует Hibernate для сопоставления классов домена с таблицами в базе данных. Мощный материал и позволяет легко вставать и бегать очень быстро!

Создание нового приложения в соответствии с так называемыми «лучшими практиками» из блогов, подобных этим & # 55357; & # 56898; и «идиоматический путь Grails», описанный в документах и ​​учебных пособиях, работает в начале, но всегда есть переломный момент — когда приложение выросло до разумного размера — когда нужно начинать следовать другой, возможно, менее Grailsey, стратегии ,

Так что же может пойти не так с использованием динамических искателей в доменном классе?

Возможно, вы можете узнать следующее описание создания одного из ваших ранних приложений Grails?

День 1: Жизнь прекрасна

Вы создаете свои основные классы домена.

Я взял примеры классов предметной области, любезно предоставленные Джеффом Брауном и Грэмом Роше «Классическое руководство по Grails » — классика!

1
2
3
4
grails create-domain-class Album
grails create-domain-class Song
grails create-domain-class Artist
...

Потрясающие.

Простой HomepageController создан, чтобы показать что-то на вашей домашней странице. Он должен возвращать коллекцию, скажем, альбомов для определенного исполнителя, и, следовательно, динамический поиск , очевидно, является самым простым способом сделать это.

1
2
3
4
5
class HomepageController {
    def list(Artist artist) {
        [albums: Album.findAllByArtist(artist)]
    }
}

Вы и ваши товарищи по команде поражаются волшебству ! Производительность не имеет себе равных . Вы можете запросить почти что-нибудь подобное. Вы нашли лучших друзей на всю жизнь, и в течение следующих дней команда просматривает другие истории пользователей домашней страницы, как будто завтра не наступит.

Перемотка вперед на 14 дней.

День 14: DOA

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

Наверняка все добавленные виджеты, графики панели инструментов и отчеты на главной странице не имеют к этому никакого отношения?

Вы включаете ведение журнала запросов Hibernate и пытаетесь увидеть, какие запросы происходят при повторном открытии домашней страницы.

Консоль отображает лавину SQL, пока домашняя страница, наконец, не завершит загрузку.

1
2
3
4
5
6
7
8
9
Hibernate: select this_.id as id1_2_0_, this_.version as version2_2_0_, this_.name as name3_2_0_ from artist this_ where this_.name=? limit ?
Hibernate: select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.artist_id as artist_i3_0_0_, this_.title as title4_0_0_ from album this_ where this_.artist_id=?
Hibernate: select this_.id as id1_2_0_, this_.version as version2_2_0_, this_.name as name3_2_0_ from artist this_ where (this_.name=?) limit ?
Hibernate: select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.artist_id as artist_i3_0_0_, this_.title as title4_0_0_ from album this_ where (this_.artist_id=?)
Hibernate: select this_.id as id1_2_0_, this_.version as version2_2_0_, this_.name as name3_2_0_ from artist this_ where (this_.name=?)
Hibernate: select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.artist_id as artist_i3_0_0_, this_.title as title4_0_0_ from album this_ where this_.artist_id in (?) limit ?
Hibernate: select this_.id as id1_2_0_, this_.version as version2_2_0_, this_.name as name3_2_0_ from artist this_
Hibernate: select this_.id as id1_0_0_, this_.version as version2_0_0_, this_.artist_id as artist_i3_0_0_, this_.title as title4_0_0_ from album this_ where this_.artist_id=?
etc etc etc

Это куча похожих запросов, забивающих вашу консоль.

Кроме того, на производстве это тратит драгоценное время вашего клиента, которое теперь делает покупки в другом месте на более быстром веб-сайте конкурента .

Объяснение

Давайте углубимся в код Grails, потому что где-то что-то должно быть причиной этого.

Вы помните тот простой HomepageController вы лично начинали в первый день, когда жизнь была прекрасной?

1
2
3
4
5
class HomepageController {
    def list(Artist artist) {
        [albums: Album.findAllByArtist(artist)]
    }
}

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

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

В одном месте:

1
2
Artist artist = Artist.findByName(artistName)
Album.findAllByArtist(artist)

(«Хорошо, не элегантно, но это работает»)

В другом месте:

1
Album.findAllWhere(artist: Artist.findWhere(name: artistName))

(«Конечно, похоже, делает то же самое, но в качестве одной строки»)

Снова в другом месте:

1
2
def artists = Artist.findAllWhere(name: artistName)
Album.findAllByArtistInList(artists, [max: 1])

(«Что, почему? И зачем ограничивать результаты только 1 здесь?»)

Снова в другом месте:

1
2
3
4
5
Album.where {
    artist == Artist.list().find { a ->
        a.name == artistName
    }
}.findAll()

( «NOOOooo!»)

Шаблон

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

Случай с альбомом и исполнителями динамического поиска выше часто не далек от реальности. Даже в одной команде разные разработчики в конечном итоге будут везде использовать то, что я называю «локально оптимизированными динамическими поисковиками» . Почему?

  • Их слишком легко начать с
  • Их слишком легко продолжать использовать

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

Динамические искатели очень удобочитаемы (и легко тестируются), поэтому члены команды

  • скопируйте и адаптируйте их для немного другого сценария (здесь нам нужна сортировка, здесь нам нужен только один вместо списка)
  • попытаться быть умнее , написав запрос более «умным» способом (иногда приводящим к обратному)
  • даже не знаю о каких-либо существующих применениях в приложении и просто, с помощью документации или руководителя, пытаюсь выполнить работу с нуля (так они думают)

Недостатки будут проявляться по мере роста приложения:

  • Каждый запрос имеет разную структуру, поэтому по внешнему виду вы не можете определить, есть ли у вас на самом деле 18 различных запросов или только 2 . Любые кэши запросов (например, Hibernate), возможно, либо менее эффективны, либо фактически не нужны, но теперь просто занимают память.
  • Тщательно созданные индексы , созданные администратором базы данных в первый день, в конце большинства запросов больше не используются на 100% из-за произвольного присутствия и порядка столбцов в предложении WHERE. Любая оптимизация базы данных становится бесполезной , и, возможно, вместо этого вводится медленное сканирование таблиц.
  • Любой рефакторинг к запросу «найти альбомы для исполнителя» должен быть тщательно обработан вручную во всех 18 различных местах. Это вызывает у всех головные боли. И ошибки.

Что мы можем с этим поделать?

Может быть, очевидно, но держать его сухим

Централизуйте вещи. Точно так же, как вы делаете, когда код дублируется во 2-й или 3-й раз, вы находите хорошее место в своем приложении, где его может найти каждый разработчик. Вам просто нужно беспокоиться об этом одном месте.

Во-первых, лучшее, что мы можем сделать в Grails, — это создать такое центральное место , например, создав обычную службу Grails, такую ​​как, например, AlbumService .

1
2
grails create-service Album
| Created grails-app/services/example/AlbumService.groovy

Во-вторых, проанализируйте все ваши запросы «найти альбомы для исполнителя» и попробуйте сойтись только на 1 или 2, которые должны быть помещены в новый сервис.

Попробуйте найти разумные имена для некоторых новых методов, которые в некоторых случаях могут быть просто именами исходного динамического поиска, такого как findAllByArtist :

1
2
3
4
5
6
7
@Transactional
class AlbumService {
 
    Album findAllByArtist(Artist artist) {
        Album.findAllByArtist(artist)
    }
}

В-третьих, замените все случаи использования вызовом на новый сервис. Помните наш контроллер домашней страницы? Это изменится так:

1
2
3
4
5
6
7
8
class HomepageController {
 
    AlbumService albumService
 
    def list(Artist artist) {
        [albums: albumService.findAllByArtist(artist)]
    }
}

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

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

Только тест для AlbumService должен иметь дело с тестированием запросов сейчас.

В конце концов, AlbumService может содержать только 1 или 2 результирующих запроса, которые используются из любого необходимого места в кодовой базе, но высоко оптимизированы для скорости, используя все виды функций, таких как кэширование, индексы базы данных и т. Д. Устроить нас в беспорядок должно быть в прошлом. Теперь мы можем создавать другие проблемы & # 55357; & # 56898;

Динамические искатели — это здорово!

Если вы до сих пор не уверены, защищаю ли я abanondon динамические искатели? Нет, конечно, динамические искатели — это лучшее, что когда-либо происходило со времен нарезанной пиццы! Начать новый проект или прототип с ними — отлично! И сделайте умную вещь очень скоро и вместе с командой _ разместите свои запросы вместе _ в организованном месте.

Самый важный урок, который я усвоил: если все запросы находятся в аккуратном, четко определенном месте, чтобы все могли его увидеть и перейти , наиболее вероятно, что члены команды по умолчанию будут придерживаться базового человеческого поведения — разработка программного обеспечения иногда является просто психологией — следуйте по заданному пути и раскрасьте хорошо определенные линии этой части кодовой базы. Понимание того, что уже есть, заставляет людей задуматься о том, стоит ли вводить совершенно новый запрос по сравнению с изменением одного из существующих.