拓展阅读

linq

querydsl

ORM-01-Hibernate、MyBatis、EclipseLink、Spring Data JPA、TopLink、ActiveJDBC、Querydsl 和 JOOQ 对比

ORM-02-Hibernate 对象关系映射(ORM)框架

ORM-02-JPA Java Persistence API 入门介绍

orm-04-Spring Data JPA 入门介绍

ORM-05-javalite activejdbc

ORM-06-jooq 入门介绍

ORM-07-querydsl 入门介绍

ORM-08-EclipseLink 入门介绍

ORM-09-TopLink

前言

自己通过 jdbc 实现了一个 数据库查询工具,不过后来想拓展查询功能时,总觉得不够尽兴。

所以在想能不能把 SQL 的构建单独抽离出来。

这里整理学习下其他的组件。

jkrasnay/sqlbuilder

SQL构建器实用工具

该包含了一系列实用工具类,用于简化使用SQL。

引入

maven

将以下依赖项添加到您的POM文件中:

<dependency>
  <groupId>ca.krasnay</groupId>
  <artifactId>sqlbuilder</artifactId>
  <version>1.2</version>
</dependency>

SQL构建器依赖于slf4j和Spring JDBC,请确保将所需版本添加到您的 <dependencyManagement> 部分。

Gradle

将以下依赖项添加到您的build.gradle中:

compile 'ca.krasnay:sqlbuilder:1.2'

构建器

构建器简化了SQL字符串的创建。

它们对SQL语法有一定了解,并使在Java中创建动态SQL变得更加方便。

与Java的StringBuilder类似,它们使用可链接的调用。

以下是使用SelectBuilder的示例。

new SelectBuilder()
    .column("name")
    .column("age")
    .from("Employee")
    .where("dept = 'engineering'")
    .where("salary > 100000")
    .toString();

这将生成SQL字符串

select name, age from Employee where dept = 'engineering' and salary > 100000

注意SelectBuilder知道如何用逗号连接列,并用and连接where子句。

更多信息,请参阅 http://john.krasnay.ca/2010/02/15/building-sql-in-java.html

ParameterizedPreparedStatementCreator

Spring有一个强大的抽象称为JdbcTemplate,它使得使用JDBC变得容易。

JdbcTemplate负责从DataSource中正确分配和释放JDBC连接。

它从不返回Connection;相反,连接对象被传递给调用者提供的回调函数。

其中一个回调函数是PreparedStatementCreator,用于根据连接创建预处理语句。

在典型的PreparedStatementCreator中,创建具有由问号指示的可替换参数的SQL,然后通过索引设置参数值。

在使用动态SQL时,跟踪这些索引可能会很具有挑战性。

为了简化此过程,ParameterizedPreparedStatementCreator使用了命名参数。

以下是一个示例:

PreparedStatementCreator psc =
    new ParameterizedPreparedStatementCreator()
        .setSql("update Employee set name = :name where id = :id")
        .setParameter("name", "Bob")
        .setParameter("id", 42);

new JdbcTemplate(dataSource).update(psc);

创建器

每个构建器类都有一个相应的Creator类,它结合了构建器和ParameterizedPreparedStatmentCreator。

PreparedStatementCreator psc =
    new UpdateCreator("Employee")
        .setValue("name", "Bob")
        .whereEquals("id", 42);

new JdbcTemplate(dataSource).update(psc);

创建器本身并不添加太多功能,但它们使得使用构建器和ParameterizedPreparedStatementCreators变得更加容易(而且您不必不断地输入那个非常长的类名!)。

设计的初衷

在Java应用程序中处理SQL可能会有些棘手。

首先,Java不支持多行字符串常量,因此您可能会得到看起来像这样的代码:

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

这段代码不仅难看,而且容易出错:您注意到Employee和where之间缺少空格了吗?

在使用Java处理SQL时的另一个挑战是我们经常需要动态构建SQL。

假设我们根据用户通过搜索页面输入的数据生成查询。

我们希望根据用户输入的数据动态构建WHERE子句:

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();

请注意,我们添加了一个虚假的谓词(1=1),这样我们就不必总是决定是要在后续谓词前加上where还是and。

这并不总是必要的–我们经常有一个始终需要的谓词,例如active = ‘Y’–但这很笨拙。

为了解决这些问题,我创建了一个简单的名为SelectBuilder的类。

SelectBuilder的使用方式如下:

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);
}

在这里,我们不需要使用虚假的谓词,因为SelectBuilder会正确地添加我们的where和and关键字。

我们也不必担心在不同SQL片段之间添加空格。

与StringBuilder类似,SelectBuilder使用设置器链接,因此我们可以编写类似SQL语句本身的代码:

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不关心其方法被调用的顺序。

考虑一个表示可以通过子类定制的报表的基类:

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) {
    }
}

我们可以将此报表子类化以添加表示员工部门名称的列:

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'");

    }
}

编写了这个类之后,我发现了Squiggle SQL Builder库。

Squiggle的SelectQuery类与SelectBuilder类似,但它使用Java对象和方法管理更多的SQL语法。

例如,使用SelectQuery,您可能会编写如下代码:

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));

在SelectBuilder中的等效代码将如下所示:

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

我发现后者比Squiggle代码更易读、更灵活。

SelectBuilder的完整代码如下:

PS: 这里还有一种方式:

SelectBuilder select = new SelectBuilder()
    .column("id")
    .column("total_price")
    .from("orders_table")
    .where(eq("status", "processed"))
	.where(lt("items", 5))

个人感觉其实还是直接写比价直观,但是可以考虑把 3 种方式作为只是不同的写法。

完整代码

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;
    }
}

看的出来,这种就是简单粗暴的 string 拼接。

好处是实现简单,缺点是写法可能并不优雅。

参考资料