Build JPA Specification from tree

Build JPA Specification from tree



I have created an API which allows users to build out queries using a tree. The tree is built from the SearchOperationRequest class.


SearchOperationRequest


@Data
@ApiModel(value = "SearchOperationRequest", description = "Condition for the query")
public class SearchOperationRequest

@ApiModelProperty(value = "Conditional statement for the where clause", allowableValues = "EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH, ENDS_WITH, CONTAINS")
private SearchConditionOperation condition;

@ApiModelProperty(value = "Column name to be searched on")
private String column;

@ApiModelProperty(value = "Value of column")
private Object value;

@ApiModelProperty(value = "Value of OR / AND")
private SearchComparator comparator;

@ApiModelProperty(value = "Left node of comparator condition")
private SearchOperationRequest left;

@ApiModelProperty(value = "Right node of comparator condition")
private SearchOperationRequest right;

public boolean isTreeLeaf()
return left == null && right == null;


public boolean isComparator()
return comparator != null;




So from this example I could create a SearchOperationRequest that asks for all WHERE hidden = false AND X = 88


SearchOperationRequest


"searchOperation":
"left":
"column": "Hidden",
"condition": "EQUALS",
"value": false
,
"comparator": "AND",
"right":
"left":
"column": "X",
"condition": "EQUALS",
"value": 88
,
"comparator": "AND"




This request is built into a specification using a generic specification builder.


public class GenericSpecificationsBuilder<U>

public Specification<U> buildSpecificationFromSearchOperationRequest(SearchOperationRequest root, Function<SpecificationSearchCriteria, Specification<U>> converter) !stack.empty())

if (pointer.isTreeLeaf())
specStack.push(converter.apply(new SpecificationSearchCriteria(pointer.getColumn(), pointer.getCondition(), pointer.getValue())));


if (pointer.isComparator())
comparatorStack.push(pointer);


if (pointer.getRight() != null)
stack.push(pointer.getRight());


if (pointer.getLeft() != null)
pointer.setRight(pointer.getLeft());
pointer.setLeft(null);
else if (!stack.empty())
SearchOperationRequest temp = stack.pop();
pointer.setRight(temp);


pointer = pointer.getRight();



while (specStack.size() <= comparatorStack.size())
comparatorStack.pop();


while (!comparatorStack.empty())

SearchOperationRequest searchOperationRequest = comparatorStack.pop();

Specification<U> operand1 = specStack.pop();
Specification<U> operand2 = specStack.pop();
if (searchOperationRequest.getComparator().equals(SearchComparator.AND))
specStack.push(Specification.where(operand1)
.and(operand2));
else if (searchOperationRequest.getComparator().equals(SearchComparator.OR))
specStack.push(Specification.where(operand1)
.or(operand2));




return specStack.pop();




My current works great for RIGHT heavy tree's. Meaning queries such as:


WHERE X = 6 AND X = 9
WHERE Z = 5 OR T=9
WHERE Z = 5 OR T=9 OR H=6



But it does not work with building more complex trees where the criteria in braces should get precedence and executed first.


WHERE (X = 6 OR Z = 9) AND (T=6 OR H=8)



The model for this more complex SearchOperationRequest would be:


SearchOperationRequest


"searchOperation":
"left":
"left":
"column": "X",
"condition": "EQUALS",
"value": 6
,
"comparator": "AND",
"right":
"column": "Z",
"condition": "EQUALS",
"value": 9

,
"comparator": "AND",
"right":
"left":
"column": "T",
"condition": "EQUALS",
"value": 6
,
"comparator": "AND",
"right":
"column": "H",
"condition": "EQUALS",
"value": 8





How do I modify my GenericSpecificationsBuilder to be able to handle more complex SearchOperationRequest trees?


GenericSpecificationsBuilder


SearchOperationRequest





Looks like I didn't put the root comparator on the stack, fixed the code. Did you get it to work?
– ingen
Sep 3 at 17:13


comparator





@ingen First couple of test cases and it is working great.
– Sixthpoint
Sep 3 at 20:38




1 Answer
1



Let's follow the execution flow using your example tree.


AND
/
leftOR rightOR
/ /
X=6 Z=9 T=6 H=8



