Первоначально я написал большинство запросов в плагине 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 Берта Беквита из блога « Армия солипсистов» . |