12

我们当前的项目不使用 Hibernate(由于各种原因),我们使用 Spring 的 SimpleJdbc 支持来执行我们所有的数据库操作。我们有一个实用程序类,它抽象了所有 CRUD 操作,但复杂的操作是使用自定义 SQL 查询执行的。

目前,我们的查询以字符串常量的形式存储在服务类本身中,并提供给一个实用程序以由 SimpleJdbcTemplate 执行。我们处于一个必须平衡可读性和可维护性的僵局。类本身内的 SQL 代码更易于维护,因为它与使用它的代码一起存在。另一方面,如果我们将这些查询存储在外部文件(平面文件或 XML)中,SQL 本身将比转义的 java 字符串语法更具可读性。

有没有人遇到过类似的问题?什么是好的平衡?您将自定义 SQL 保存在项目中的什么位置?

示例查询如下:

private static final String FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS = 
"    FROM PRODUCT_SKU T \n" +
"    JOIN \n" +
"    ( \n" +
"        SELECT S.PRODUCT_ID, \n" +
"               MIN(S.ID) as minimum_id_for_price \n" +
"          FROM PRODUCT_SKU S \n" +
"         WHERE S.PRODUCT_ID IN (:productIds) \n" +
"      GROUP BY S.PRODUCT_ID, S.SALE_PRICE \n" +
"    ) FI ON (FI.PRODUCT_ID = T.PRODUCT_ID AND FI.minimum_id_for_price = T.ID) \n" +
"    JOIN \n" +
"    ( \n" +
"        SELECT S.PRODUCT_ID, \n" +
"               MIN(S.SALE_PRICE) as minimum_price_for_product \n" +
"          FROM PRODUCT_SKU S \n" +
"         WHERE S.PRODUCT_ID IN (:productIds) \n" +
"      GROUP BY S.PRODUCT_ID \n" +
"    ) FP ON (FP.PRODUCT_ID = T.PRODUCT_ID AND FP.minimum_price_for_product = T.sale_price) \n" +
"WHERE T.PRODUCT_ID IN (:productIds)";

这是它在平面 SQL 文件中的样子:

--namedQuery: FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS
FROM PRODUCT_SKU T 
JOIN 
( 
    SELECT S.PRODUCT_ID, 
           MIN(S.ID) as minimum_id_for_price 
      FROM PRODUCT_SKU S 
     WHERE S.PRODUCT_ID IN (:productIds) 
  GROUP BY S.PRODUCT_ID, S.SALE_PRICE 
) FI ON (FI.PRODUCT_ID = T.PRODUCT_ID AND FI.minimum_id_for_price = T.ID) 
JOIN 
( 
    SELECT S.PRODUCT_ID, 
           MIN(S.SALE_PRICE) as minimum_price_for_product 
      FROM PRODUCT_SKU S 
     WHERE S.PRODUCT_ID IN (:productIds) 
  GROUP BY S.PRODUCT_ID 
) FP ON (FP.PRODUCT_ID = T.PRODUCT_ID AND FP.minimum_price_for_product = T.sale_price) 
WHERE T.PRODUCT_ID IN (:productIds)
4

16 回答 16

9

我将 SQL 存储为 Java 类中的字符串和在运行时加载的单独文件。我非常喜欢后者,原因有两个。首先,代码在很大程度上更具可读性。其次,如果将 SQL 存储在单独的文件中,则更容易单独测试 SQL。除此之外,当查询位于单独的文件中时,更容易找到比我更擅长 SQL 的人来帮助我处理查询。

于 2009-05-07T15:24:53.460 回答
5

我也遇到过这个问题,目前是出于同样的原因——一个基于 spring jdbc 的项目。我的经验是,虽然在 sql 本身中包含逻辑并不是很好,但确实没有更好的地方,并且放入应用程序代码比让 db 执行它要慢,而且不一定更清晰。

我见过的最大的陷阱是 sql 开始在整个项目中扩散,有多种变体。“从 FOO 中获取 A、B、C”。“从 Foo 中获取 A、B、C、E”等。当项目达到一定的临界质量时,这种扩散尤其可能 - 10 个查询似乎不是问题,但是当整个项目中有 500 个查询分散时,要弄清楚你是否已经做了一些事情变得更加困难. 抽象出基本的 CRUD 操作让您在游戏中遥遥领先。

最好的解决方案,AFAIK,是与编码的 SQL 严格一致 - 评论,测试,并在一个一致的地方。我们的项目有 50 行未注释的 sql 查询。他们的意思是什么?谁知道?

至于外部文件中的查询,我看不出这有什么好处-您仍然依赖于 SQL,并且除了将 sql 排除在类之外的(有问题的)美学改进之外,您的类是仍然依赖于 sql - 例如,您通常将资源分开以获得插入替换资源的灵活性,但是您不能插入替换 sql 查询,因为这会改变类的语义或不起作用全部。所以这是一种虚幻的代码清洁度。

