【问题标题】:Proper way of handling database connections in a medium sized web application在中型 Web 应用程序中处理数据库连接的正确方法
【发布时间】:2011-10-01 16:24:37
【问题描述】:

我目前正在维护一个实习生为公司内部使用而制作的中小型 Java Web 应用程序(仅使用普通 JSP/Servlet),但我在连接方面遇到了一些问题。

有时会突然出现“语句已关闭”或“连接已关闭”之类的错误,然后整个应用程序将停止工作并且必须重新启动服务器。

我没有太多经验,也没有人可以指导或教我有关最佳实践、设计模式等方面的知识,但我很确定这不是正确的做法。我读过诸如 DAL、DAO 和 DTO 之类的东西。我们的应用没有这些。

整个 Web 应用程序(即 servlet)基本上都充满了类似于以下的调用:

Database db = Database.getInstance();
db.execute("INSERT INTO SomeTable VALUES (a, b, c)");
db.execute("UPDATE SomeTable SET Col = Val");

SELECT 是这样完成的:

ArrayList<Model> results = Model.fetch("SELECT * FROM SomeTable");

其中 Model 是一个扩展 HashMap 并表示表中的单个行的类。

这是 Database.java 的代码,想知道是否有人可以指出明显的错误(我很确定有很多),可以完成的任何快速修复以及有关最佳实践的一些资源关于数据库连接/连接处理。

package classes;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

public final class Database {

    public static Database getInstance() {
        if (Database.instance == null) {
            Database.instance = new Database();
        }
        return Database.instance;
    }

    // Returns the results for an SQL SELECT query.
    public ArrayList<HashMap<String, Object>> fetch(String sql) {

        ArrayList<HashMap<String, Object>> results = new ArrayList<HashMap<String, Object>>();

        try {

            PreparedStatement stmt = this.connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT);
            ResultSet rs = stmt.executeQuery();
            this.doFetch(rs, results);
            stmt.close();

        } catch (SQLException e) {
            this.handleException(e, sql);
        }

        return results;
    }

    public ArrayList<HashMap<String, Object>> fetch(String sql, ArrayList<Object> parameters) {

        ArrayList<HashMap<String, Object>> results = new ArrayList<HashMap<String, Object>>();

        try {

            // Bind parameters to statement.
            PreparedStatement pstmt = this.connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT);
            for (int i=0; i<parameters.size(); i++) {
                pstmt.setObject(i+1, parameters.get(i));
            }

            ResultSet rs = pstmt.executeQuery();
            this.doFetch(rs, results);
            pstmt.close();

        } catch (SQLException e) {
            this.handleException(e, sql, parameters);
        }

        return results;
    }

    public int execute(String sql) {
        int result = 0;
        try {
            Statement stmt = this.connection.createStatement();
            result = stmt.executeUpdate(sql);
            stmt.close();
        } catch (SQLException e) {
            this.handleException(e, sql);
        }
        return result;
    }

    public int execute(String sql, ArrayList<Object> parameters) {
        int result = 0;
        try {
            PreparedStatement pstmt = this.connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT);
            for (int i=0; i<parameters.size(); i++) {
                if (parameters.get(i) == null) {
                    pstmt.setNull(i+1, java.sql.Types.INTEGER);
                } else {
                    pstmt.setObject(i+1, parameters.get(i));
                }
            }
            result = pstmt.executeUpdate();
            pstmt.close();
        } catch (SQLException e) {
            this.handleException(e, sql, parameters);
        }
        return result;
    }

    public void commit() {
        try {
            this.connection.commit();
        } catch (SQLException e) {
            System.out.println("Failed to commit transaction.");
        }
    }

    public Connection getConnection() {
        return this.connection;
    }


    private static Database instance;
    private static DataSource dataSource = null;
    private Connection connection;

    private Database() {
        this.connect();
        this.execute("SET SCHEMA " + Constant.DBSCHEMA);
    }

    private void connect() {
        Connection connection = null;
        if (dataSource == null) {
            try {
                InitialContext initialContext = new InitialContext();
                dataSource = (DataSource)initialContext.lookup(
                        Constant.DEPLOYED ? Constant.PROD_JNDINAME : Constant.TEST_JNDINAME);
            } catch (NamingException e) {
                e.printStackTrace();
            }
        }
        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        this.connection = connection;
    }

    // Fetches the results from the ResultSet into the given ArrayList.

    private void doFetch(ResultSet rs, ArrayList<HashMap<String, Object>> results) throws SQLException {
        ResultSetMetaData rsmd = rs.getMetaData();

        ArrayList<String> cols = new ArrayList<String>();           
        int numCols = rsmd.getColumnCount();

        for (int i=1; i<=numCols; i++) {
            cols.add(rsmd.getColumnName(i));
        }

        while (rs.next()) {
            HashMap<String, Object> result = new HashMap<String, Object>();
            for (int i=1; i<=numCols; i++) {
                result.put(cols.get(i-1), rs.getObject(i));
            }
            results.add(result);
        }

        rs.close();
    }

    private void handleException(SQLException e, String sql) {
        System.out.println("SQLException " + e.getErrorCode() + ": " + e.getMessage());
        System.out.println("Statement: " + sql);
        ExceptionAdapter ea = new ExceptionAdapter(e);
        ea.setSQLInfo(e, sql);
        throw ea;
    }

    private void handleException(SQLException e, String sql, ArrayList<Object> parameters) {
        if (parameters.size() < 100) {
            System.out.println("SQLException " + e.getErrorCode() + ": " + e.getMessage());
            System.out.println("PreparedStatement: " + sql.replace("?", "[?]"));
            System.out.println("Parameters: " + parameters.toString());
        }
        ExceptionAdapter ea = new ExceptionAdapter(e);
        ea.setSQLInfo(e, sql, parameters);
        throw ea;
    }
}

