用Java构建SQL字符串的最干净方法


107

我想构建一个SQL字符串来进行数据库操作(更新,删除,插入,选择等等)-而不是使用数百万个“ +”和引号的可怕字符串concat方法,该方法充其量是无法理解的-一定是更好的方法。

我确实考虑过使用MessageFormat-但应该将其用于用户消息,尽管我认为它会做得很合理-但是我想应该在Java sql库中有一些与SQL类型操作更为一致的东西。

Groovy会好吗?

Answers:


76

首先考虑在准备好的语句中使用查询参数:

PreparedStatement stm = c.prepareStatement("UPDATE user_table SET name=? WHERE id=?");
stm.setString(1, "the name");
stm.setInt(2, 345);
stm.executeUpdate();

可以做的另一件事是将所有查询保留在属性文件中。例如,在querys.properties文件中可以放置上面的查询:

update_query=UPDATE user_table SET name=? WHERE id=?

然后借助一个简单的实用程序类:

public class Queries {

    private static final String propFileName = "queries.properties";
    private static Properties props;

    public static Properties getQueries() throws SQLException {
        InputStream is = 
            Queries.class.getResourceAsStream("/" + propFileName);
        if (is == null){
            throw new SQLException("Unable to load property file: " + propFileName);
        }
        //singleton
        if(props == null){
            props = new Properties();
            try {
                props.load(is);
            } catch (IOException e) {
                throw new SQLException("Unable to load property file: " + propFileName + "\n" + e.getMessage());
            }           
        }
        return props;
    }

    public static String getQuery(String query) throws SQLException{
        return getQueries().getProperty(query);
    }

}

您可以按以下方式使用查询:

PreparedStatement stm = c.prepareStatement(Queries.getQuery("update_query"));

这是一个相当简单的解决方案,但是效果很好。


1
我更喜欢使用像这样的干净的SQL构建器:mentabean.soliveirajr.com
TraderJoeChicago 2011年

2
我可以建议您将语句放在InputStream内部,if (props == null)以便在不需要该语句时不要实例化它。
SyntaxRules 2013年

64

对于任意SQL,请使用jOOQ。jOOQ目前支持SELECTINSERTUPDATEDELETETRUNCATE,和MERGE。您可以这样创建SQL:

String sql1 = DSL.using(SQLDialect.MYSQL)  
                 .select(A, B, C)
                 .from(MY_TABLE)
                 .where(A.equal(5))
                 .and(B.greaterThan(8))
                 .getSQL();

String sql2 = DSL.using(SQLDialect.MYSQL)  
                 .insertInto(MY_TABLE)
                 .values(A, 1)
                 .values(B, 2)
                 .getSQL();

String sql3 = DSL.using(SQLDialect.MYSQL)  
                 .update(MY_TABLE)
                 .set(A, 1)
                 .set(B, 2)
                 .where(C.greaterThan(5))
                 .getSQL();

除了获取SQL字符串,您还可以使用jOOQ执行它。看到

http://www.jooq.org

(免责声明:我为jOOQ背后的公司工作)


在许多情况下,这不是一个不好的解决方案,因为您无法让dbms事先使用“ 5”,“ 8”等不同的值来解析该语句吗?我想用jooq执行会解决吗?
Vegard 2013年

@Vegard:您可以完全控制jOOQ如何在其SQL输出中呈现绑定值:jooq.org/doc/3.1/manual/sql-building/bind-values。换句话说,您可以选择是渲染"?"还是内联绑定值。
卢卡斯·埃德

是的,但是关于构建sql的干净方法,如果您不使用JOOQ来执行,这在我眼中会有些混乱。在此示例中,将A设置为1,将B设置为2,依此类推,但是如果不使用JOOQ执行,则必须再执行一次。
Vegard 2013年

1
@Vegard:没有什么可以阻止您将变量传递给jOOQ API并重建SQL语句。另外,还可以使用它们的顺序提取绑定值jooq.org/javadoc/latest/org/jooq/Query.html#getBindValues()通过使用他们的名字,或者叫绑定值jooq.org/javadoc/latest/org/jooq /Query.html#getParams()。我的答案仅包含一个非常简单的示例...不过,我不确定这是否可以解决您的问题?
卢卡斯·埃德

2
这是一个昂贵的解决方案。
Sorter

15

您应该考虑的一种技术是SQLJ-一种将SQL语句直接嵌入Java中的方法。作为一个简单的示例,您可能在名为TestQueries.sqlj的文件中包含以下内容:

