Building SQL in Java

February 15, 2010

Handling SQL within Java applications can be tricky. For one thing, Java does not support multi-line string constants, so you can end up with code that looks like this:

String sql = "select *" +
             "from Employee" +
             "where name like 'Fred%'";

This code is not just ugly but also error-prone: did you notice the missing space between Employee and where?

A further challenge when working with SQL in Java is that we often need to build the SQL dynamically. Suppose we were generating a query based on data the user had entered through a search page. We would want to build the WHERE clause dynamically, based on which data the user had entered:

List<String> params = new ArrayList<String>();
StringBuilder sqlBuilder = new StringBuilder()
    .append("select * ")
    .append("from Employee ")
    .append("where 1=1 ");

if (name != null) {
    sqlBuilder.append("and name like ? ");
    params.add(name + "%");
}

if (age != null) {
    sqlBuilder.append("and age = ? ");
    params.add(age);
}

String sql = sqlBuilder.toString();

Note that we've added a dummy predicate (1=1) so we don't have to always decide whether to prepend where or and to subsequent predicates. This is not always necessary--we often have a predicate that's always required, such as active = 'Y'--but it's awkward.

To address these problems, I created a simple class called SelectBuilder. SelectBuilder is used like this:

List<String> params = new ArrayList<String>();
SelectBuilder sqlBuilder = new SelectBuilder()
    .column("*")
    .from("Employee");

if (name != null) {
    sqlBuilder.where("name like ?");
    params.add(name + "%");
}

if (age != null) {
    sqlBuilder.where("age = ?");
    params.add(age);
}

Here, we don't need to use the dummy predicate, as SelectBuilder takes care of properly adding our where and and keywords. We also didn't have to worry about adding spaces between the different SQL fragments.

Like StringBuilder, SelectBuilder uses setter chaining so we can write code that reads like the SQL statement itself:

SelectBuilder sqlBuilder = new SelectBuilder()
    .column("e.id")
    .column("e.name")
    .column("d.name as deptName")
    .from("Employee e");
    .join("Department d on e.dept_id = d.id")
    .where("e.salary > 100000");

SelectBuilder doesn't care about the order in which its methods are called. Consider a base class that represents a report that can be customized by subclassing:

public class BaseEmpReport {
    public String buildSelect() {

        SelectBuilder sqlBuilder = new SelectBuilder()
            .column("e.id")
            .column("e.name")
            .from("Employee e");
            .where("e.salary > 100000");
        
        modifySelect(sqlBuilder);

        return sqlBuilder.toString();
    }

    protected void modifySelect(SelectBuilder builder) {
    }
}

We can subclass this report to add a column representing the employee's department name:

public class DeptReport extends BaseEmpReport {
    protected void modifySelect(SelectBuilder builder) {

        builder
            .column("d.name as deptName")
            .join("Department d on e.dept_id = d.id")
            .where("d.name = 'Marketing'");

    }
}

After writing this class, I discovered the Squiggle SQL Builder library. Squiggle's SelectQuery class is similar to SelectBuilder, but it manages more of the SQL syntax with Java objects and methods. For example, with SelectQuery you might write:

    Table orders = new Table("orders_table");
    SelectQuery select = new SelectQuery(orders);
    select.addColumn(orders, "id");
    select.addColumn(orders, "total_price");
    select.addCriteria(new MatchCriteria(orders, "status", MatchCriteria.EQUALS, "processed"));
    select.addCriteria(new MatchCriteria(orders, "items", MatchCriteria.LESS, 5));

The equivalent in SelectBuilder would read like this:

    SelectBuilder select = new SelectBuilder()
    .column("id")
    .column("total_price")
    .from("orders_table")
    .where("status = 'processed'")
    .where("items < 5");

I find the latter to more readable and more flexible than the Squiggle code.

The full code for SelectBuilder is as follows:

package ca.krasnay.common.sql;

import java.util.ArrayList;
import java.util.List;

public class SelectBuilder {

    private List<String> columns = new ArrayList<String>();

    private List<String> tables = new ArrayList<String>();

    private List<String> joins = new ArrayList<String>();

    private List<String> leftJoins = new ArrayList<String>();

    private List<String> wheres = new ArrayList<String>();

    private List<String> orderBys = new ArrayList<String>();

    private List<String> groupBys = new ArrayList<String>();

    private List<String> havings = new ArrayList<String>();

    public SelectBuilder() {

    }

    public SelectBuilder(String table) {
        tables.add(table);
    }

    private void appendList(StringBuilder sql, List<String> list, String init,
String sep) {
        boolean first = true;
        for (String s : list) {
            if (first) {
                sql.append(init);
            } else {
                sql.append(sep);
            }
            sql.append(s);
            first = false;
        }
    }

    public SelectBuilder column(String name) {
        columns.add(name);
        return this;
    }

    public SelectBuilder column(String name, boolean groupBy) {
        columns.add(name);
        if (groupBy) {
            groupBys.add(name);
        }
        return this;
    }

    public SelectBuilder from(String table) {
        tables.add(table);
        return this;
    }

    public SelectBuilder groupBy(String expr) {
        groupBys.add(expr);
        return this;
    }

    public SelectBuilder having(String expr) {
        havings.add(expr);
        return this;
    }

    public SelectBuilder join(String join) {
        joins.add(join);
        return this;
    }

    public SelectBuilder leftJoin(String join) {
        leftJoins.add(join);
        return this;
    }

    public SelectBuilder orderBy(String name) {
        orderBys.add(name);
        return this;
    }

    @Override
    public String toString() {

        StringBuilder sql = new StringBuilder("select ");

        if (columns.size() == 0) {
            sql.append("*");
        } else {
            appendList(sql, columns, "", ", ");
        }

        appendList(sql, tables, " from ", ", ");
        appendList(sql, joins, " join ", " join ");
        appendList(sql, leftJoins, " left join ", " left join ");
        appendList(sql, wheres, " where ", " and ");
        appendList(sql, groupBys, " group by ", ", ");
        appendList(sql, havings, " having ", " and ");
        appendList(sql, orderBys, " order by ", ", ");

        return sql.toString();
    }

    public SelectBuilder where(String expr) {
        wheres.add(expr);
        return this;
    }
}