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] } ... }
Результат:
Такой подход сократит вам много кода, и сделает его более безопасным и объектно ориентированным.
Пример демо проекта я выложил на гитхаб.