Java和SQL:返回null还是抛出异常?

4
这是另一个备受争议的话题,但这次我只寻找简单且有文献支持的答案。情境如下:
 public static Hashtable<Long, Dog> getSomeDogs(String colName, String colValue) {
  Hashtable<Long, Dog> result = new Hashtable<Long, Dog>();
  StringBuffer sql = null;
  Dog dog = null;
  ResultSet rs = null;
      try {
          sql = new StringBuffer();
          sql.append("SELECT * FROM ").append("dogs_table");
          sql.append(" WHERE ").append(colName).append("='");
          sql.append(colValue).append("'");
          rs = executeQuery(sql.toString());
              while (rs.next()) {
                  dog= new Dog();
                  //...initialize the dog from the current resultSet row
              result.put(new Long(dog.getId()), dog);
              }
          }
     catch (Exception e) {
         createErrorMsg(e);
         result = null; //i wonder....
         }
     finally {
         closeResultSet(rs); //this method tests for null rs and other stuff when closing the rs.
     }
   return result;
 }

问题:

1. 你建议改进这种具有某些属性的狗返回技术的方法有哪些?

2. 对于空ResultSet,rs.next()将返回false,或者会生成一个异常,例如:

String str = null;
System.out.println(str.toString());

3. 如果在从ResultSet的当前行初始化狗对象时出现问题,例如:连接失败,向狗属性setter传递了不兼容的值等,则会发生什么情况?现在我可能已经在哈希表中拥有10个元素,也可能没有(第一行)。下一步操作将是什么:a)返回null哈希表;b)以目前的状态返回结果哈希表;c)抛出异常:此处的异常类型将是什么?

4. 我认为您都会同意:没有发生任何不良事件,质询上没有行,将返回空值。但是,@Thorbjørn Ravn Andersen在这里说,我应该返回NullObject而不是null值。我想知道那是什么。

5. 我注意到人们和人群说应将应用程序分成层或某种级别。考虑上面的例子,除了我能想到的这些层之外,还有哪些层:

Layer1 :: 数据库层,执行操作:此方法。

Layer2 :: ??? :某个层,在此层中构造新的Dog对象:我的Dog对象。

Layer3 :: ? :我打算对狗的集合做些什么:大多是GUI层,或用户界面的子层。

根据应用程序流程,如果在第1层中发生异常,最好处理它? 我的想法:捕获异常,记录异常,返回一些值。这是最佳实践吗?

谢谢你的回答,期待看到其他人对这些问题的看法。