public class TestQueries
{
    public String getUsername(int id)
    {
        String username;
        #sql
        {
            select username into :username
            from users
            where pkey = :id
        };
        return username;
    }
}

还有一个额外的预编译步骤,该步骤将您的.sqlj文件转换为纯Java-简而言之,它将查找以以下字符分隔的特殊块

#sql
{
    ...
}

并将它们转换为JDBC调用。使用SQLJ有几个主要优点:

  • 完全抽象了JDBC层-程序员只需要考虑Java和SQL
  • 编译器可以在编译时针对数据库检查您的查询的语法等
  • 使用“:”前缀直接绑定查询中的Java变量的功能

大多数主要数据库供应商都提供了转换器的实现,因此您应该能够轻松找到所需的一切。


根据维基百科,这已经过时了。
宙斯

1
在撰写本文时(2016年1月),SQLJ 在Wikipedia上被称为“已过时”,没有任何引用。它被正式放弃了吗?如果是这样,我会在此答案的顶部贴上警告。
Ashley Mercer

注意:例如,最新版本的Oracle 12c仍支持该技术。我承认这不是最现代的标准,但它仍然有效,并且具有其他系统无法提供的一些好处(例如,对数据库查询的编译时验证)。
Ashley Mercer


9

我将看一下Spring JDBC。每当需要以编程方式执行SQL时,我都会使用它。例:

int countOfActorsNamedJoe
    = jdbcTemplate.queryForInt("select count(0) from t_actors where first_name = ?", new Object[]{"Joe"});

对于任何类型的sql执行,尤其是查询,它确实很棒。它将帮助您将结果集映射到对象,而不会增加完整ORM的复杂性。


我怎样才能得到真正执行的SQL查询?我要记录它。
kodmanyagha

5

我倾向于使用Spring的命名JDBC参数,因此我可以编写一个标准字符串,例如“ select * from blah where colX =':someValue'“; 我认为这很可读。

一种替代方法是在单独的.sql文件中提供字符串,然后使用实用程序方法读取内容。

哦,也值得一看Squill:https://squill.dev.java.net/docs/tutorial.html


我假设您是说您正在使用BeanPropertySqlParameterSource?我几乎同意您的观点,我刚提到的类在使用严格的bean时很酷,但否则我建议使用自定义ParameterizedRowMapper构造对象。
Esko

不完全的。您可以将任何SqlParameterSource与命名的JDBC参数一起使用。它适合我使用MapSqlParameterSource而不是bean品种的需要。无论哪种方式,这都是一个很好的解决方案。但是,RowMappers处理SQL难题的另一面:将结果集转换为对象。
GaryF

4

我对使用像Hibernate这样的ORM的建议表示赞同。但是,在某些情况下肯定无法解决问题,因此,我将借此机会介绍一些我曾经写过的东西:SqlBuilder是一个Java库,用于使用“构建器”样式动态构建sql语句。它既强大又灵活。


4

我一直在研究Java servlet应用程序,该应用程序需要为临时报告目的构造非常动态的SQL语句。该应用程序的基本功能是将一堆命名的HTTP请求参数输入到预编码的查询中,并生成格式正确的输出表。我使用Spring MVC和依赖项注入框架将我所有的SQL查询存储在XML文件中,并将它们与表格式信息一起加载到报表应用程序中。最终,报告要求变得比现有参数映射框架的功能更为复杂,因此我必须编写自己的报告。这是开发中的一个有趣的练习,并且为参数映射提供了一个框架,该框架比我所能找到的任何其他东西都要健壮得多。

新的参数映射如下所示:

select app.name as "App", 
       ${optional(" app.owner as "Owner", "):showOwner}
       sv.name as "Server", sum(act.trans_ct) as "Trans"
  from activity_records act, servers sv, applications app
 where act.server_id = sv.id
   and act.app_id = app.id
   and sv.id = ${integer(0,50):serverId}
   and app.id in ${integerList(50):appId}
 group by app.name, ${optional(" app.owner, "):showOwner} sv.name
 order by app.name, sv.name

最终框架的优点在于,可以使用适当的类型检查和限制检查将HTTP请求参数直接处理到查询中。输入验证不需要额外的映射。在上面的示例查询中, 将检查名为serverId的参数,以确保它可以转换为整数,且范围为0-50。参数appId将作为整数数组处理,长度限制为50。如果字段showOwner如果存在并设置为“ true”,则引号中的SQL位将添加到生成的查询中,以用于可选字段映射。字段还有更多可用的参数类型映射,包括带有可选参数映射的SQL的可选段。它允许开发人员提出尽可能复杂的查询映射。它甚至在报表配置中具有控件,可以确定给定查询是通过PreparedStatement进行最终映射还是仅作为预建查询运行。