When we exit the first while loop, our stacks look like:


while


stack =
comparatorStack = AND, leftOR, rightOR
specStack = X=6, Z=9, T=6, H=8



The same state enters the final while loop.


while


while (!comparatorStack.empty())

SearchOperationRequest searchOperationRequest = comparatorStack.pop();

Specification<U> operand1 = specStack.pop();
Specification<U> operand2 = specStack.pop();
if (searchOperationRequest.getComparator().equals(SearchComparator.AND))
specStack.push(Specification.where(operand1)
.and(operand2));
else if (searchOperationRequest.getComparator().equals(SearchComparator.OR))
specStack.push(Specification.where(operand1)
.or(operand2));




The problem here is you're pushing the result back on the specStack. So at the second iteration, you'll be popping off the result of the first iteration (the rightOR), as well as Z=9, and apply the leftOr logic to it.


specStack


rightOR


Z=9


leftOr



Decomposing the tree



Let's take a step back and look at how you decompose the tree, more specifically:


if (pointer.getLeft() != null)
pointer.setRight(pointer.getLeft());
pointer.setLeft(null);
else if (!stack.empty())
SearchOperationRequest temp = stack.pop();
pointer.setRight(temp);



The problem with this code is you are altering the nodes in the tree. In the first example, at one time our pointer points to the node:


Z=9
/
null rightOR



That doesn't look right. Instead of decomposing the tree using a stack (Depth First Search), you could use a queue (Breadth First Search) and get the ordering you're after for free.



BFS



Does that solve the problem of applying each logical operator (comparator) to the right operands? Not quite, to be able to solve both layouts below, we could decompose the operators and operands in different workflows, instead of all of them together.


comparator


AND | rootAND
/ | /
leftOR rightOR | leftOR rightOR
/ / | / /
X=6 Z=9 T=6 H=8 | X=6 AND Z=9 H=8
| /
| T=6 Y=3



Solution



The first json-like representation in your post has an illogical layout, as logical operators are expected to operate on both a left and right operand. Instead you had:


"right":
"left":
"column": "X",
"condition": "EQUALS",
"value": 88
,
"comparator": "AND"



Let's consider a solution for symmetrical representations, one where both a left and right operand are present for each logical operator.



First we process the tree Breadth First, level by level. Meanwhile, we put each comparator on a stack, so we get the last ones out first in our second while loop.


comparator


while



In the second loop we then use a new Queue to store our 'in-between-results' as we work our way back to the root.


Queue


Queue<SearchOperationRequest> queue = new LinkedList<>();
Deque<SearchOperationRequest> comparatorStack = new LinkedList<>();

if (root == null || !root.isComparator()) return;
queue.add(root);
while(!queue.isEmpty())
SearchOperationRequest node = queue.poll();
comparatorStack.push(node);
if(node.left != null && node.left.isComparator()) queue.add(node.left);
if(node.right != null && node.right.isComparator()) queue.add(node.right);


Queue<Specification> specQueue = new LinkedList<>();
while(!comparatorStack.isEmpty())
SearchOperationRequest comparator = comparatorStack.pop();
// reverse operand order so already computed values are polled correctly
Specification operand2;
SearchOperationRequest pointer = comparator.getRight();
if(pointer.isTreeLeaf())
operand2 = converter.apply(new SpecificationSearchCriteria(pointer.getColumn(), pointer.getCondition(), pointer.getValue()));
else
operand2 = specQueue.poll();

Specification operand1;
pointer = comparator.getLeft();
if(pointer.isTreeLeaf())
operand1 = converter.apply(new SpecificationSearchCriteria(pointer.getColumn(), pointer.getCondition(), pointer.getValue()));
else
operand1 = specQueue.poll();

if (comparator.equals(SearchComparator.AND))
specQueue.add(Specification.where(operand1).and(operand2));
else if (comparator.equals(SearchComparator.OR))
specQueue.add(Specification.where(operand1).or(operand2));



return specQueue.poll();



I haven't tested the code, but you should be able to extract (and refactor) the workflows.



Required, but never shown



Required, but never shown






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

Edmonton

Crossroads (UK TV series)