请使用4个空格缩进代码。 - Dark Falcon
我对缩进并不在意,但是通过代码格式化器实际格式化它似乎越来越少了 :-( - Brian Agnew
非常抱歉提问的问题,但是我该怎么做呢?我已经按了 n 次“代码”按钮,但这些代码行“String str = null; System.out.println(str.toString());”没有任何反应 :-(。 - hypercube
那不是问题,但我想知道格式是否会因为额外的HTML元素(例如<p/>)而混乱。 - Brian Agnew
高亮显示:选择您想要格式化为代码的行,然后按下代码按钮。 - Jorn
哦,但是我已经按了几十次按钮 :-)。这一定是一个 bug :-D,与基本的 HTML 插入有关。 - hypercube
6个回答

8

我会避免以下内容

   sql.append("SELECT * FROM ").append("dogs_table");
   sql.append(" WHERE ").append(colName).append("='");
                        sql.append(colValue).append("'");

而是使用PreparedStatement及其相关的参数设置方法(setString())等。这将防止colValue的值带有引号,以及SQL注入攻击(或更一般地,colValue形成某些SQL语法)。
如果集合仅为空,我绝不会返回null。从客户端的角度来看,这似乎非常违反直觉,完全出乎意料。
在错误情况下,我不建议返回null,因为您的客户端必须明确检查这一点(并且可能会忘记)。如果需要,我会返回一个空集合(这可能类似于您关于null对象的评论),或者更可能抛出异常(根据情况和严重程度)。异常很有用,因为它将携带与遇到的错误相关的一些信息。Null什么也没说。
如果在构建 Dog 对象时遇到问题,您应该怎么办?我认为这取决于您希望应用程序具有多么强大和有弹性。返回 Dog 的子集是否是问题,还是完全灾难性的,需要报告此问题?这是应用程序要求的(我过去必须满足其中任何一种情况 - 最佳尝试或全部或无)。
几点观察。我会使用 HashMap 而不是旧的 Hashtable(对所有访问进行同步,并且更重要的是,不是真正的 Collection - 如果您有一个 Collection,则可以将其传递给期望任何 Collection 的其他方法),以及出于类似原因选择 StringBuilder 而非 StringBuffer。这不是一个主要问题,但值得知道。

1
+1推荐使用PreparedStatementHashMap而不是Hashtable。在现代代码中,没有使用Hashtable的好理由。并且,任何时候当您手动构建SQL字符串时,都会为所有SQL注入漏洞设置自己的陷阱。 - Daniel Pryden
在我看来,在这里抛出异常对我的代码没有任何价值,因为它需要在更高的级别上被捕获(我们可能会忘记这样做),并且不会告诉我更多信息,因为我已经记录了错误详细信息。在这里是否应该提示用户发生了错误? - hypercube
你能否将那段StringBuffer创建的代码转换成PreparedStatement版本,以便清晰地标记出它们之间的区别?另外,如果创建者足够注意到它,那么使用StringBuffer构建查询会有什么问题吗? - hypercube
我不太清楚HashMap..我已经谷歌过了,发现了这个链接:http://www.coderanch.com/t/202040/Performance/java/Hashtable-vs-HashMap。轻微的性能提升并不能弥补失去线程安全属性的代价。 - hypercube
是的,连接池是更合适的解决方案。然而,仅仅为了使用新的PreparedStatement而实现连接池可能有点过头了。如果我有一个名为con的静态Connection对象,并且每次调用con.createPreparedStatement(someQuery)就足够了吗?在这里我不太关心SQL注入问题,因为我不会直接从解析用户输入创建查询。 - hypercube
显示剩余6条评论

6

您有五个问题

1. 您建议改进返回带有某些属性的一些狗的技术的方法有哪些?

实际上有几种。

  • 您的方法是静态的 - 这并不可怕,但会导致您使用另一个静态的“executeQuery”,这让我想到了 Singleton…
  • 类“Dogs”违反了面向对象的命名惯例 - 复数名词不是好的类名,除非类的一个实例保存了一组东西 - 而且看起来 Dogs 实际上是“Dog”。
  • HashTable已经过时。HashMap或ConcurrentHashMap可以提供更好的性能。
  • 我看不出为什么要使用多个 append 创建查询的第一部分 - 它不错,但比它可能更难读取,因此如果您只打算硬编码所选列(*)和表名(dogs_table),则 sql.append("SELECT * FROM dogs_table WHERE "); 是一个更合理的开头。

2. rs.next() 对于空 ResultSet 将返回 false,或者生成异常

这似乎不是一个问题,但是是的,rs.next() 一旦没有任何行可以处理,就会返回 false。

3. 如果在从 ResultSet 的当前行初始化 dog 对象时发生了什么不好的事情

如果“发生了什么不好的事情”,那么您下一步要做的就取决于您的设计。有宽容的方法(返回您可以的所有行),以及无情的方法(抛出异常)。我倾向于采用“无情”的方法,因为使用“宽容”的方法,用户不会知道您没有返回所有存在的行 - 只有在错误之前获取的所有行。但是可能有宽容的情况。

4. 我认为大家都会同意这一点:没有发生任何不好的事情,在询问中没有行,将返回 null 值。

这不是一个明显的正确答案。首先,这不是现有方法中正在发生的事情。它将返回一个空的 HashTable(这是“null对象”的含义)。其次,在“未找到结果”情况下,null 不总是答案。

我看过 null,但我也看过一个空的结果变量。我声称它们都是正确的方法,但我更喜欢空的结果变量。但是,始终最好保持一致,选择一种返回“无结果”的方法并坚持使用。

5. 我注意到人们和一些组织说应该将应用程序分成层或某种级别。

如果没有看到您的应用程序的其余部分,这比其他问题更难回答。


谢谢您提供的有关问题的上下文和简明扼要的答案。您是正确的。我有5个问题(我原本只想到4个,但实际上我又添加了一个 :-))。我正在使用单例管理器来访问数据库(这样做是否不好?)。如果建议的类型表现更好,我会采用它们。这可能听起来很愚蠢,但我对这里的一些事情并不知道...如果我知道,我就不会询问信息了。再次感谢。我将使用:HashMap、StringBuilder(在需要时)、PreparedStatement而不是执行字符串、NullObjects而不是null值以及Dog而不是Dogs。这是非常有价值的信息。 - hypercube
更新。我检查了与数据库相关的项目对象,它们全部都被命名为“Dog”而不是“Dogs”。事实上,我有点不太了解惯例,但某种程度上它似乎更合适。又是黑暗中的一束光明 :-)。 - hypercube
不用客气。是的,单例模式……嗯,我不会说它不好,但这是一个值得怀疑的做法。每当我在设计中看到一个单例模式时,我都会想知道它是否真的需要。在谷歌上搜索一下,你就会明白我的意思。 - CPerkins
一些人在这里进行了一些测试:http://forums.sun.com/thread.jspa?trange=15&threadID=536477&forumID=24&tstart=0 - hypercube
谢谢您耐心对待这个固执的驴子 :-). 我会再考虑一下。 - hypercube
显示剩余3条评论

