Tagged: filtration
[Grails] Параметры пейджинации как модель и простая фильтрация
Допустим есть простой доменный класс:
class User {
String email
String name
Date dateCreated
}
Сгенерируем контроллер, и посмотрим на list() action который выгребает из БД всех пользователей.
class UserController {
...
def list() {
respond User.list(), model:[userInstanceCount: User.count()]
}
...
}
Посмотрим что получилось в броузере:

Что если мы хотим добавить пейджинацию? Очень просто, GORM метод list() имеет встроенную поддержку пейджинации. Например, у нас есть постраничный список пользователей. На каждой странице отображается по десять. Мы хотим выбрать третью страницу отсортированных по имени в восходящем порядке:
def books = User.list(offset: 20, max: 10, sort: 'name', order: 'asc')
offset — это сколько записей пропустить, max — сколько записей выбрать.
Тег добавляет пейджинатор который передаёт эти параметры в параметрах GET запроса. От туда мы их сразу передаём в list():
class UserController {
...
def list() {
respond User.list(params), model:[userInstanceCount: User.count()]
}
...
}
Тут могут возникнуть неприятности: хакер может задать очень большой max и такими большими запросами к БД положить сайт. Для этого нужно ограничивать max:
class UserController {
...
def list() {
params.max = Math.min(params.max ?: 10, 100)
respond User.list(params), model:[userInstanceCount: User.count()]
}
...
}
Хитрая конструкция Math.min(params.max ?: 10, 100) делает следующее:
Если max не задан, устанавливаем его по умолчанию в десять.
Если max задан больше 100, то он будет установлен в сто.
Простая защита для корректирования параметра max.
Если у вас контроллером уже много то эта строчка начинает повторятся. Мелочь, но коробит, DRY. Во вторых, на моём текущем проекте высокие требования по безопасности, и все параметры из запросов мы обязательно оборачиваем в Command object где их строго типизируем.
Так что даже на такую мелочь было решено написать простенький команд обжект:
@Validateable
class ListParams {
public static final int MAX_DEFAULT = 10
public static final int MAX_HIGH_LIMIT = 100
Integer max = MAX_DEFAULT
Integer offset
String sort
String order
static constraints = {
max(nullable: true, max: MAX_HIGH_LIMIT)
offset(nullable: true, min: 0)
sort(nullable: true, blank: false)
order(nullable: true, blank: false, inList: ['asc', 'desc'])
}
Map <String, Object> getParams() {
return [max: correctMax, offset: offset, sort: sort, order: order]
}
Integer getCorrectMax() {
return Math.min(max ?: MAX_DEFAULT, MAX_HIGH_LIMIT)
}
}
И принимаем его аргументом в list():
def index(ListParams listParams) {
respond User.list(listParams.params), model:[userInstanceCount: User.count()]
}
Стоило ли оно того? Наверное, да. Любая типизация и абстракция позволяет нам лучше протестировать и сделать более безопасное приложение.
Самое интересное начинается далее, когда вам начинают быть нужными фильтры. В таком случае можно создать простой класс UserListFilter отнаследованный от ListParams и добавляющий свои поля. Например на форме фильтра у нас есть ещё поле поиска по email, по имени и
@Validateable
class UserListFilter extends ListParams {
String email
String name
Date dateCreatedFrom
Date dateCreatedTo
}
И принимать сразу его через аргумент в контроллере, где Grails автоматически всё сконвертирует, сбиндит и провалидирует:
class UserController {
...
def index(UserListFilter filter) {
def criteria = User.where {
if (filter.email) {
email == filter.email
}
if (filter.name) {
name =~ '%' + filter.name + '%'
}
if (filter.dateCreatedFrom) {
dateCreated >= filter.dateCreatedFrom
}
if (filter.dateCreatedTo) {
dateCreated <= filter.dateCreatedTo
}
}
respond criteria.list(filter.params), model: [userInstanceCount: criteria.count(), filter: filter]
}
...
}
Результат:

Такой подход сократит вам много кода, и сделает его более безопасным и объектно ориентированным.
Пример демо проекта я выложил на гитхаб.