【问题标题】:Cleanest way to build an SQL string in Java在 Java 中构建 SQL 字符串的最简洁方法
【发布时间】:2010-09-27 02:46:20
【问题描述】:

我想构建一个 SQL 字符串来执行数据库操作(更新、删除、插入、选择等)——而不是使用数百万个“+”和引号的糟糕的字符串 concat 方法在最好的——必须有更好的方法。

我确实想过使用 MessageFormat - 但它应该用于用户消息,虽然我认为它会做一个合理的工作 - 但我想应该有一些更符合 java sql 库中的 SQL 类型操作的东西。

Groovy 有用吗?

【问题讨论】:

    标签: java sql oracle string


    【解决方案1】:

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

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

    可以做的另一件事是将所有查询保存在属性文件中。例如 在 queries.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"));
    

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

    【讨论】:

    • 我更喜欢使用像这样一个干净的 SQL 构建器:mentabean.soliveirajr.com
    • 我建议您将InputStream 放在if (props == null) 语句中,这样您就不会在不需要时实例化它。
    • 第一个和最后一个代码sn-p中的c是什么?
    • @AlexanderTerp 它是一个活跃的Connection
    【解决方案2】:

    对于任意 SQL,请使用 jOOQ。 jOOQ目前支持SELECTINSERTUPDATEDELETETRUNCATEMERGE。您可以像这样创建 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();
    

    您也可以使用 jOOQ 来执行它,而不是获取 SQL 字符串。见

    http://www.jooq.org

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

    【讨论】:

    • 在很多情况下这不是一个糟糕的解决方案,因为您无法让 dbms 事先使用“5”、“8”等不同的值解析语句?我猜用 jooq 执行会解决它?
    • @Vegard:您可以完全控制jOOQ 如何在其 SQL 输出中呈现绑定值:jooq.org/doc/3.1/manual/sql-building/bind-values。换句话说,你可以选择是渲染"?"还是内联绑定值。
    • @Vegard:没有什么能阻止您将变量传递给 jOOQ API 并重建 SQL 语句。此外,您可以使用jooq.org/javadoc/latest/org/jooq/Query.html#getBindValues() 按其顺序提取绑定值,或使用jooq.org/javadoc/latest/org/jooq/Query.html#getParams() 按名称命名绑定值。我的回答只包含一个非常简单的例子......不过,我不确定这是否回应了您的担忧?
    • 这是一个昂贵的解决方案。
    • 别忘了添加免责声明,说明您是 jOOQ 背后公司的 CEO。 ;)
    【解决方案3】:

    您应该考虑的一项技术是SQLJ - 一种直接在 Java 中嵌入 SQL 语句的方法。举个简单的例子,您可能在名为 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 变量

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

    【讨论】:

    • 根据维基百科,这个现在已经过时了。
    • 在撰写本文时(2016 年 1 月)SQLJ 在没有任何引用的情况下被称为“过时”on Wikipedia。正式放弃了吗?如果是这样,我会在这个答案的顶部发出警告。
    • NB 该技术仍受支持,例如latest version of Oracle, 12c。我承认它不是最现代的标准,但它仍然有效并且具有其他系统不具备的一些优点(例如针对数据库的查询的编译时验证)。
    【解决方案4】:

    我想知道你是否在追求类似Squiggle 的东西。还有一些非常有用的东西是jDBI。但它不会帮助您处理查询。

    【讨论】:

      【解决方案5】:

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

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

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

      【讨论】:

      • 我怎样才能得到真正执行的 sql 查询?我想记录它。
      【解决方案6】:

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

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

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

      【讨论】:

      • 我假设您的意思是您正在使用 BeanPropertySqlParameterSource?我几乎同意你的观点,我刚才提到的类在使用严格的 bean 时很酷,但否则我建议使用自定义 ParameterizedRowMapper 来构造对象。
      • 不完全。您可以使用任何带有命名 JDBC 参数的 SqlParameterSource。它适合我使用 MapSqlParameterSource 的需要,而不是 bean 品种。无论哪种方式,这都是一个很好的解决方案。然而,RowMappers 处理 SQL 难题的另一面:将结果集转换为对象。
      【解决方案7】:

      我一直致力于开发一个 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 查询,并且所有代码都属于一家大公司。有一天我可能会采纳我的想法并构建一个新的开源框架,但我希望现有的大玩家之一能够接受挑战。

      【讨论】:

        【解决方案8】:

        我支持使用像 Hibernate 这样的 ORM 的建议。然而,在某些情况下,这肯定是行不通的,所以我将借此机会宣传一些我帮助编写的东西:SqlBuilder 是一个使用“builder”样式动态构建 sql 语句的 java 库.它相当强大且相当灵活。

        【讨论】:

          【解决方案9】:

          为什么要手动生成所有的sql?你有没有看过像 Hibernate 这样的 ORM 根据你的项目,它可能会完成你需要的至少 95% 的工作,以比原始 SQL 更简洁的方式完成,如果你需要获得最后一点性能,你可以创建需要手动调整的 SQL 查询。

          【讨论】:

            【解决方案10】:

            你也可以看看 MyBatis (www.mybatis.org) 。它可以帮助您在 java 代码之外编写 SQL 语句,并将 sql 结果映射到您的 java 对象中。

            【讨论】:

              【解决方案11】:

              Google 提供了一个名为Room Persitence Library 的库,该库提供了一种非常简洁的Android 应用程序 编写SQL 方法,基本上是底层SQLite 数据库 之上的抽象层。 Bellow是来自官网的短代码sn-p:

              @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 ORM。它具有很好的功能,并且似乎是一种非常简单的 SQL 编写方式。

              【讨论】:

              • 根据Room documentationRoom provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite。因此,它不是 RDBMS 的通用 ORM 库。它主要用于 Android 应用程序。
              【解决方案12】:

              读取一个 XML 文件。

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

              使用属性做更多事情

              您可以在标签上包含一些带有属性的语义信息,以帮助使用 SQL 做更多事情。这可以是方法名称或查询类型或任何有助于减少代码的内容。

              维护

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

              转化

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

              用例

              Metamug uses xml to configure REST resource files with sql.

              【讨论】:

              • 如果你喜欢,可以使用 yaml 或 json。它们比存储在普通属性文件中更好
              • 问题是如何构建 SQL。要构建 SQL,如果您需要使用 XML、Parser、Validation 等,则负担过重。大多数涉及 XML 来构建 SQL 的早期尝试都被拒绝,转而使用 Annotation。 Piotr Kochańskiaccepted answer 简单而优雅 并且切中要害 - 解决了问题并且可维护。 注意: 没有其他方法可以用不同的语言维护更好的 SQL。
              • 我删除了我之前的评论I don't see a reason to make use of XML. ,因为我无法编辑它。
              【解决方案13】:

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

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

              【讨论】:

                【解决方案14】:

                您如何获得字符串连接,除了 PreparedStatements 中的长 SQL 字符串(您可以轻松地在文本文件中提供并作为资源加载)之外,您会中断多行?

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

                【讨论】:

                • 但是,如果您不向公众公开网页 - SQL 注入是否是一个相关问题?
                • SQL 注入总是相关的,因为它可能会意外发生,也可能是有意的。
                • @Vidar - 您可能现在不会将网页公开给公众,但即使是“始终”在内部的代码也经常最终会获得某种外部曝光更进一步的一些点。与以后审核整个代码库的问题相比,第一次就做对更快、更安全……
                • 即使是 PreparedStatement 也需要从字符串创建,不是吗?
                • 是的,但是从字符串构建 PreparedStatement 是安全的,只要您构建一个安全的 PreparedStatement。您可能应该编写一个 PreparedStatementBuilder 类来生成它们,以隐藏连接事物的混乱。
                猜你喜欢
                • 2023-03-21
                • 1970-01-01
                • 2016-06-22
                • 2016-07-15
                • 1970-01-01
                • 2017-05-25
                • 2020-07-13
                • 2014-05-25
                • 2016-09-12
                相关资源
                最近更新 更多