于 2009-05-07T15:03:17.333 回答
2

一个相当激进的解决方案是使用 Groovy 来指定您的查询。Groovy 具有对多行字符串和字符串插值(有趣地称为 GStrings)的语言级支持。

例如,使用 Groovy,您在上面指定的查询将是:

class Queries
    private static final String PRODUCT_IDS_PARAM = ":productIds"

    public static final String FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS = 
    """    FROM PRODUCT_SKU T 
        JOIN 
        ( 
            SELECT S.PRODUCT_ID, 
                   MIN(S.ID) as minimum_id_for_price 
              FROM PRODUCT_SKU S 
             WHERE S.PRODUCT_ID IN ($PRODUCT_IDS_PARAM) 
          GROUP BY S.PRODUCT_ID, S.SALE_PRICE 
        ) FI ON (FI.PRODUCT_ID = T.PRODUCT_ID AND FI.minimum_id_for_price = T.ID) 
        JOIN 
        ( 
            SELECT S.PRODUCT_ID, 
                   MIN(S.SALE_PRICE) as minimum_price_for_product 
              FROM PRODUCT_SKU S 
             WHERE S.PRODUCT_ID IN ($PRODUCT_IDS_PARAM) 
          GROUP BY S.PRODUCT_ID 
        ) FP ON (FP.PRODUCT_ID = T.PRODUCT_ID AND FP.minimum_price_for_product = T.sale_price) 
    WHERE T.PRODUCT_ID IN ($PRODUCT_IDS_PARAM) """

您可以从 Java 代码访问这个类,就像它是在 Java 中定义的一样,例如

String query = QueryFactory.FIND_ALL_BY_CHEAPEST_AND_PRODUCT_IDS;

我承认,将 Groovy 添加到您的类路径中只是为了让您的 SQL 查询看起来更好,这有点像“大锤破解坚果”的解决方案,但是如果您使用的是 Spring,那么您很有可能已经在您的类路径中使用了 Groovy .

此外,您的项目中可能还有很多其他地方可以使用 Groovy(而不是 Java)来改进您的代码(特别是现在 Groovy 归 Spring 所有)。示例包括编写测试用例,或用 Groovy bean 替换 Java bean。

于 2009-05-07T15:15:55.010 回答
1

我们使用存储过程。这对我们有好处,因为我们使用 Oracle Fine Grain Access。这允许我们通过限制用户访问相关程序来限制用户查看特定报告或搜索结果。它还为我们带来了一点性能提升。

于 2009-05-07T16:04:22.647 回答
0

我想将查询存储在外部文件中,然后让应用程序在需要时读取它会带来巨大的安全漏洞。

如果一个邪恶的策划者可以访问该文件并更改您的查询,会发生什么?

例如更改

 select a from A_TABLE;

 drop table A_TABLE;

或者

 update T_ACCOUNT set amount = 1000000

此外,它还增加了必须维护两件事的复杂性:Java 代码和 SQL 查询文件。

编辑:是的,您可以在不重新编译您的应用程序的情况下更改您的查询。我看不出有什么大不了的。如果项目太大,您可以重新编译仅保存/创建 sqlQueries 的类。此外,如果文档很差,也许您最终会更改不正确的文件,这将变成一个巨大的无声错误。不会抛出异常或错误代码,当您意识到自己做了什么时,可能为时已晚。

另一种方法是使用某种 SQLQueryFactory,有据可查,并使用返回您想要使用的 SQL 查询的方法。

例如

public String findCheapest (String tableName){

      //returns query.
}
于 2009-05-07T14:57:13.587 回答
0

为什么不使用存储过程而不是硬编码查询?存储过程将提高可维护性并为 SQL 插入攻击等事情提供更多安全性。

于 2009-05-07T14:58:08.657 回答
0

在课堂上可能是最好的 - 如果查询足够长以至于转义是一个问题,您可能想要查看存储过程或简化查询。

于 2009-05-07T15:00:16.920 回答
0

一种选择是使用iBatis。与 Hibernate 等成熟的 ORM 相比,它相当轻量级,但提供了一种将 SQL 查询存储在 .java 文件之外的方法

于 2009-05-07T15:03:54.460 回答
0

我们将所有的 SQL 存储在一个类中作为一堆静态最终字符串。为了可读性,我们将它分布在几行使用 + 连接的行上。另外,我不确定你是否需要转义任何东西——“字符串”在 sql 中用单引号括起来。

于 2009-05-07T15:05:47.123 回答
0

我们有一个项目,我们使用了与您完全相同的方法,除了我们将每个查询外部化到一个单独的文本文件。每个文件都使用 Spring 的 ResourceLoader 框架读入(一次),应用程序通过如下接口工作:

public interface SqlResourceLoader {
    String loadSql(String resourcePath);
}

