问题 Java类中的SQL代码


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

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

有谁遇到过类似的问题?什么是良好的平衡?您在哪里保留自定义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)

9299
2018-05-07 14:48


起源

你需要那些新线路吗?我倾向于把+放在线的开头,所以它不那么凌乱。 - Tom Hawtin - tackline
对可读性的一个小改进是在每行末尾放弃'\ n'并用空格替换它 - Dónal
与Tom H.关于换行的问题。 - Tom
当发生错误并打印SQL语句时,换行符对堆栈跟踪的可读性有很大帮助 - Gennadiy
我宁愿拥有可读代码(即Java方法中的SQL,甚至不是文件顶部的const),而不是偶尔使用好的堆栈跟踪。阅读代码是程序员大多数时间都在做的事情,因此让SQL接近相关代码,在那里你知道参数是什么以及方法名称是什么,使它更具可读性。基于句法类型对代码元素进行分组对我来说似乎很奇怪。 - Jan Soltis


答案:


我将SQL作为两个字符串存储在Java类中,并作为在运行时加载的单独文件存储。我非常喜欢后者有两个原因。首先,代码更具可读性。其次,如果将SQL存储在单独的文件中,则可以更容易地单独测试SQL。除此之外,在SQL中使用比我更好的人来帮助我查询它们在单独的文件中时更容易。


8
2018-05-07 15:24



另一个优点是,如果SQL是外部的,则不必重新构建和重新部署应用程序。 - Jason Day
我们将把我们的SQL语句外部化为一个可在运行时重新加载的平面文件。所有查询将在每个查询的开头一个接一个地列出,并带有包含“--namedQuery [QUERY_NAME]”的注释。将加载和缓存查询。每5-10分钟,加载程序将异步检查是否有更新的文件并加载它。感谢所有的建议。 - Gennadiy


我也遇到过这种情况,目前出于同样的原因 - 一个基于spring jdbc的项目。我的经验是,虽然在sql本身中拥有逻辑并不是很好,但实际上没有更好的地方,并且放入应用程序代码比使用db执行它并且不一定更清楚。

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

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

对于外部文件中的查询,我看不出这些是什么 - 你仍然只是依赖于SQL,除了保持sql不在类中的(可疑的)审美改进之外,你的类是仍然只是依赖于sql -.eg你通常将资源分开以获得插件替换资源的灵活性,但你无法插入替换的sql查询,因为它会改变类的语义或不工作所有。所以这是一个虚幻的代码清洁。


5
2018-05-07 15:03





一个相当激进的解决方案是使用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。


2
2018-05-07 15:15



我们已经使用groovy来满足各种脚本需求。下一个项目将从头开始构建在groovy中,允许我们将SQL作为块引用文本存储在服务类中。我只是希望java有blockquotes,它会解决我们的问题。 :( - Gennadiy
.NET也有这种类型的多行引用...这是我在短暂的C#gig之后回到Java时我真正错过的事情之一 - Jeff Olson


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


1
2018-05-07 16:04





为什么不使用存储过程而不是硬编码查询?存储过程将提高可维护性,并为SQL Interjection Attacks等事件提供更高的安全性。


0
2018-05-07 14:58



某些平台不支持存储过程。并且,存储过程中没有任何内在的东西可以提供更多的安全性来防止sql注入。 - Edward Q. Bridges
我们已经受到保护免受注入攻击,因为所有查询都通过spring的SimpleJdbc框架并使用主机绑定变量。 - Gennadiy
存储过程会给你一种方法来封装所有的sql代码,然后你只需要担心传递参数,所以你会在这里放弃一个大脑袋。至于存储过程更安全,它们会强制您使用参数而不仅仅是字符串(尽管您也可以在字符串中使用params ......但您不必这样做)。所以在这个意义上他们更安全。此外,他们还减少了从Web服务器到数据库服务器的带宽。试想一下5000 char查询被反复发送到SQL db而不是100 char存储过程。 - infocyde
存储过程也存在许多缺点,例如它们往往不能在RDBMS之间移植,如果代码中的“一切”,它们会使部署变得更加复杂。 - Dónal
你有多少次在RDBMS上移植存储过程?我已经在IT工作了大约12年,而且我从来没有这样做过一次......就部署更复杂而言,在企业界,登录SQL服务器并更新存储更加容易proc比搭建一个新版本。 - infocyde


在类中可能是最好的 - 如果查询足够长以至于转义是一个问题,您可能希望查看存储过程或简化查询。


0
2018-05-07 15:00





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


0
2018-05-07 15:03



ibatis不是一个选项,因为我们只想存储SQL并使用spring的简单jdbc实用程序将它们映射到对象。 - Gennadiy


我们将所有SQL存储在一个类中作为一组静态最终字符串。为了便于阅读,我们将其分布在使用+连接的几行中。另外,我不确定你是否需要逃避任何事情 - “字符串”用sql中的单引号括起来。


0
2018-05-07 15:05





我们有一个项目,我们使用您的确切方法,除了我们将每个查询外部化为单独的文本文件。使用Spring的ResourceLoader框架读取每个文件(一次),应用程序通过如下界面工作:

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

这方面的一个明显优势是,以非转义格式使SQL允许更容易的调试 - 只需将文件读入查询工具即可。一旦你有多个中等复杂度的查询,在测试和调试(特别是调优)时处理代码中的un / escaping是非常宝贵的。

我们还必须支持几个不同的数据库,因此它允许更容易地交换平台。


0
2018-05-07 15:08