题
我们当前的项目不使用Hibernate(出于各种原因),我们使用Spring的SimpleJdbc支持来执行所有数据库操作。我们有一个实用程序类,它抽象所有CRUD操作,但使用自定义SQL查询执行复杂操作。
目前,我们的查询作为字符串常量存储在服务类本身中,并被提供给由SimpleJdbcTemplate执行的实用程序。我们陷入僵局,可读性必须与可维护性相平衡。类本身内部的SQL代码更易于维护,因为它驻留在使用它的代码中。另一方面,如果我们将这些查询存储在外部文件(平面或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;
我承认,为了让你的SQL查询看起来更好而将Groovy添加到你的类路径中是一个“破解坚果”的“大锤”。解决方案,但是如果你使用的是Spring,你很可能已经在你的类路径上使用了Groovy。
此外,您的项目中可能还有很多其他地方可以使用Groovy(而不是Java)来改进代码(特别是现在Groovy归Spring所有)。示例包括编写测试用例或使用Groovy bean替换Java bean。
我们使用存储过程。这对我们有好处,因为我们使用Oracle Fine Grain Access。这允许我们通过限制用户对相关过程的访问来限制用户查看特定报告或搜索结果。它还为我们提供了一点性能提升。
为什么不使用存储过程而不是硬编码查询? Stored Procs将提高可维护性,并为SQL Interjection Attacks等提供更高的安全性。
在类中可能是最好的 - 如果查询足够长以至于转义是一个问题,您可能希望查看存储过程或简化查询。
一种选择是使用 iBatis 。与完全成熟的ORM(如Hibernate)相比,它相当轻量级,但提供了一种在.java文件之外存储SQL查询的方法
我们将所有SQL存储在一个类中作为一组静态最终字符串。为了便于阅读,我们将其分布在使用+连接的几行中。此外,我不确定你是否需要逃避任何事情 - “字符串”用sql中的单引号括起来。
我们有一个项目,我们使用您的确切方法,除了我们将每个查询外部化为单独的文本文件。每个文件都使用Spring的ResourceLoader框架读入(一次),应用程序通过以下接口工作:
public interface SqlResourceLoader {
String loadSql(String resourcePath);
}
这方面的一个明显优势是,使用非转义格式的SQL可以更容易地进行调试 - 只需将文件读入查询工具即可。一旦你有一些中等复杂度的查询,在测试和处理时处理代码中的un / escaping。调试(特别是调整)这是非常宝贵的。
我们还必须支持几个不同的数据库,因此它允许更容易地交换平台。
根据您解释的问题,除了将其保留在代码中,并且在任何地方删除 / n 字符之外,没有真正的选择。这是你提到的唯一影响可读性的东西,它们绝对没必要。
除非您在代码中遇到其他问题,否则您的问题很容易解决。
我认为将查询存储在外部文件中,然后让应用程序在需要时读取它会带来巨大的安全漏洞。
如果邪恶的策划者有权访问该文件并更改您的查询,会发生什么?
例如更改
select a from A_TABLE;
要
drop table A_TABLE;
OR
update T_ACCOUNT set amount = 1000000
另外,它增加了必须保留两件事的复杂性:Java代码和SQL查询文件。
编辑:是的,您可以在不重新编译应用的情况下更改查询。我没有看到那里的重大事件。如果项目太大,您可以重新编译仅保存/创建sqlQueries的类。此外,如果文档很差,也许你最终会更改不正确的文件,这将变成一个巨大的无声错误。不会抛出任何异常或错误代码,当您意识到自己已经完成了什么时,可能为时已晚。
另一种方法是使用某种SQLQueryFactory,记录良好,以及返回要使用的SQL查询的方法。
例如
public String findCheapest (String tableName){
//returns query.
}
根据我的经验,最好将SQL语句保留在代码中而不将它们分开,这样可以使事情更易于维护(比如注释与配置文件),但现在我与团队成员就此进行了辩论。
我有一个小程序,可以使用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理念为您自动执行相同操作,但从简单到代码。
这是我恢复的旧版本。它有点破碎,无法处理?
如果有人改进了,请告诉我。
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*;<*>quot; ) ) {
return s.replaceAll("\\\\n\\\"\\s*[+]\n\\s*\\\"","\n")
.replaceAll("^\\s*\\\"","")
.replaceAll("\\\"\\s*;<*>quot;,"");
}else{
return s.replaceAll("\n","\\\\n\\\" +\n \\\" ")
.replaceAll("^"," \\\"")
.replaceAll("<*>quot;," \\\";");
}
}
}
既然你已经使用了Spring,为什么不把SQL放在Spring配置文件中并把它放到DAO类中呢?这是一种外部化SQL字符串的简单方法。
HTH 汤姆
我更喜欢外部选项。我支持多个项目,并且发现支持内部SQL要困难得多,因为每次要对SQL进行轻微更改时都必须编译和重新部署。将SQL放在外部文件中可以轻松地以较小的风险进行大小变化。如果您只是编辑SQL,则无法输入打破课程的拼写错误。
对于Java,我使用Properties类来表示.properties文件中的SQL,如果要重新使用查询而不是多次读取文件,则允许您传递SQL查询。