Первоначально я написал большинство запросов в плагине spring-security-ui, используя HQL, потому что считаю его более интуитивным, чем критерии, но HQL работает только с Hibernate и реляционными базами данных. Пул-запрос обновил запросы, чтобы использовать критерии запросов, чтобы позволить плагину использоваться с хранилищами данных NoSQL, но один запрос не соответствовал стилю программирования, который я использовал. Это не имело большого значения, но, поскольку большая часть кода контроллера в основном является кодом CRUD и очень похожа на другие, я попытался сохранить универсальный код и внедрить общую логику в базовые классы.
Оригинальный HQL включил этот
|
1
|
hql.append " AND e.aclObjectIdentity.aclClass.id=:aclClass" |
и преобразованный код критерия был
|
1
2
3
4
5
|
aclObjectIdentity { aclClass { eq 'id', params.long('aclClass') }} |
со всем запросом, подобным этому:
|
01
02
03
04
05
06
07
08
09
10
11
|
def results = lookupClass().createCriteria().list(max: max, offset: offset) { // other standard criteria method calls if (params.aclClass) { aclObjectIdentity { aclClass { eq 'id', params.long('aclClass') } } }} |
Это заставило меня задуматься о создании способа представления этой двухуровневой проекции и критерия в целом.
Если мы восстановим опущенные необязательные скобки, код станет
|
1
2
3
4
5
|
aclObjectIdentity({ aclClass({ eq('id', params.long('aclClass')) })}) |
Поэтому должно быть более ясно, что это последовательность вызовов методов; вызов aclObjectIdentity с аргументом закрытия, затем aclClass с аргументом закрытия и, наконец, eq со String и long аргументом. Разделение замыканий как локальных переменных делает это более понятным, во-первых, как
|
1
2
3
4
5
6
7
|
def aclClassClosure = { eq('id', params.long('aclClass'))}aclObjectIdentity({ aclClass(aclClassClosure)}) |
а потом
|
1
2
3
4
5
6
7
8
9
|
def aclClassClosure = { eq 'id', params.long('aclClass')}def aclObjectIdentityClosure = { aclClass(aclClassClosure)}aclObjectIdentity(aclObjectIdentityClosure) |
Чтобы сделать это немного более конкретным, скажем, у нас есть три класса предметной области;
Department :
|
1
2
3
|
class Department { String name} |
Manager :
|
1
2
3
4
|
class Manager { String name Department department} |
и Employee :
|
1
2
3
4
|
class Employee { String name Manager manager} |
Мы создаем несколько экземпляров:
|
1
2
3
4
|
Department d = new Department(name: 'department1').save()Manager m = new Manager(name: 'manager1', department: d).save()Employee e1 = new Employee(name: 'employee1', manager: m).save()Employee e2 = new Employee(name: 'employee2', manager: m).save() |
и позже хочу выполнить запрос:
|
1
2
3
4
5
6
7
8
9
|
Employee.createCriteria().list(max: 10, offset: 0) { eq 'name', 'employee1' manager { department { eq 'name', 'department1' } }} |
Моя цель — представить этот запрос только с некоторыми вспомогательными методами и без каких-либо замыканий (или как можно меньше). Разделив это, как указано выше, мы имеем
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
def departmentClosure = { eq 'name', 'department1'}def managerClosure = { department(departmentClosure)}def criteriaClosure = { eq 'name', 'employee1' manager(managerClosure)}Employee.createCriteria().list([max: 10, offset: 0], criteriaClosure) |
Когда запрос выполняется, делегат criteriaClosure устанавливается на экземпляр HibernateCriteriaBuilder при использовании Hibernate или аналогичного компоновщика для MongoDB или любой другой используемой вами реализации GORM. В построителе определены методы для eq , like , between и т. Д., Поэтому, когда вы делаете эти вызовы в закрытии критериев, они запускаются в построителе.
Оказывается, это работает одинаково, если вы разбиваете замыкание на несколько замыканий и вызываете их с помощью построителя в качестве делегата для каждого. Итак, такой метод работает:
|
1
2
3
4
5
6
7
8
|
def runCriteria(Class clazz, List<Closure> criterias, Map paginateParams) { clazz.createCriteria().list(paginateParams) { for (Closure criteria in criterias) { criteria.delegate = delegate criteria() } }} |
а это значит, что мы можем разделить
|
1
2
3
4
5
6
7
8
9
|
Employee.createCriteria().list(max: 10, offset: 0) { eq 'name', 'employee1' manager { department { eq 'name', 'department1' } }} |
в
|
01
02
03
04
05
06
07
08
09
10
11
|
def closure1 = { eq 'name', 'employee1'}def closure2 = { manager { department { eq 'name', 'department1' } }} |
и запустить его как
|
1
|
runCriteria Employee, [closure1, closure2], [max: 10, offset: 0] |
Но как мы можем сделать эту проекцию общей? Это внутренний вызов метода, заключенный в одно или несколько замыканий, которые проецируются в другой класс домена.
В конечном итоге я хочу уметь указывать проекцию с внутренним критерием вызова без замыканий:
|
1
2
3
|
def projection = buildProjection('manager.department', 'eq', ['name', 'department1'])runCriteria Employee, [closure1, projection], [max: 10, offset: 0] |
Вот метод buildProjection который делает это:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
Closure buildProjection(String path, String criterionMethod, List args) { def invoker = { String projectionName, Closure subcriteria -> delegate."$projectionName"(subcriteria) } def closure = { -> delegate."$criterionMethod"(args) } for (String projectionName in (path.split('\\.').reverse())) { closure = invoker.clone().curry(projectionName, closure) } closure} |
Чтобы понять, как это работает, еще раз посмотрим на внутреннее замыкание:
|
1
2
3
|
department { eq 'name', 'department1'} |
Это будет вызвано как вызов метода для делегата, по сути
|
1
2
3
|
delegate.department({ eq 'name', 'department1'}) |
Groovy позволяет нам вызывать методы динамически, используя GString s, так что это то же самое, что
|
1
2
3
4
5
|
String methodName = 'department'delegate."$methodName"({ eq 'name', 'department1'}) |
Таким образом, мы можем представить вложенные замыкания как внутреннее замыкание, вызываемое как аргумент замыкания его содержащего замыкания, и вызываемое как аргумент замыкания его содержащего замыкания, и так далее, пока у нас не закончатся уровни.
И мы можем построить замыкание, которое вызывает eq 'name', 'department1' (или любой метод критерия с аргументами, это просто упрощенный пример), так как
|
1
2
3
|
def closure = { -> delegate."$criterionMethod"(args)} |
Итак, чтобы представить вложенные замыкания, начните с замыкания ‘invoker’:
|
1
2
3
|
def invoker = { String projectionName, Closure subcriteria -> delegate."$projectionName"(subcriteria)} |
и последовательно клонируем его на каждом уровне вложенности и каррируем, чтобы внедрить имя проекции и его внутреннее замыкание, поскольку построитель критериев не ожидает никаких аргументов замыкания, работая изнутри:
|
1
2
3
|
for (String projectionName in (path.split('\\.').reverse())) { closure = invoker.clone().curry(projectionName, closure)} |
Итак, наконец, мы можем запустить разложенный запрос как одно или несколько «основных» критериальных замыканий со стандартными вызовами методов критериев плюс ноль или более производных замыканий проекций:
|
1
2
3
4
5
6
7
|
def criteria = { eq 'name', 'employee1'}def projection = buildProjection('manager.department', 'eq', ['name', 'department1'])runCriteria Employee, [criteria, projection], [max: 10, offset: 0] |
Честно говоря, я сомневаюсь, что здесь есть много возможностей для повторного использования, но проработка этого помогла мне лучше понять, как GORM выполняет запросы критериев. Я буду говорить об этом и некоторых других темах GORM на Greach в следующем месяце, поэтому, если вам это интересно, обязательно посмотрите запись этого выступления.
| Ссылка: | Построение динамических запросов GORM от нашего партнера JCG Берта Беквита из блога « Армия солипсистов» . |