Построить спецификацию JPA с дерева

Я создал API, который позволяет пользователям создавать запросы с использованием дерева. Дерево построено из класса 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;
    }
}

Поэтому из этого примера я мог бы создать SearchOperationRequest который запрашивает все WHERE hidden = false AND X = 88

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

Этот запрос встроен в спецификацию с использованием универсального конструктора спецификаций.

public class GenericSpecificationsBuilder<U> {

    public Specification<U> buildSpecificationFromSearchOperationRequest(SearchOperationRequest root, Function<SpecificationSearchCriteria, Specification<U>> converter) {

        Stack<SearchOperationRequest> stack = new Stack<>();

        Stack<SearchOperationRequest> comparatorStack = new Stack<>();
        Deque<Specification<U>> specStack = new LinkedList<>();

        SearchOperationRequest pointer = root;

        while (pointer != null || !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();
    }
}

Мое настоящее отлично работает для правого тяжелого дерева. Значение таких запросов, как:

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

Но он не работает с построением более сложных деревьев, где критерии в фигурных скобках должны иметь приоритет и выполняться первыми.

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

Модель для этого более сложного 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
        }
    }
}

Как мне изменить свой GenericSpecificationsBuilder чтобы иметь возможность обрабатывать более сложные деревья SearchOperationRequest?

Ответ 1

Пусть выполняется поток выполнения, используя дерево примеров.

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

Когда мы выходим из первой while цикл, наши стеки выглядеть следующим образом:

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

Же состояние входит в окончательном 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));
    }
}

Проблема здесь в том, что вы возвращаете результат обратно на specStack. Таким образом, на второй итерации вы будете выскакивать результат первой итерации (rightOR), а также Z=9 и применять к leftOr логику leftOr.

Разложение дерева

Позвольте сделать шаг назад и посмотреть, как вы разложите дерево, а точнее:

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

Проблема с этим кодом заключается в том, что вы изменяете узлы в дереве. В первом примере, когда наш указатель указывает на узел:

    Z=9      
  /     \ 
null   rightOR

Это не выглядит правильным. Вместо того, чтобы разлагать дерево с помощью стека (Depth First Search), вы можете использовать очередь (Breadth First Search) и получить заказы, которые вы получите бесплатно.

BFS

Решает ли это проблему применения каждого логического оператора (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 

Решение

Первое json-подобное представление в вашем сообщении имеет нелогичный макет, так как ожидается, что логические операторы будут работать как с левым, так и с правым операндом. Вместо этого у вас было:

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

Рассмотрим решение для симметричных представлений, где оба левого и правого операнда присутствуют для каждого логического оператора.

Сначала мы обрабатываем дерево Breadth First, уровень за уровнем. В то же время, мы ставим каждый comparator на стеке, так что мы получаем последние из них из первых в нашей второй в while цикл.

Во втором цикле мы используем новую 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();

Я не тестировал код, но вы должны иметь возможность извлекать (и реорганизовывать) рабочие процессы.