这样做的一个明显优势是,使用非转义格式的 SQL 可以更轻松地进行调试——只需将文件读入查询工具即可。一旦您有多个中等复杂度的查询,在测试和调试(尤其是调整)时处理代码的非/转义/转义是非常宝贵的。

我们还必须支持几个不同的数据库,因此可以更轻松地交换平台。

于 2009-05-07T15:08:58.037 回答
0

根据您解释的问题,这里没有真正的选择,除了将其保留在代码中,并在任何地方摆脱/n字符。这是您提到的唯一影响可读性的事情,它们绝对没有必要。

除非您在代码中遇到其他问题,否则您的问题很容易解决。

于 2009-05-07T15:10:15.180 回答
0

您需要的是SQLJ,它是一个 SQL Java 预处理器。可悲的是,尽管我已经看到了一些IBMOracle的实现,但它显然从未起飞。但它们已经过时了。

如果我是你并且在系统上有很多查询,我会将它们存储在一个单独的文件中并在运行时加载它们。

于 2009-05-07T15:36:53.837 回答
0

根据我的经验,最好将 SQL 语句保留在代码中而不将它们分开,这样可以使事情更易于维护(例如注释与配置文件),但现在我与团队成员就这件事进行了辩论。

于 2009-05-07T17:32:26.163 回答
0

我有一个小程序,可以使用 Java 字符串文字访问剪贴板和转义/取消转义纯文本。

我将它作为“快速启动”工具栏上的快捷方式,所以我唯一需要做的就是

Ctrl+C, Click jar, Ctrl+V

当我想在 SQL 工具中运行我的“代码”时,反之亦然。

所以我通常有这样的事情:

String query = 
    "SELECT a.fieldOne, b.fieldTwo \n"+
    "FROM  TABLE_A a, TABLE b \n"+ 
    "... etc. etc. etc";


logger.info("Executing  " + query  );

PreparedStatement pstmt = connection.prepareStatement( query );
....etc.

变成:

    SELECT a.fieldOne, b.fieldTwo 
    FROM  TABLE_A a, TABLE b
    ... etc. etc. etc

要么是因为在某些项目中我无法创建单独的文件,要么是因为我很偏执,并且我觉得在从外部文件读取时会插入/删除一些位(通常是一个不可见的 \n

select a,b,c 
from 

进入

select a,b,cfrom 

IntelliJ idea 自动为您做同样的事情,但只是从简单到代码。

这是我恢复的旧版本。它有点坏了,不能处理?

让我知道是否有人改进它。

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;

/**
 * Transforms a plain string from the clipboard into a Java 
 * String literal and viceversa.
 * @author <a href="http://stackoverflow.com/users/20654/oscar-reyes">Oscar Reyes</a>
 */
public class ClipUtil{

    public static void main( String [] args ) 
                                throws UnsupportedFlavorException,
                                                     IOException {

        // Get clipboard
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        Clipboard clipboard = toolkit.getSystemClipboard();

        // get current content.
        Transferable transferable = clipboard.getContents( new Object() ); 
        String s = ( String ) transferable.getTransferData( 
                                                DataFlavor.stringFlavor );

        // process the content
        String result = process( s );

        // set the result
        StringSelection ss = new StringSelection( result );
        clipboard.setContents( ss, ss );

    }
    /**
     * Transforms the given string into a Java string literal 
     * if it represents plain text and viceversa. 
     */
    private static String process( String  s ){
        if( s.matches( "(?s)^\\s*\\\".*\\\"\\s*;$" ) ) {
            return    s.replaceAll("\\\\n\\\"\\s*[+]\n\\s*\\\"","\n")
                       .replaceAll("^\\s*\\\"","")
                       .replaceAll("\\\"\\s*;$","");
        }else{
            return     s.replaceAll("\n","\\\\n\\\" +\n \\\" ")
                        .replaceAll("^"," \\\"")
                        .replaceAll("$"," \\\";");
        }
    }
}
于 2009-05-08T20:42:32.383 回答
0

既然您已经使用 Spring,为什么不将 SQL 放在 Spring 配置文件中并将其 DI 到 DAO 类中呢?这是一种将 SQL 字符串外部化的简单方法。

HTH 汤姆

于 2009-05-14T16:08:06.903 回答
0

我更喜欢外部选项。我支持多个项目,但发现支持内部 SQL 要困难得多,因为每次要对 SQL 进行细微更改时都必须编译和重新部署。将 SQL 放在外部文件中可以让您轻松地进行大大小小的更改,同时降低风险。如果您只是在编辑 SQL,则不可能输入会破坏课程的错字。

对于 Java,我使用在 .properties 文件中表示 SQL 的 Properties 类,如果您想重复使用查询而不是多次读取文件,它允许您传递 SQL 查询。

于 2009-09-28T15:29:32.337 回答