4

Null Object Pattern是一种设计模式,它总是返回一个对象来避免在代码中出现NPE:s和任何空值检查。在您的情况下,这意味着不要返回null,而是返回一个空的Hashtable<Long, Dogs>

推理是,由于它是一个集合,并且您的其他代码将按此方式访问它,因此如果返回空集合,则不会崩溃;它不会被迭代,不会包含任何令人惊讶的内容,也不会引发NPE:s等问题。

确切地说,Null Object是一个特殊的类/接口实现,它什么也不做,因此没有任何副作用。由于其不为null使用它将使您的代码更清洁,因为当您知道您始终会从方法调用中获得对象时,无论发生了什么,您甚至都不必检查null或编写反应对它们的代码!因为Null Object什么也不做,所以您甚至可以将它们作为单例放在那里,从而通过这样保存内存。


没错,这将是一个长度为2的字符串数组的Null对象版本。现在,您可以像这样简单明了地编写代码:if(arr[i].equals("val")){},而不必再写if(arr[i] != null && arr[i].equals("val")){},这样更加简洁明了。 - Esko
明白了。如果我的对象X是手工制作的,有Y个字段和Z个方法怎么办?我必须编写一个名为NullX的类,其中包含Y个“默认”或“null”值和Z个空方法,以替换原始对象?这也意味着必须创建一个接口。这不是太麻烦了吗?使用具有默认值的new X()不能做到同样的事情(除了每个实例消耗的额外内存之外)。或者,我可以将此对象(new X - 我的null对象)作为静态变量放在某个地方,在所有情况下都调用它? - hypercube
有点混乱,但据我所理解,那应该是单例模式。考虑到POJO和bean中的Null Object模式,是的,这意味着您也必须对字段等实现Null Objects,但通常用""替换null字符串和-1或0替换数字就足够了。 - Esko
我已经阅读了关于NullObjects可能的两种主要实现方式:1.使用接口,2. NullObject将扩展RealObject,因此必须实现其方法。对于这个模式存在严重的疑虑<a href ="http://blog.bielu.com/2008/12/null-object-design-pattern.html">在这里</a>。我希望我的评论链接能按预期工作... - hypercube
如果我添加一个名为getEmptyObject的静态方法,它只是返回我的类型的新对象,这不就足够了吗?默认(null值)初始化将在构造函数中完成。 - hypercube
显示剩余2条评论

