【问题标题】:resultSet.next() returns false, even though table is populatedresultSet.next() 返回 false,即使表格已填充
【发布时间】:2017-07-08 18:05:26
【问题描述】:

我有一些函数可以帮助从数据库中检索对象。

public User getUser(int beamID) throws NoSuchUserException {
    return userFromResultSet(getUserResultSet(beamID));
}


private ResultSet getUserResultSet(int beamID) {
    try(Connection conn = dataSource.getConnection()) {

        // queries.getUserByBeamID() returns "SELECT * FROM user WHERE beamID=?"
        PreparedStatement stmt = conn.prepareStatement(queries.getUserByBeamID());

        stmt.setInt(1, beamID);
        System.out.println(stmt.toString());
        return stmt.executeQuery();

    } catch (SQLException e) {
        e.printStackTrace();
        throw new IllegalStateException();
    }
}

private User userFromResultSet(ResultSet resultSet) {
    try {
        boolean next = resultSet.next();  // Debugger tells me this is false.
        if (!next)
            throw new NoSuchUserException();

        User user = new User(this,
             resultSet.getInt("beamID"),
             resultSet.getString("name"),
             resultSet.getInt("points"),
             resultSet.getInt("time")
        );

        if (resultSet.next())
            throw new IllegalStateException("Duplicate user entries exist - database integrity compromised!");

        return user;
    } catch (SQLException e) {
        e.printStackTrace();
        throw new IllegalStateException();
    }
}

奇怪的是,我知道数据确实存在有两个原因:

  • 如果条目不存在,我的程序会尝试创建该条目,但尝试这样做会导致错误,即未遵循唯一约束

  • 在我的 SQLite DB 浏览器中运行查询工作正常:

我非常怀疑这是未提交数据的问题,因为这是一个基于文件的数据库,使用文本编辑器打开该文件会显示数据中用户名的实例。

【问题讨论】:

  • “如果条目不存在,我的程序会尝试创建该条目,但尝试这样做会给出未遵循唯一约束的错误。” - 所以,这意味着用户确实 not 存在(否则它不会尝试创建它,这会导致违反唯一约束),但唯一约束可能在 id 上(而不是 or除了beam_id)

标签: java sqlite jdbc resultset


【解决方案1】:

仔细看看你在这里做什么:

try (Connection conn = dataSource.getConnection()) {
    PreparedStatement stmt = conn.prepareStatement(queries.getUserByBeamID());

    stmt.setInt(1, beamID);
    System.out.println(stmt.toString());
    return stmt.executeQuery();
} catch (SQLException e) {
    e.printStackTrace();
    throw new IllegalStateException();
}

我相信 try-with-resources 的约定保证在表达式完成执行后关闭try 子句中指定的资源。我相信结果集也在try 块的末尾关闭,因此调用next() 返回false,因为那里什么都没有。

我编写代码的方式是在try 块内填充User POJO,并返回User 对象而不是返回结果集:

private User getUserResultSet(int beamID) {
    User user = null;
    try (Connection conn = dataSource.getConnection()) {
        PreparedStatement stmt = conn.prepareStatement(queries.getUserByBeamID());

        stmt.setInt(1, beamID);

        ResultSet rs = stmt.executeQuery();
        rs.next();
        user = new User(this,
            rs.getInt("beamID"),
            rs.getString("name"),
            rs.getInt("points"),
            rs.getInt("time")
        );

    } catch (SQLException e) {
        e.printStackTrace();
        throw new IllegalStateException();
    }

    return user;
}

现在您的关注点分离比以前更好了。如果连接、结果集等出现问题,则会在处理这些事情的实际代码中进行处理。如果发生异常或其他错误,将返回 null 用户对象,您应该更新代码以处理这种可能性。

【讨论】:

  • 这是一个很好的观点,但是:ResultSet.next() 的 javadoc 声明它会抛出“如果发生数据库访问错误或在关闭的结果集上调用此方法时会引发 SQLException”。因此,如果这是原因,则 OP 应该收到 SQLException,而不是 false。但是有可能是驱动坏了,没有正确实现 JDBC 规范。
  • @ErwinBolwidt 这也让我想到了,但从我读到的行为是特定于驱动程序的。如果您对有效查询中的 result set 为空有更好的解释,请随时发布答案。
  • 这个,其实解决了;我将 ResultSet 的用法嵌套在 try-with-resources 中,它工作得很好。感谢您的帮助!
  • @TimBiegeleisen 我已经对这个问题添加了评论,因为 OP 关于插入用户“如果它不存在”的功能的评论是矛盾的。
  • @ErwinBolwidt,问题在于ResultSet 提前关闭,使其表现好像所有数据都已处理,或者在这种情况下,查询返回一个空集。因此,我的程序表现得好像用户不存在,尽管它确实存在。
