Search code examples
java-8concatenation

Java 8 How to concat query condition based on a list of string


How can I write the following code in a more elegant way using java8?

String query = "select device, manufacturer, type from devices where username = ?";

boolean found = false;
if (deviceTypes.stream().filter(t -> "typeA".equals(t)).findFirst().isPresent()) {
    query += " AND type like 'A%'";
    found = true;
}

if (deviceTypes.stream().filter(t -> "typeB".equals(t)).findFirst().isPresent()) {
    if (found) {
        query += " OR ";

    } else {
        query += " AND ";
    }
    query += " type like 'B%'";
}

If in the device types there are both A and B, the query should contain both with OR.

Example:-

deviceTypes="A,B" --> query condition => AND type like 'A%' OR type like 'B%';

Example:-

deviceTypes="B" --> query condition => AND type like 'B%';
thanks a lot

Solution

  • You may store the association between the deviceTypes strings and the predicate expressions in a map like

    final Map<String,String> typeToExpression
        = Map.of("typeA", "type like 'A%'", "typeB", "type like 'B%'");
    

    Or use static final. This factory method requires Java 9+ but constructing a map should be straight-forward.

    Then, one solution would be

    String query = "select device, manufacturer, type from devices where username = ?";
    query += deviceTypes.stream()
        .map(typeToExpression::get).filter(Objects::nonNull)
        .reduce((a, b) -> a + " OR " + b)
        .map(" AND "::concat).orElse("");
    

    which produces a string similar to your approach. But considering the operator precedence, I suppose, you might rather want

    String query = "select device, manufacturer, type from devices where username = ?";
    query += deviceTypes.stream()
        .map(typeToExpression::get).filter(Objects::nonNull)
        .reduce((a, b) -> "(" + a + " OR " + b + ")")
        .map(" AND "::concat).orElse("");
    

    This is optimized for your described case of having a rather small number of possible conditions, especially for two or less. But we can still reduce some of the largest string concatenation steps with only slightly more code.

    final String baseQ = "select device, manufacturer, type from devices where username = ?";
    String query = deviceTypes.stream()
        .map(typeToExpression::get).filter(Objects::nonNull)
        .reduce((a, b) -> "(" + a + " OR " + b + ")")
        .map(cond -> baseQ + " AND " + cond).orElse(baseQ);
    

    Here, baseQ is a compile-time constant, which in turn makes baseQ + " AND " a compile-time constant too which is combined with the predicate(s) in one step. If there is only one matching predicate, only one string concatenation will be performed. Further, if no matching predicate has been found, this evaluates to the already existing constant baseQ string.

    But if you expect large numbers of predicates, a different approach should be used, optimized for larger numbers of elements, at the expense of the small number cases:

    final String baseQ = "select device, manufacturer, type from devices where username = ?";
    String query = deviceTypes.stream()
        .map(typeToExpression::get).filter(Objects::nonNull)
        .collect(
            () -> new StringJoiner(" OR ", baseQ + " AND (", ")").setEmptyValue(baseQ),
            StringJoiner::add,
            StringJoiner::merge)
        .toString();
    

    Unlike the other approaches this will always put brackets around the predicates, even if there is one, but it will still only use that single pair of brackets when there are three or more. It avoids performing multiple string concatenations in a row by letting the StringJoiner do the entire work.