0

It seems that standard check of ResultSet emptiness will not work in Spring's JDBC ResultSetExtractor. I use it in following way:

    return jdbcTemplate.query(sql, ps -> {
        ps.setLong(1, fooParam1);
        ps.setLong(2, fooParam2);
    }, rs -> {
        return rs.getLong(1);
    });

So what is the correct way check ResultSet emptiness in such pattern?

Andremoniy
  • 34,031
  • 20
  • 135
  • 241
  • Why don't you use rs.hasNext()? – Radhakrishna Sharma Gorenta May 25 '16 at 12:26
  • @RadhakrishnaSharmaGorenta there isn't such method in `ResultSet` class – Andremoniy May 25 '16 at 14:45
  • Of course, it is possible that your first parameter `sql` is a `PreparedStatementCreator` implementation that does already invoke `next()`, but you can’t blame me, when you are using something contradicting to the API without posting the code in your question. You can try your setup with one of the `query` overloads that return a `List`. Is the first row included or missing? As already proven, Springs own `ResultSetExtractor` implementations *do* call `next()`. – Holger May 26 '16 at 09:03
  • You wording suggests that the “synthetic” nature of the `ResultSet` makes a difference. That would be a quite dangerous setup. As already emphasized multiple times, [Springs own `ResultSetExtractor` implementations](http://grepcode.com/file/repository.springsource.com/org.springframework/org.springframework.jdbc/3.2.5/org/springframework/jdbc/core/RowMapperResultSetExtractor.java#88) will invoke `next()` and if you managed to get an environment, where that doesn’t work correctly, you should rethink that. You solution may work in that setup, but that’s not helpful to other users then. – Holger May 26 '16 at 09:19

1 Answers1

6

There seems to be some confusion about the roles of the interfaces, I saw some tutorials doing it wrong. The interfaces RowMapper and RowCallbackHandler are responsible for providing a value, resp. producing a side effect, per row.

Therefore, they are not responsible for invoking next() as the framework will take care of advancing to the next row and not invoking the callback, once next() returned false which may happen even when advancing to the first row, in which case the ResultSet is empty. In the latter case, query(… RowMapper<T>) will return an empty List without invoking the RowMapper and query(… RowCallbackHandler) will simply do nothing.

In contrast, ResultSetExtractor is responsible for processing the entire ResultSet, which includes invoking next() appropriately. So your question’s premise (“… each call of extractData gives me new row.”) is wrong. The standard behavior of all JDBC operations is to return a ResultSet being before the first row so that the first next() invocation will either move the current row pointer to the first row or return false if the ResultSet is empty. So if you expect at most one row, your query should rather look like

return jdbcTemplate.query(sql, ps -> {
        ps.setLong(1, fooParam1);
        ps.setLong(2, fooParam2);
   }, rs -> rs.next()? rs.getLong(1): null);

EDIT: This solution tested and works correctly on standard MySQL and Postgresql instances /Andremoniy/.

EDIT-2: This solution absolutely correct and works correctly anywhere. But it should be noticed that there are two different methods in JdbcTemplate class:

  1. public <T> T query(String sql, PreparedStatementSetter pss, ResultSetExtractor<T> rse) - it is used in this example and it returns 0 for rs.getRows() always, indeed. Provided solution is correct for this case.

  2. public void query(String sql, PreparedStatementSetter pss, RowCallbackHandler rch) which has another type of 3rd parameter, and similar code:

    jdbcTemplate.query(sql, ps -> { ps.setLong(1, fooParam1); ps.setLong(2, fooParam2); }, rs -> { System.out.println(rs.getRow()); });

will work differently: rs.getRow() will return number of processing row, and if no rows exist, processRow method will be never invoked (this second method caused my confusing). /Andremoniy/

Andremoniy
  • 34,031
  • 20
  • 135
  • 241
Holger
  • 285,553
  • 42
  • 434
  • 765
  • excuse me, but I can not agree with you. `ResultSetExractor` gives me `extractData` each time new `ResultSet` object, so it calls `next` somewhere internally. Furthermore, if you call `next()` inside `extractData()` several times, you will move advance and will broke processing of entire queue result. In respect to your rating I will not downvote your answer, but it looks like incorrect, sorry – Andremoniy May 25 '16 at 17:27
  • 2
    Of course it provides you a **new** `ResultSet` each time. How does that prove that `next()` has been called on that `ResultSet`? It actually proves what I said, this callback is responsible for *processing the entire result set* as next time it gets called, it will receive a new one. – Holger May 25 '16 at 17:33
  • 2
    Apparently you are confusing *rows* and `RowSet`. A `RowSet` can have an arbitrary number of rows and while a `RowMapper` is invoked *once for each row*, in other words, gets a new **row** on each call, a `ResultSetExtractor` is not. The `ResultSetExtractor` is invoked *once per result*, represented by a fresh `ResultSet`. Since each call represents an entirely new query, it doesn’t matter how often (or whether) you invoked `next()` on the previous query’s `ResultSet` (which has been closed in the meanwhile). – Holger May 25 '16 at 19:37
  • @Andremoniy: then *why* do you think you can not invoke `next()` on the `ResultSet`? – Holger May 26 '16 at 07:52
  • *Why* I can not invoke `rs.getRow()` in same case? – Andremoniy May 26 '16 at 08:05
  • Because the initial state of the `ResultSet` is *always* before the first row. You will always get `0`, unless you invoke `next()` at least once. Did you look at [the code, I already linked](http://grepcode.com/file/repository.springsource.com/org.springframework/org.springframework.jdbc/3.2.5/org/springframework/jdbc/core/RowMapperResultSetExtractor.java#88)? Notice how `next()` is invoked *before* the first row is read. Besides that, `getRow` is not supported by all `ResultSet`s. – Holger May 26 '16 at 08:12
  • 1
    @Andremoniy And have you read the JDBC api doc for [`ResultSet.getRow()`](http://docs.oracle.com/javase/8/docs/api/java/sql/ResultSet.html#getRow--) _" Note:Support for the getRow method is optional for ResultSets with a result set type of TYPE_FORWARD_ONLY"_ and _"SQLFeatureNotSupportedException - if the JDBC driver does not support this method"_ – Mark Rotteveel May 26 '16 at 08:23
  • @MarkRotteveel And what? Have I use this type? Does `JdbcTemplate` use it in `query` method? What's problem? – Andremoniy May 26 '16 at 08:32
  • @Andremoniy: that’s the whole point, “Does `JdbcTemplate` use it in `query` method?” You don’t know, as `JdbcTemplate` does not fix the type of the result set, but abstracts the entire thing. So it *could* use it, depending on the actual `PreparedStatementCreator` in use. But you are still ignoring the point that the current row is zero anyway, unless you invoke `next()` at least once. – Holger May 26 '16 at 08:37
  • I’m not going to install MySQL for that. The [source code](http://grepcode.com/file/repository.springsource.com/org.springframework/org.springframework.jdbc/3.2.5/org/springframework/jdbc/core/JdbcTemplate.java#639) consists of 20 lines and it’s easy to see that there is no one calling `next()` before the `ResultSetExtractor` receives the `ResultSet`. Unless you have modified the environment in a way, not specified in your question. – Holger May 26 '16 at 08:56
  • @Andremoniy I'm not sure where your disagreement with this answer is coming from. Holger is correct in saying that a `ResultSetExtractor` should process the _whole_ ResultSet, which means calling `next()` itself to loop over the results. If you compare with `query(..., ..., RowCallbackHandler)`, you'll notice that [it creates](http://grepcode.com/file/repo1.maven.org/maven2/org.springframework/spring-jdbc/4.2.0.RELEASE/org/springframework/jdbc/core/JdbcTemplate.java/#736) a `RowCallbackHandlerResultSetExtractor` which loops over the results with `while (rs.next()) {`. – Tunaki May 26 '16 at 18:24
  • @Andremoniy This is link to [the soure code of that while loop](http://grepcode.com/file/repo1.maven.org/maven2/org.springframework/spring-jdbc/4.2.0.RELEASE/org/springframework/jdbc/core/JdbcTemplate.java#1565). This is expected since a `RowCallbackHandler` is called for each row of the results (i.e. Spring handle the calls to `next()` in this case and invokes your `processRow` on each row). Contrary to a `ResultSetExtractor`, which is supposed to extract all of the results itself. – Tunaki May 26 '16 at 18:29
  • @Tunaki, I've made check of this solution. Incorrectness caused by the fact, that invocation of `rs.next()` leads to moving to next row, while current row will be skipped. So this is just incorrect usage of `rs.next()` for `ResultSetExtractor`. – Andremoniy May 26 '16 at 20:41
  • @Holger I have to ask you excuse me. It appears to be bug in `mysql` instance, because on `postgresql` and standard `mysql` instances it works correctly, while my solution - not. I'm going to test it more detailed and create appropriate ticket for that. Essentially it appears, that your solution will not work in my particular environment. My apologies again. I have to slightly edit your answer in order to delete downvote. – Andremoniy May 26 '16 at 21:37
  • @Tunaki, Holger, I was confused about two similar methods `T query(...)` and `void query(...)`. They work differently, and last one indeed returns number of row inside `rs.getRow()`, but it doesn't matter. My great apologies. – Andremoniy May 26 '16 at 22:16
  • 1
    @Andremoniy: regarding the overloads, I think they are indeed unfortunate. But that’s why I emphasized in an earlier comment the “shape of the lambda expression”, which is sufficient here to disambiguate. A single argument lambda which returns a value (and is not `void` compatible) can only be a `ResultSetExtractor`. This can be quite subtle, i.e. in your question you are using `rs -> { return rs.getLong(1); }` which is unambiguous, but when you simplify it to `rs -> rs.getLong(1)`, it can be `void` compatible, dropping the result, thus, could be a `RowCallbackHandler` as well. – Holger May 27 '16 at 06:55