Tagged: filtration

[Grails] Параметры пейджинации как модель и простая фильтрация

Допустим есть простой доменный класс:

class User {
    String email
    String name
    Date dateCreated
}

Сгенерируем контроллер, и посмотрим на list() action который выгребает из БД всех пользователей.

class UserController {
...
    def list() {
        respond User.list(), model:[userInstanceCount: User.count()]
    }
...
}

Посмотрим что получилось в броузере:
Grails 2.3. list action

Что если мы хотим добавить пейджинацию? Очень просто, 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]
    }
...
}

Результат:

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