谢谢!

【问题讨论】:

    标签: java jsp servlets jdbc websphere


    【解决方案1】:

    为了回答你关于设计原则的问题,这个对象本质上是一个 DAO 对象,它只是不使用命名约定,而且一个大型应用程序会有几个这样的对象用于不同类型的数据(可能使用基本 DAO它们都继承自的对象)。

    广义上的想法是,DAO 是处理数据库连接的中心位置,因此您无需在 Controller 对象中拥有所有代码。

    除了其他人已经指出的缺点之外,这是一些非常了解面向对象编程的人编写的可靠代码。我的建议是从单例更改对象并使用连接池管理数据库连接(正如其他人已经提到的那样)。

    这似乎是一个高度抽象的对象,它返回一个映射数组列表(键值对),可用于不同的数据类型,然后在模型对象或数据类型中使用返回的信息来构建 java 对象。

    【讨论】:

      【解决方案2】:

      该类永远不会关闭连接:this.connection.close()。由于Database 是一个Singleton,应用程序不使用连接池(数据源)。所有传入请求仅使用一个连接。

      经验法则:每个方法获取一个连接(可能是每个 SQL 语句)。 dataSource.getConnection()不贵。

      这就是我重构类的方式:

      1. 删除公共的getConnection 方法,如果它在Databaseclass 之外使用,你确实有设计问题
      2. 删除commit 方法。我想这没有意义,因为从未调用过 connection.setAutoCommit(false),而且我没有看到 rollback 方法
      3. 删除实例变量connection,改为每次调用获取一个连接
      4. 并在每次调用的finally 块中正确关闭此连接

      免责声明:目前不知道您的事务处理是如何工作的,所以我可能对#2 有误。

      获取连接的方法示例代码:

      Connection c = null;
      try {
          c = this.dataSource.getConnection();
          c.executeStatement("select * from dual");
      } catch (SQLException e) {
          // handle...
      } finally {
          closeConnection(c);
      }
      

      有趣的是这个应用程序是如何工作的 :-)

      【讨论】:

        【解决方案3】:

        您正在以一种非常不安全的方式使用 JDBC 连接。它可以从多个线程访问,它不是线程安全的。这是一个 Web 应用程序,多个请求可以同时来自不同的用户。您的应用程序没有更频繁地崩溃是一个小奇迹。您可以使用多种策略来解决此问题。您可以将连接存储在 ThreadLocal 或堆栈中。如果要在堆栈上保持连接,则必须在每个方法调用中打开和关闭它们。为此,您必须使用连接池。无论如何,连接池都不会受到伤害。

        【讨论】:

          【解决方案4】:

          对于简单的应用来说,这样做并没有错。但是,如果您的应用程序甚至比较复杂,您可能希望研究一个简单的框架,例如 iBatis。

          我肯定会做几件事。一方面,当抛出异常时,应用程序可能会泄漏连接,这与关闭语句的方式有关。所有关闭语句都应该移到 finally 块中。

          所以而不是:

          try {
                      Statement stmt = this.connection.createStatement();
                      result = stmt.executeUpdate(sql);
                      stmt.close();
                  } catch (SQLException e) {
                      this.handleException(e, sql);
                  }
          

          改为这样做:

          Statement stmt = null;
          try {
                  stmt = this.connection.createStatement();
                  result = stmt.executeUpdate(sql);
              } catch (SQLException e) {
                  this.handleException(e, sql);
              } finally {
                  if (stmt != null) stmt.close();
              }
          

          另一件事是我会确保您为数据源使用数据库连接池。如果您在 Tomcat 中运行它,希望在 Tomcat 安装中定义了一个连接池,并且您的应用程序正在使用它。

          编辑:再次查看代码后,我也没有看到数据库连接实际关闭的位置。这可能就是您连接不足的原因。您需要向 Database 类添加一个 close 方法,该方法调用 connection.close()。并确保在完成查询后调用它。同样,在 try/finally 块中。

          【讨论】:

          • 我建议您使用 apache commons 的 DbUtils.closeQuietly() 来关闭 finally 块中的语句、结果集和连接对象。这样可以避免在调用 close() 时检查 null 并捕获 SQLExceptions
          • 这是一个网络应用程序。这意味着在任何给定时刻都可能有多个请求正在处理。没有同步,连接是数据库类的成员变量,可以从多个线程中使用。
          猜你喜欢
          • 1970-01-01
          • 2014-07-02
          • 2023-03-05
          • 1970-01-01
          • 2023-04-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-11-09
          相关资源
          最近更新 更多