对于示例Http请求值:

showOwner: true
serverId: 20
appId: 1,2,3,5,7,11,13

它将产生以下SQL:

select app.name as "App", 
       app.owner as "Owner", 
       sv.name as "Server", sum(act.trans_ct) as "Trans"
  from activity_records act, servers sv, applications app
 where act.server_id = sv.id
   and act.app_id = app.id
   and sv.id = 20
   and app.id in (1,2,3,5,7,11,13)
 group by app.name,  app.owner,  sv.name
 order by app.name, sv.name

我真的认为Spring或Hibernate或其中一个框架应该提供一种更强大的映射机制,以验证类型,允许使用复杂的数据类型,例如数组和其他此类功能。我写引擎只是出于我的目的,对于一般发行版来说,它并不是很完整。目前,它仅适用于Oracle查询,并且所有代码都属于一家大公司。总有一天,我可能会采纳我的想法并建立一个新的开源框架,但是我希望现有的大公司中的一位能够接受挑战。


3

为什么要手动生成所有sql?您是否看过像Hibernate这样的ORM,取决于您的项目,它可能至少完成您需要的95%的工作,然后以一种比原始SQL更干净的方式进行,如果您需要获得最后的性能,则可以创建需要手动调整的SQL查询。



3

Google提供了一个名为Room Persitence Library的库,它提供了一种非常干净的方式为Android Apps编写SQL ,基本上是底层SQLite数据库的抽象层。贝娄是官方网站上的简短代码段:

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
           + "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}

该库的官方文档中有更多示例和更好的文档。

还有一个称为MentaBean的Java Java ORM。它具有不错的功能,并且似乎是编写SQL的非常简单的方法。


根据房间文件Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite。因此,它不是RDBMS的通用ORM库。它主要用于Android Apps。
拉菲·阿尔罕默德

2

读取XML文件。

您可以从XML文件中读取它。它易于维护和使用。有标准的STaX,DOM,SAX解析器可供使用,以使其在Java中仅几行代码。

利用属性做更多

您可以在标记上具有一些带有属性的语义信息,以帮助使用SQL进行更多操作。这可以是方法名称或查询类型,也可以是可以减少编码的任何内容。

维护

您可以将xml放在jar之外并轻松维护它。与属性文件具有相同的好处。

转换次数

XML是可扩展的,可以轻松转换为其他格式。

用例

Metamug使用xml使用sql配置REST资源文件。


如果喜欢,可以使用yaml或json。它们比存储在简单的属性文件中更好
Sorter

问题是如何构建SQL。要构建SQL,如果您需要使用XML,解析器,验证等,那么它很负担。涉及XML来构建SQL的大多数早期尝试都被Annotation取代了。该接受的答案彼得Kochański简洁大方,突出重点-解决问题和维护。注意: 没有其他方法可以使用其他语言维护更好的SQL。
拉菲·阿尔罕默德

我删除了以前的评论I don't see a reason to make use of XML. ,因为无法编辑。
拉菲·阿尔罕默德

1

如果将SQL字符串放在属性文件中,然后再读入,则可以将SQL字符串保留在纯文本文件中。

这不能解决SQL类型问题,但至少可以使从TOAD或sqlplus的复制和粘贴更加容易。


0

除了PreparedStatements中的长SQL字符串(您可以很容易地在文本文件中提供并无论如何将其加载为资源)之外,如何拆分多行代码,还如何获得字符串连接?

您不是直接创建SQL字符串吗?那是编程中最大的禁忌。请使用PreparedStatements,并提供数据作为参数。它极大地减少了SQL注入的机会。


但是,如果您不向公众公开网页-SQL注入是否是相关问题?
维达尔

4
SQL注入始终是相关的,因为它可能偶然发生,也可能是故意发生的。
sleske

1
@Vidar -你可能不被暴露网页向公众现在,但即使代码,将“永远”是内部往往最终得到某种形式的外照射的某一时刻进一步向下行。而且,与在以后重新审核整个代码库以解决问题相比,第一次正确地执行此操作既更快又更安全……
Andrzej Doyle,2009年

4
甚至需要从字符串创建PreparedStatement,对吗?
斯图尔特

是的,但是只要您构建安全的PreparedStatement,从字符串构建PreparedStatement是安全的。您可能应该编写PreparedStatementBuilder类来生成它们,以隐藏级联的混乱情况。
JeeBee
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.