【解决方案2】:

虽然接受的答案解决了问题的根本原因,但实际实施存在一些问题。

  • Exceptions被吞了
  • PreparedStatement 应该独立于 Connection 关闭
  • ResultSet 应该独立于 PreparedStatementConnection 关闭
  • 它不处理 TYPE_FORWARD_ONLY ResultSetsResultSet.next() 为空时抛出的问题
  • 它不检查结果是否唯一
  • 它没有正确的日志记录

一个更完整的例子如下:

package com.stackoverflow.questions.Q42327984;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;

import javax.sql.DataSource;

import lombok.Value;
import lombok.extern.log4j.Log4j2;

@SuppressWarnings("javadoc")
public class Answer {

    /**
     * <p>
     * We use lombok's {@link Log4j2} annotation to create a Log4j2 logger, avoid using {@link System#out},
     * {@link System#err} or {@link Throwable#printStackTrace()}
     * </p>
     *
     * @See Lombok's <a href="https://www.projectlombok.org/features/log">log</a> api.
     * @See Log4j2's install <a href="https://logging.apache.org/log4j/2.x/maven-artifacts.html">guide</a>.
     */
    @Log4j2
    static class UserDao {

        final DataSource dataSource;

        final UserQueries queries;

        public UserDao(final DataSource dataSource, final UserQueries queries) {
            this.dataSource = dataSource;
            this.queries = queries;
        }
        /**
         * Find {@link User} by beam id.
         * <p>
         * In most systems not finding a User would not be an "exceptional" condition, we should return {@link Optional}
         * instead of throwing an {@link Exception}
         * </p>
         * <p>
         * 'get' is fine for bean like things, but 'find' is for queries
         * </p>
         * <p>
         * We add "ByBeamId" so we can disambiguate between this method and other queries that might also find
         * {@link User} by a int value.
         * </p>
         *
         * @param beamID the beam ID
         * @return the {@link Optional} {@link User}, null's are the "billion dollar mistake"
         * @throws {@link NoSuchUserException} the no such user exception
         */
        public Optional<User> findUserByBeamId(int beamID) throws NoSuchUserException {

            log.debug("findUserByBeamId({})", beamID);

            final String userByBeamID = this.queries.getUserByBeamID();
            log.debug("findUserByBeamId({}) userByBeamID query: {}", beamID, userByBeamID);

            try (final Connection conn = this.dataSource.getConnection();
                    final PreparedStatement stmt = conn.prepareStatement(userByBeamID)) {

                stmt.setInt(1, beamID);

                /*
                 * ResultSets should be closed properly also
                 */
                try (final ResultSet rs = stmt.executeQuery()) {

                    /*
                     * There are other ways to find if the ResultSet is empty but the method below is safe and
                     * can handle most implementations
                     */
                    int resultSetType = rs.getType();

                    try {

                        boolean scrolledForwardToRow = rs.next();

                        if (!scrolledForwardToRow) {
                            return Optional.empty();
                        }

                    } catch (SQLException e) {

                        /*
                         * ResultSets of TYPE_FORWARD_ONLY can throw a SQLException on ResultSet.next() if the ResultSet
                         * is
                         * empty.
                         */
                        if (ResultSet.TYPE_FORWARD_ONLY == resultSetType) {
                            log.debug(
                                    "findUserByBeamId({}): initial rs.next() call failed but ResultSet is TYPE_FORWARD_ONLY so returning empty.",
                                    userByBeamID, e);
                            return Optional.empty();
                        }

                        log.error("findUserByBeamId({}): initial rs.next() call failed, return empty.", userByBeamID,
                                e);
                        throw e;
                    }

                    /*
                     * User should not have a reference to its DAO, this is a recipe for trouble.
                     *
                     * In general User should be an interface and a static factory method or a builder should be used
                     * for construction.
                     * 'new' should be avoided as it tightly couples a specific implementation of User.
                     *
                     * User user = new User(this,
                     * rs.getInt("beamID"),
                     * rs.getString("name"),
                     * rs.getInt("points"),
                     * rs.getInt("time"));
                     */

                    /*
                     * Assigning to local variables has a couple advantages:
                     * - It makes stepping through the debugger much easier.
                     * - When an exception occurs the line number in the stack trace will only contain one method.
                     *
                     * Note, that the performance impact of local variables is not significant in a method of this
                     * complexity with so many external calls.
                     */

                    final int beamIDFromRs = rs.getInt("beamID");
                    final String name = rs.getString("name");
                    final int points = rs.getInt("points");
                    final int time = rs.getInt("time");

                    final User user = User.of(beamIDFromRs, name, points, time);

                    /*
                     * Before we return our result we need to make sure it was unique.
                     *
                     * There are other ways to find if the ResultSet has no more row but the method below is safe
                     * can handle most implementations.
                     */
                    try {

                        boolean scrolledForward = rs.next();

                        if (!scrolledForward) {
                            return Optional.of(user);
                        }

                    } catch (SQLException se) {

                        /*
                         * ResultSets of TYPE_FORWARD_ONLY can throw a SQLException on ResultSet.next() if the ResultSet
                         * is
                         * empty.
                         */
                        if (ResultSet.TYPE_FORWARD_ONLY == resultSetType) {

                            log.debug(
                                    "findUserByBeamId({}): rs.next() call failed but ResultSet is TYPE_FORWARD_ONLY so returning user: {}.",
                                    userByBeamID, user, se);

                            return Optional.of(user);
                        }

                        log.error("findUserByBeamId({}): rs.next() call failed.", userByBeamID, se);

                        throw se;

                    }

                    log.error("findUserByBeamId({}): results not unique.", userByBeamID);

                    throw new IllegalStateException("findUserByBeamId(" + beamID + ") results were not unique.");
                }

            } catch (SQLException se) {

                log.error("findUserByBeamId({}): failed", userByBeamID, se);

                throw new IllegalStateException("findUserByBeamId(" + beamID + ") failed", se);
            }

        }
        /**
         * Gets the user.
         *
         * @param beamID the beam ID
         * @return the {@link User}
         * @throws NoSuchUserException if the user does not exist.
         * @deprecated use {@link #findUserByBeamId(int)}
         */
        @Deprecated
        public User getUser(final int beamID) throws NoSuchUserException {
            return findUserByBeamId(beamID).orElseThrow(() -> new NoSuchUserException(beamID));
        }

    }