3

不要通过字符串拼接来构建SQL查询,就像你现在正在做的一样:

sql = new StringBuffer();
sql.append("SELECT * FROM ").append("dogs_table");
sql.append(" WHERE ").append(colName).append("='");
sql.append(colValue).append("'");

这会让你的代码易受一种广为人知的安全攻击,SQL注入。不要这样做,而是使用PreparedStatement并通过调用它上面相应的set...()方法来设置参数。请注意,你只能用它来设置列的值,不能像你现在做的那样动态构建列名。例如:
PreparedStatement ps = connection.prepareStatement("SELECT * FROM dogs_table WHERE MYCOL=?");
ps.setString(1, colValue);

rs = ps.executeQuery();

如果您使用 PreparedStatement,JDBC 驱动程序将自动处理转义可能存在于 colValue 中的某些字符,以防止 SQL 注入攻击。

说得好。谢谢。看到更改的原因后,将会进行更改。 - hypercube

2
如果出现错误,请抛出异常。如果没有数据,则返回一个空集合,而不是null。(此外,通常应返回更通用的“Map”,而不是特定的实现)。

不错。我错过了显式类型的返回。+1。 - CPerkins

0

使用Spring-JDBC而不是普通的JDBC,可以显著减少样板式JDBC代码的数量。下面是同一个方法使用Spring-JDBC重写后的代码:

public static Hashtable<Long, Dogs> getSomeDogs(String colName, String colValue) {

    StringBuffer sql = new StringBuffer();
    sql.append("SELECT * FROM ").append("dogs_table");
    sql.append(" WHERE ").append(colName).append("='");
    sql.append(colValue).append("'");

    Hashtable<Long, Dogs> result = new Hashtable<Long, Dogs>();

    RowMapper mapper = new RowMapper() {

        public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
            Dogs dog = new Dogs();
            //...initialize the dog from the current resultSet row
            result.put(new Long(dog.getId()), dog);
        }
    };
    (Hashtable<Long, Dogs>) jdbcTemplate.queryForObject(sql, mapper);
}

Spring 负责以下事项:

  1. 迭代 ResultSet
  2. 关闭 ResultSet
  3. 一致地处理异常

正如其他人所提到的,您真的应该使用 PreparedStatement 来构建 SQL,而不是使用 String(或 StringBuffer)。如果由于某种原因您无法这样做,您可以通过以下方式构建 SQL 以提高查询的可读性:

    String sql = 
        "SELECT * FROM dogs_table " +
        "WHERE " + "colName" + " = '" + colValue + "'";

虽然这种方式看起来更易读,但如果像我的例子一样使用“手动”查询构建器,我发现它并不是。使用“已弃用”的StringBuilder的.append()方法,将查询分隔成更明显的标记以进行构建。所有这些现在都没有太大意义,与人们建议始终根据自己的风格或推荐的风格进行更改相比。然而,一个开发者不能采用X种不同回答者的风格。例如,异常的已检查或未检查使用问题到目前为止还没有答案...等等... - hypercube
有些人甚至更在意问题的格式,而不是问题本身的实质。我尽力让它看起来好一点,这就是它被发布出去的样子。我提出了4个不同的问题,得到了一些模糊的答案...很抱歉,但除了使用像StringBuilder或HashMap这样的新Java类型之外,我无法从迄今为止的答案中提取任何有用的东西 :-(。是的,也许SpringJDBC是一个很棒的框架,但我并没有要求它。这只是我的项目的另一个依赖项...我不确定我是否想要承诺它。 - hypercube
关于SpringJDBC的优点:1.我不必自己迭代结果集; 2.我有一个安全关闭resultSet的静态方法; 3.我已经使用apache.commons.lang来处理异常,例如获取rootStackTrace()而不是一些嵌套的nasty stacktraces,那么还有什么优势呢? - hypercube

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接