我们当前的项目不使用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)
我将SQL作为两个字符串存储在Java类中,并作为在运行时加载的单独文件存储。我非常喜欢后者有两个原因。首先,代码更具可读性。其次,如果将SQL存储在单独的文件中,则可以更容易地单独测试SQL。除此之外,在SQL中使用比我更好的人来帮助我查询它们在单独的文件中时更容易。
我也遇到过这种情况,目前出于同样的原因 - 一个基于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查询,因为它会改变类的语义或不工作所有。所以这是一个虚幻的代码清洁。
一个相当激进的解决方案是使用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。
我们使用存储过程。这对我们有好处,因为我们使用Oracle Fine Grain Access。这允许我们通过限制用户对相关过程的访问来限制用户查看特定报告或搜索结果。它还为我们提供了一点性能提升。
为什么不使用存储过程而不是硬编码查询?存储过程将提高可维护性,并为SQL Interjection Attacks等事件提供更高的安全性。
在类中可能是最好的 - 如果查询足够长以至于转义是一个问题,您可能希望查看存储过程或简化查询。
一种选择是使用 iBatis的。与完全成熟的ORM(如Hibernate)相比,它相当轻量级,但它提供了一种在.java文件之外存储SQL查询的方法
我们将所有SQL存储在一个类中作为一组静态最终字符串。为了便于阅读,我们将其分布在使用+连接的几行中。另外,我不确定你是否需要逃避任何事情 - “字符串”用sql中的单引号括起来。
我们有一个项目,我们使用您的确切方法,除了我们将每个查询外部化为单独的文本文件。使用Spring的ResourceLoader框架读取每个文件(一次),应用程序通过如下界面工作:
public interface SqlResourceLoader {
String loadSql(String resourcePath);
}
这方面的一个明显优势是,以非转义格式使SQL允许更容易的调试 - 只需将文件读入查询工具即可。一旦你有多个中等复杂度的查询,在测试和调试(特别是调优)时处理代码中的un / escaping是非常宝贵的。
我们还必须支持几个不同的数据库,因此它允许更容易地交换平台。