    interface UserQueries {
        String getUserByBeamID();
    }

    /**
     * Use interfaces to avoid tight coupling
     */
    interface User {

        /**
         * static factory method for creating User instances
         *
         * @param beamID the beam ID
         * @param name the name
         * @param points the points
         * @param time the time
         * @return the user
         */
        static User of(final int beamID, final String name, final int points, final int time) {
            return new DefaultUser(beamID, name, points, time);
        }

        /**
         * Gets the beam ID.
         *
         * @return the beam ID
         */
        int getBeamID();

        /**
         * Gets the name.
         *
         * @return the name
         */
        String getName();

        /**
         * Gets the points.
         *
         * @return the points
         */
        int getPoints();

        /**
         * Gets the time.
         *
         * @return the time
         */
        int getTime();
    }

    /**
     * <p>
     * We use lombok's {@link Value} to create a final immutable object with an all args constuctor
     * ({@link #DefaultUser(int, String, int, int)} and proper
     * {@link #toString()}, {@link #equals(Object)} and {@link #hashCode()} methods
     * </p>
     *
     * @See Lombok's <a href="https://www.projectlombok.org/features/Value">value</a> api.
     */
    @Value
    static class DefaultUser implements User {

        final int beamID;

        final String name;

        final int points;

        final int time;
    }

    /**
     * The Class NoSuchUserException.
     */
    @SuppressWarnings("serial")
    public static class NoSuchUserException extends RuntimeException {

        final int beamID;

        public NoSuchUserException(int beamID) {
            this.beamID = beamID;
        }

        /**
         * Gets the beam ID.
         *
         * @return the beam ID
         */
        public int getBeamID() {
            return this.beamID;
        }

    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-05
    • 2011-02-16
    • 2019-11-08
    相关资源
    最近更新 更多