zoukankan      html  css  js  c++  java
  • JavaWeb(四):JDBC

    数据持久化(persistence)

    把数据保存到可掉电式存储设备中以供之后使用。

    大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以”固化”,而持久化的实现过程大多通过各种关系数据库来完成。持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中。

    Java中的数据存储技术

    在Java中,数据库存取技术可分为如下几类:

    • JDBC直接访问数据库
    • JDO技术
    • 第三方O/R工具,如Hibernate, ibatis 等

    JDBC是java访问数据库的基石,JDO、Hibernate等只是更好的封装了JDBC。

    一、JDBC基础

    JDBC(Java Database Connectivity)是一个独立于特定数据库管理系统、通用的SQL数据库存取和操作的公共接口(一组API),定义了用来访问数据库的标准Java类库,使用这个类库可以以一种标准的方法、方便地访问数据库资源。JDBC为访问不同的数据库提供了一种统一的途径,为开发者屏蔽了一些细节问题。JDBC的目标是使Java程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,这样就使得程序员无需对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程。

    没有JDBC时,直接连接,各个数据库都要单独编写连接程序

     JDBC提供了一组接口,面向接口编程,各数据库厂商只需提供各自的接口实现

    JDBC体系结构

    JDBC接口(API)包括两个层次:

    • 面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果)。
    • 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用。

    JDBC驱动程序分类

    JDBC驱动程序:各个数据库厂商根据JDBC的规范制作的 JDBC 实现类的类库
    JDBC驱动程序总共有四种类型:

    • 第一类:JDBC-ODBC桥。
    • 第二类:部分本地API部分Java的驱动程序。
    • 第三类:JDBC网络纯Java驱动程序。
    • 第四类:本地协议的纯 Java 驱动程序
    • 第三、四两类都是纯Java的驱动程序,因此,对于Java开发者来说,它们在性能、可移植性、功能等方面都有优势。

    本地协议的纯Java驱动程序

    多数数据库厂商已经支持允许客户程序通过网络直接与数据库通信的网络协议。这种类型的驱动程序完全使用 Java 编写,通过与数据库建立的 Socket 连接,采用具体与厂商的网络协议把 JDBC 调用转换为直接连接的网络调用。

     

    JDBC API

    JDBC API 是一系列的接口,它使得应用程序能够进行数据库联接,执行SQL语句,并且得到返回结果。

    二、Driver接口和DriverManager

    Java.sql.Driver 接口是所有 JDBC 驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供不同的实现。在程序中不需要直接去访问实现了 Driver 接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现

        /**
         * Driver是一个接口:数据厂商必须实现,能从其中获取数据库连接。 
         * 下面的代码执行前要加入mysql驱动
         */
        @Test
        void testDriver() throws SQLException {
    
            // 1.创建一个Driver实现类的对象
            Driver driver = new com.mysql.jdbc.Driver();
    
            // 3.准备连接数据库的基本信息:url,user,password
            String url = "jdbc:mysql://127.0.0.1:3306/test";
            Properties info = new Properties();
            info.put("user", "root");
            info.put("password", "root");
    
            // 3.调用Driver接口的connect(url, info)获取数据库连接
            Connection connection = driver.connect(url, info);
            System.out.println(connection);
        }

    上面的代码依赖于mysql提供的Driver

    我们的目标是编写一个通用的方法,在不修改源程序的情况下,可以获取任何数据库的连接

        /**
         * 编写一个通用的方法,在不修改源程序的情况下,可以获取任何数据库的连接 解决方案:把数据库驱动Driver实现类的全类名、url、user、password放入一个 配置文件中,通过修改配置文件的方式实现和具体的数据库解耦。
         * 
         * @throws ClassNotFoundException
         * @throws IllegalAccessException
         * @throws InstantiationException
         */
        public Connection getConnection() throws Exception {
            String driverClass = null;
            String jdbcUrl = null;
            String user = null;
            String password = null;
    
            // 读取类路径下的jdbc.properties文件
            InputStream in = getClass().getClassLoader().getResourceAsStream("jdbc.properties");
            Properties properties = new Properties();
            properties.load(in);
            driverClass = properties.getProperty("driver");
            jdbcUrl = properties.getProperty("jdbcUrl");
            user = properties.getProperty("user");
            password = properties.getProperty("password");
    
            Driver driver = (Driver) Class.forName(driverClass).newInstance();
            Properties info = new Properties();
            info.put("user", user);
            info.put("password", password);
            Connection connection = driver.connect(jdbcUrl, info);
            return connection;
        }

    加载与注册JDBC驱动

    加载 JDBC 驱动需调用 Class 类的静态方法 forName(),向其传递要加载的 JDBC 驱动的类名。DriverManager 类是驱动程序管理器类,负责管理驱动程序。通常不用显式调用 DriverManager 类的 registerDriver() 方法来注册驱动程序类的实例,因为 Driver 接口的驱动程序类都包含了静态代码块,在这个静态代码块中,会调用 DriverManager.registerDriver() 方法来注册自身的一个实例。

    建立连接
    可以调用 DriverManager 类的 getConnection() 方法建立到数据库的连接。JDBC URL 用于标识一个被注册的驱动程序,驱动程序管理器通过这个 URL 选择正确的驱动程序,从而建立到数据库的连接。
    JDBC URL的标准由三部分组成,各部分间用冒号分隔。
    jdbc:<子协议>:<子名称>

    • 协议:JDBC URL中的协议总是jdbc
    • 子协议:子协议用于标识一个数据库驱动程序
    • 子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库提供足够的信息

    几种常用数据库的JDBC的url

    对于 Oracle 数据库连接,采用如下形式:

    jdbc:oracle:thin:@localhost:1521:sid

    对于 SQLServer 数据库连接,采用如下形式:

    jdbc:microsoft:sqlserver//localhost:1433; DatabaseName=sid

    对于 MYSQL 数据库连接,采用如下形式:

    jdbc:mysql://localhost:3306/sid

    实际开发中不推荐使用Driver,而是使用DriverManager

    com.mysql.jdbc.Driver的源代码

    public class Driver extends NonRegisteringDriver implements java.sql.Driver {  
        // ~ Static fields/initializers  
        // ---------------------------------------------  
      
        //  
        // Register ourselves with the DriverManager  
        //  
        static {  
            try {  
                java.sql.DriverManager.registerDriver(new Driver());  
            } catch (SQLException E) {  
                throw new RuntimeException("Can't register driver!");  
            }  

    只要Driver对象生成,就会调用DriverManager进行驱动的注册

    在 com.mysql.jdbc.Driver中有一段静态代码块,是向 DriverManager注册一个Driver实例。这样在 Class.forName("com.mysql.jdbc.Driver")的时候,就会首先去执行这个静态代码块,于是和DriverManager.registerDriver(new Driver())有了相同的效果。

    既然已经注册了Driver,不必再生成Driver对象,直接用DriverManager连接就可了。

     /**
         * DriverManager是驱动的管理类
         * 1. 可以通过重载getConnection()方法获取数据库连接,较为方便
         * 2. 可以同时管理多个驱动,若注册了多个数据库连接,则调用getConnection()方法时传入的参数不同,
         *    即返回不同的数据库连接
         */
        @Test
        public void testDriverManager() throws SQLException, IOException, ClassNotFoundException {
    
            //1. 准备连接数据库的 4 个字符串. 
            //1). 创建 Properties 对象
            Properties properties = new Properties();
            
            //2). 获取 jdbc.properties 对应的输入流
            InputStream in = 
                    this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");
            
            //3). 加载 2) 对应的输入流
            properties.load(in);
            
            //4). 具体决定 user, password 等4 个字符串. 
            String user = properties.getProperty("user");
            String password = properties.getProperty("password");
            String jdbcUrl = properties.getProperty("jdbcUrl");
            String driver = properties.getProperty("driver");
            
            String driverClass2 = "oracle.jdbc.driver.OracleDriver";
            String jdbcUrl2 = "jdbc:oracle:thin:@localhost:1521:orc1";
            String user2 = "root";
            String password2 = "root";
            user = properties.getProperty("user");
            password = properties.getProperty("password");
    
            // 2. 加载数据库驱动程序(注册驱动)
            // java.sql.DriverManager.registerDriver(Class.forName(driverClass).newInstance());
            // 对应的Driver实现类中有注册驱动的静态代码块,因此上面代码可以省略
            // 可以管理多个驱动程序
            Class.forName(driver);
            Class.forName(driverClass2);
            
            // 3. 通过DriverManager的getConnection()方法获取数据库连接
            Connection connection = DriverManager.getConnection(jdbcUrl, user, password);
            System.out.println(connection);
        }

    三、Statement

    通过调用 Connection 对象的 createStatement 方法创建该对象,该对象用于执行静态的 SQL 语句,并且返回执行结果。
    Statement 接口中定义了下列方法用于执行 SQL 语句:
    ResultSet excuteQuery(String sql)
    int excuteUpdate(String sql)

    /**
         * 通过 JDBC 向指定的数据表中插入一条记录. 1. Statement: 用于执行 SQL 语句的对象 
    1). 通过 Connection 的 createStatement() 方法来获取
    2). 通过
    executeUpdate(sql) 可以执行 SQL 语句. 3). 传入的 SQL 可以是 INSRET, UPDATE 或 DELETE. 但不能是 SELECT
           2. Connection、Statement
         * 都是应用程序和数据库服务器的连接资源. 使用后一定要关闭. 需要在 finally 中关闭 Connection 和 Statement 对象. 
    3. 关闭的顺序是: 先关闭后获取的. 即先关闭 Statement 后关闭 * Connection
    */ @Test public void testStatement() throws Exception { // 1. 获取数据库连接 Connection conn = null; Statement statement = null; try { conn = getConnection2(); // 3. 准备插入的 SQL 语句 String sql = null; sql = "INSERT INTO customers (NAME, address, phone) " + "VALUES('XYZ', 'bj', '1990-12-12')"; // sql = "DELETE FROM customers WHERE id = 1"; // sql = "UPDATE customers SET name = 'TOM' " + // "WHERE id = 4"; System.out.println(sql); // 4. 执行插入. // 1). 获取操作 SQL 语句的 Statement 对象: // 调用 Connection 的 createStatement() 方法来获取 statement = conn.createStatement(); // 2). 调用 Statement 对象的 executeUpdate(sql) 执行 SQL 语句进行插入 statement.executeUpdate(sql); } catch (Exception e) { e.printStackTrace(); } finally { try { // 5. 关闭 Statement 对象. if (statement != null) statement.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { // 2. 关闭连接 if (conn != null) conn.close(); } } }

    于是,我们可以写一个通用的更新方法:

        /**
         * 通用的更新的方法: 包括 INSERT、UPDATE、DELETE
         * 版本 1.
         */
        public void update(String sql){
            Connection conn = null;
            Statement statement = null;
            
            try {
                conn = JDBCTools.getConnection();
                statement = conn.createStatement();
                statement.executeUpdate(sql);
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.release(statement, conn);
            }
        }

    将连接和断开连接封装到类中,工具方法都定义为静态方法:

    /**
     * 操作 JDBC 的工具类. 其中封装了一些工具方法 Version 1
     */
    public class JDBCTools {
    
        public static void release(ResultSet rs, 
                Statement statement, Connection conn) {
            if(rs != null){
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            
            
            if (statement != null) {
                try {
                    statement.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
    
            if (conn != null) {
                try {
                    conn.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
        }
        
        /**
         * 关闭 Statement 和 Connection
         * @param statement
         * @param conn
         */
        public static void release(Statement statement, Connection conn) {
            if (statement != null) {
                try {
                    statement.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
    
            if (conn != null) {
                try {
                    conn.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
        }
    
        /**
         * 1. 获取连接的方法. 通过读取配置文件从数据库服务器获取一个连接.
         * 
         * @return
         * @throws Exception
         */
        public static Connection getConnection() throws Exception {
            // 1. 准备连接数据库的 4 个字符串.
            // 1). 创建 Properties 对象
            Properties properties = new Properties();
    
            // 2). 获取 jdbc.properties 对应的输入流
            InputStream in = JDBCTools.class.getClassLoader().getResourceAsStream(
                    "jdbc.properties");
    
            // 3). 加载 2) 对应的输入流
            properties.load(in);
    
            // 4). 具体决定 user, password 等4 个字符串.
            String user = properties.getProperty("user");
            String password = properties.getProperty("password");
            String jdbcUrl = properties.getProperty("jdbcUrl");
            String driver = properties.getProperty("driver");
    
            // 2. 加载数据库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块.)
            Class.forName(driver);
    
            // 3. 通过 DriverManager 的 getConnection() 方法获取数据库连接.
            return DriverManager.getConnection(jdbcUrl, user, password);
        }
    
    }

    通过调用 Statement 对象的 excuteQuery() 方法创建该对象,ResultSet 对象以逻辑表格的形式封装了执行数据库操作的结果集,ResultSet 接口由数据库厂商实现。ResultSet 对象维护了一个指向当前数据行的游标,初始的时候,游标在第一行之前,可以通过 ResultSet 对象的 next() 方法移动到下一行。
    ResultSet 接口的常用方法:

    • boolean next()
    • getString()

        public <T> List<T> query(Class<T> clazz, String sql){
            return null;
        }
        
        /**
         * ResultSet: 结果集. 封装了使用 JDBC 进行查询的结果. 
         * 1. 调用 Statement 对象的 executeQuery(sql) 可以得到结果集.
         * 2. ResultSet 返回的实际上就是一张数据表. 有一个指针指向数据表的第一样的前面.
         * 可以调用 next() 方法检测下一行是否有效. 若有效该方法返回 true, 且指针下移. 相当于
         * Iterator 对象的 hasNext() 和 next() 方法的结合体
         * 3. 当指针对位到一行时, 可以通过调用 getXxx(index) 或 getXxx(columnName)
         * 获取每一列的值. 例如: getInt(1), getString("name")
         * 4. ResultSet 当然也需要进行关闭. 
         */
        @Test
        public void testResultSet(){
            //获取 id=4 的 customers 数据表的记录, 并打印
            
            Connection conn = null;
            Statement statement = null;
            ResultSet rs = null;
            
            try {
                //1. 获取 Connection
                conn = JDBCTools.getConnection();
                System.out.println(conn);
                
                //2. 获取 Statement
                statement = conn.createStatement();
                System.out.println(statement);
                
                //3. 准备 SQL
                String sql = "SELECT id, name, email, birth " +
                        "FROM customers";
                
                //4. 执行查询, 得到 ResultSet
                rs = statement.executeQuery(sql);
                System.out.println(rs);
                
                //5. 处理 ResultSet
                while(rs.next()){
                    int id = rs.getInt(1);
                    String name = rs.getString("name");
                    String email = rs.getString(3);
                    Date birth = rs.getDate(4);
                    
                    System.out.println(id);
                    System.out.println(name);
                    System.out.println(email);
                    System.out.println(birth);
                }
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                //6. 关闭数据库资源. 
                JDBCTools.release(rs, statement, conn);
            }
            
        }

    四、PreparedStatement

    使用Statement需要拼写SQL语句,麻烦且容易出错 

    String sql = "INSERT INTO examstudent VALUES(" + student.getFlowId()
                    + "," + student.getType() + ",'" + student.getIdCard() + "','"
                    + student.getExamCard() + "','" + student.getStudentName()
                    + "','" + student.getLocation() + "'," + student.getGrade()
                    + ")";

    写成如下形式就好多了

    String sql = "INSERT INTO examstudent VALUE(?, ?, ?, ?, ?, ?, ?);"

    使用PreparedStatement

    可以通过调用 Connection 对象的 preparedStatement() 方法获取 PreparedStatement 对象,PreparedStatement 接口是 Statement 的子接口,它表示一条预编译过的 SQL 语句,PreparedStatement 对象所代表的 SQL 语句中的参数用问号(?)来表示。

    步骤:

    • 创建PreparedStatement。
    String sql = "INSERT INTO examstudent VALUE(?, ?, ?, ?, ?, ?, ?);"
    PreparedStatement ps = conn.prepareStatement(sql);
    • 调用 PreparedStatement 对象的 setXXX() 方法来设置这些参数. setXXX(int index, Object val) 方法。有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引(从 1 开始)第二个参数是设置的 SQL 语句中的参数(即占位符)的值。
    • 执行SQL语句,使用executeQuery()或executeUpdate(),不再需要传入SQL语句。
        @Test
        public void testPreparedStatement() {
            Connection connection = null;
            PreparedStatement preparedStatement = null;
    
            try {
                connection = JDBCTools.getConnection();
                String sql = "INSERT INTO customers (name, email, birth) "
                        + "VALUES(?,?,?)";
    
                preparedStatement = connection.prepareStatement(sql);
                preparedStatement.setString(1, "ATGUIGU");
                preparedStatement.setString(2, "simpleit@163.com");
                // 外边的Date是sql Date
    preparedStatement.setDate(
    3, new Date(new java.util.Date().getTime())); preparedStatement.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } finally { JDBCTools.releaseDB(null, preparedStatement, connection); } }
    /**
         * 执行 SQL 语句, 使用 PreparedStatement
         * @param sql
         * @param args: 填写 SQL 占位符的可变参数
         */
        public static void update(String sql, Object ... args){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            
            try {
                connection = JDBCTools.getConnection();
                preparedStatement = connection.prepareStatement(sql);
                
                for(int i = 0; i < args.length; i++){
                    preparedStatement.setObject(i + 1, args[i]);
                }
                
                preparedStatement.executeUpdate();
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, preparedStatement, connection);
            }
        }
        

    PreparedStatement vs Statement 

    代码的可读性和可维护性,PreparedStatement 能最大可能提高性能:

    • DBServer会对预编译语句提供性能优化。因为预编译语句有可能被重复调用,所以语句在被DBServer的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中就会得到执行。
    • 在statement语句中,即使是相同操作但因为数据内容不一样,所以整个语句本身不能匹配,没有缓存语句的意义.事实是没有数据库会对普通语句编译后的执行代码缓存.这样每执行一次都要对传入的语句编译一次.
    • (语法检查,语义检查,翻译成二进制命令,缓存)

    PreparedStatement 可以防止 SQL 注入

    SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令,从而利用系统的 SQL 引擎完成恶意行为的做法。对于 Java 而言,要防范 SQL 注入,只要用 PreparedStatement 取代 Statement 就可以了。

    	/**
    	 * SQL 注入.
    	 */
    	@Test
    	public void testSQLInjection() {
    		String username = "a' OR PASSWORD = ";
    		String password = " OR '1'='1";
    
    		String sql = "SELECT * FROM users WHERE username = '" + username
    				+ "' AND " + "password = '" + password + "'";
    
    		System.out.println(sql);
    
    		Connection connection = null;
    		Statement statement = null;
    		ResultSet resultSet = null;
    
    		try {
    			connection = JDBCTools.getConnection();
    			statement = connection.createStatement();
    			resultSet = statement.executeQuery(sql);
    
    			if (resultSet.next()) {
    				System.out.println("登录成功!");
    			} else {
    				System.out.println("用户名和密码不匹配或用户名不存在. ");
    			}
    
    		} catch (Exception e) {
    			e.printStackTrace();
    		} finally {
    			JDBCTools.releaseDB(resultSet, statement, connection);
    		}
    	}
    

    五、利用反射及JDBC元数据编写通用查询方法

    查询customer和查询student,虽然字段不一样,但有很多相似的地方

    public Customer getCustomer(String sql, Object... args) {
            Customer customer = null;
    
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
    
            try {
                connection = JDBCTools.getConnection();
                preparedStatement = connection.prepareStatement(sql);
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
                resultSet = preparedStatement.executeQuery();
    
                if (resultSet.next()) {
                    // stu = new Student();
                    // stu.setFlowId(resultSet.getInt(1));
                    // stu.setType(resultSet.getInt(2));
                    // stu.setIdCard(resultSet.getString(3));
    
                    customer = new Customer();
                    customer.setId(resultSet.getInt(1));
                    customer.setName(resultSet.getString(2));
                    customer.setEmail(resultSet.getString(3));
                    customer.setBirth(resultSet.getDate(4));
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
    
            return customer;
        }
    
        public Student getStudent(String sql, Object... args) {
            Student stu = null;
    
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
    
            try {
                connection = JDBCTools.getConnection();
                preparedStatement = connection.prepareStatement(sql);
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
                resultSet = preparedStatement.executeQuery();
    
                if (resultSet.next()) {
                    stu = new Student();
                    stu.setFlowId(resultSet.getInt(1));
                    stu.setType(resultSet.getInt(2));
                    stu.setIdCard(resultSet.getString(3));
                    // ...
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
    
            return stu;
        }

    使用 JDBC 驱动程序处理元数据 

    Java 通过JDBC获得连接以后,得到一个Connection 对象,可以从这个对象获得有关数据库管理系统的各种信息,包括数据库中的各个表,表中的各个列,数据类型,触发器,存储过程等各方面的信息。根据这些信息,JDBC可以访问一个实现事先并不了解的数据库。获取这些信息的方法都是在DatabaseMetaData类的对象上实现的,而DataBaseMetaData对象是在Connection对象上获得的。

    DatabaseMetaData类

    DatabaseMetaData 类中提供了许多方法用于获得数据源的各种信息,通过这些方法可以非常详细的了解数据库的信息:

    • getURL():返回一个String类对象,代表数据库的URL。
    • getUserName():返回连接当前数据库管理系统的用户名。
    • isReadOnly():返回一个boolean值,指示数据库是否只允许读操作。
    • getDatabaseProductName():返回数据库的产品名称。
    • getDatabaseProductVersion():返回数据库的版本号。
    • getDriverName():返回驱动驱动程序的名称。
    • getDriverVersion():返回驱动程序的版本号。

    ResultSetMetaData 类

    可用于获取关于 ResultSet 对象中列的类型和属性信息的对象:

    • getColumnName(int column):获取指定列的名称
    • getColumnCount():返回当前 ResultSet 对象中的列数。
    • getColumnTypeName(int column):检索指定列的数据库特定的类型名称。
    • getColumnDisplaySize(int column):指示指定列的最大标准宽度,以字符为单位。
    • isNullable(int column):指示指定列中的值是否可以为 null。
    • isAutoIncrement(int column):指示是否自动为指定列进行编号,这样这些列仍然是只读的。

    public class MetaDataTest {
    
        /**
         * ResultSetMetaData: 描述结果集的元数据. 
         * 可以得到结果集中的基本信息: 结果集中有哪些列, 列名, 列的别名等.
         * 结合反射可以写出通用的查询方法. 
         */
        @Test
        public void testResultSetMetaData(){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT id, name customerName, email, birth " +
                        "FROM customers";
                preparedStatement = connection.prepareStatement(sql);
                resultSet = preparedStatement.executeQuery();
    
                //1. 得到 ResultSetMetaData 对象
                ResultSetMetaData rsmd = resultSet.getMetaData();
                
                //2. 得到列的个数
                int columnCount = rsmd.getColumnCount();
                System.out.println(columnCount);
                
                for(int i = 0 ; i < columnCount; i++){
                    //3. 得到列名
                    String columnName = rsmd.getColumnName(i + 1);
                    
                    //4. 得到列的别名
                    String columnLabel = rsmd.getColumnLabel(i + 1);
                    
                    System.out.println(columnName + ", " + columnLabel);
                }
                
                
                
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
        }
        
        /**
         * DatabaseMetaData 是描述 数据库 的元数据对象.
         * 可以由 Connection 得到. 
         * 了解. 
         */
        @Test
        public void testDatabaseMetaData(){
            Connection connection = null;
            ResultSet resultSet = null;
            
            try {
                connection = JDBCTools.getConnection();
                DatabaseMetaData data = connection.getMetaData();
                
                //可以得到数据库本身的一些基本信息
                //1. 得到数据库的版本号
                int version = data.getDatabaseMajorVersion();
                System.out.println(version);
                
                //2. 得到连接到数据库的用户名
                String user = data.getUserName();
                System.out.println(user);
                
                //3. 得到 MySQL 中有哪些数据库
                resultSet = data.getCatalogs();
                while(resultSet.next()){
                    System.out.println(resultSet.getString(1));
                }
                
                //...
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(resultSet, null, connection);
            }
        }
    
    }

    利用反射

        /**
         * 通用的查询方法:可以根据传入的 SQL、Class 对象返回 SQL 对应的记录的对象
         * @param clazz: 描述对象的类型
         * @param sql: SQL 语句。可能带占位符
         * @param args: 填充占位符的可变参数。
         * @return
         */
        public <T> T get(Class<T> clazz, String sql, Object... args) {
            T entity = null;
    
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
    
            try {
                //1. 得到 ResultSet 对象
                connection = JDBCTools.getConnection();
                preparedStatement = connection.prepareStatement(sql);
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
                resultSet = preparedStatement.executeQuery();
    
                //2. 得到 ResultSetMetaData 对象
                ResultSetMetaData rsmd = resultSet.getMetaData();
                
                //3. 创建一个 Map<String, Object> 对象, 键: SQL 查询的列的别名, 
                //值: 列的值
                Map<String, Object> values = new HashMap<>();
                
                //4. 处理结果集. 利用 ResultSetMetaData 填充 3 对应的 Map 对象
                if(resultSet.next()){
                    for(int i = 0; i < rsmd.getColumnCount(); i++){
                        String columnLabel = rsmd.getColumnLabel(i + 1);
                        Object columnValue = resultSet.getObject(i + 1);
                        
                        values.put(columnLabel, columnValue);
                    }
                }
                
                //5. 若 Map 不为空集, 利用反射创建 clazz 对应的对象
                if(values.size() > 0){
                    entity = clazz.newInstance();
                    
                    //5. 遍历 Map 对象, 利用反射为 Class 对象的对应的属性赋值. 
                    for(Map.Entry<String, Object> entry: values.entrySet()){
                        String fieldName = entry.getKey();
                        Object value = entry.getValue();
                        ReflectionUtils.setFieldValue(entity, fieldName, value);
                    }
                }
                
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
    
            return entity;
        }

    package com.atguigu.jdbc;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.lang.reflect.Modifier;
    import java.lang.reflect.ParameterizedType;
    import java.lang.reflect.Type;
    
    /**
     * 反射的 Utils 函数集合
     * 提供访问私有变量, 获取泛型类型 Class, 提取集合中元素属性等 Utils 函数
     * @author Administrator
     *
     */
    public class ReflectionUtils {
    
        
        /**
         * 通过反射, 获得定义 Class 时声明的父类的泛型参数的类型
         * 如: public EmployeeDao extends BaseDao<Employee, String>
         * @param clazz
         * @param index
         * @return
         */
        @SuppressWarnings("unchecked")
        public static Class getSuperClassGenricType(Class clazz, int index){
            Type genType = clazz.getGenericSuperclass();
            
            if(!(genType instanceof ParameterizedType)){
                return Object.class;
            }
            
            Type [] params = ((ParameterizedType)genType).getActualTypeArguments();
            
            if(index >= params.length || index < 0){
                return Object.class;
            }
            
            if(!(params[index] instanceof Class)){
                return Object.class;
            }
            
            return (Class) params[index];
        }
        
        /**
         * 通过反射, 获得 Class 定义中声明的父类的泛型参数类型
         * 如: public EmployeeDao extends BaseDao<Employee, String>
         * @param <T>
         * @param clazz
         * @return
         */
        @SuppressWarnings("unchecked")
        public static<T> Class<T> getSuperGenericType(Class clazz){
            return getSuperClassGenricType(clazz, 0);
        }
        
        /**
         * 循环向上转型, 获取对象的 DeclaredMethod
         * @param object
         * @param methodName
         * @param parameterTypes
         * @return
         */
        public static Method getDeclaredMethod(Object object, String methodName, Class<?>[] parameterTypes){
            
            for(Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()){
                try {
                    //superClass.getMethod(methodName, parameterTypes);
                    return superClass.getDeclaredMethod(methodName, parameterTypes);
                } catch (NoSuchMethodException e) {
                    //Method 不在当前类定义, 继续向上转型
                }
                // ... 
            }
            
            return null;
        }
        
        /**
         * 使 filed 变为可访问
         * @param field
         */
        public static void makeAccessible(Field field){
            if(!Modifier.isPublic(field.getModifiers())){
                field.setAccessible(true);
            }
        }
        
        /**
         * 循环向上转型, 获取对象的 DeclaredField
         * @param object
         * @param filedName
         * @return
         */
        public static Field getDeclaredField(Object object, String filedName){
            
            for(Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()){
                try {
                    return superClass.getDeclaredField(filedName);
                } catch (NoSuchFieldException e) {
                    //Field 不在当前类定义, 继续向上转型
                }
            }
            return null;
        }
        
        /**
         * 直接调用对象方法, 而忽略修饰符(private, protected)
         * @param object
         * @param methodName
         * @param parameterTypes
         * @param parameters
         * @return
         * @throws InvocationTargetException 
         * @throws IllegalArgumentException 
         */
        public static Object invokeMethod(Object object, String methodName, Class<?> [] parameterTypes,
                Object [] parameters) throws InvocationTargetException{
            
            Method method = getDeclaredMethod(object, methodName, parameterTypes);
            
            if(method == null){
                throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]");
            }
            
            method.setAccessible(true);
            
            try {
                return method.invoke(object, parameters);
            } catch(IllegalAccessException e) {
                System.out.println("不可能抛出的异常");
            } 
            
            return null;
        }
        
        /**
         * 直接设置对象属性值, 忽略 private/protected 修饰符, 也不经过 setter
         * @param object
         * @param fieldName
         * @param value
         */
        public static void setFieldValue(Object object, String fieldName, Object value){
            Field field = getDeclaredField(object, fieldName);
            
            if (field == null)
                throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
            
            makeAccessible(field);
            
            try {
                field.set(object, value);
            } catch (IllegalAccessException e) {
                System.out.println("不可能抛出的异常");
            }
        }
        
        /**
         * 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter
         * @param object
         * @param fieldName
         * @return
         */
        public static Object getFieldValue(Object object, String fieldName){
            Field field = getDeclaredField(object, fieldName);
            
            if (field == null)
                throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
            
            makeAccessible(field);
            
            Object result = null;
            
            try {
                result = field.get(object);
            } catch (IllegalAccessException e) {
                System.out.println("不可能抛出的异常");
            }
            
            return result;
        }
    }

      

      /**
         * 循环向上转型, 获取对象的 DeclaredMethod
         * @param object
         * @param methodName
         * @param parameterTypes
         * @return
         */
        public static Method getDeclaredMethod(Object object, String methodName, Class<?>[] parameterTypes){
            
            for(Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()){
                try {
                    //superClass.getMethod(methodName, parameterTypes);
                    return superClass.getDeclaredMethod(methodName, parameterTypes);
                } catch (NoSuchMethodException e) {
                    //Method 不在当前类定义, 继续向上转型
                }
                // ... 
            }
            
            return null;
        }

    向上转型?

    for 循环,判断 superClass != Object.class,也就是判断当前对象的类是不是Object,如果不是Object就执行循环体。因为循环体中有return,所以找到方法就返回了,只有出现异常,也就是找不到方法的时候,才会执行 superClass = superClass.getSuperclass()。

    也就是说,当前对象中没有要找的方法,就去父类中找,如果还是找不到,还是往父类去找,直到 superClass != Object.class 这个条件不成立,也就是说,已经找到Object了,还是没找到要找的方法,此时,for循环结束,返回null。

    六、DAO

    数据访问对象

    访问数据信息的类,包含了对数据的CRUD(增删改查),而不包含任何业务相关的信息。

    实现功能的模块化,有利于代码的维护和升级。

    DAO可以被子类继承或直接使用

    使用JDBC编写DAO可能会包含的方法:

    // INSERT,UPDATE,DELETE操作都可以包含在其中
    
    void update(String sql, Object ... args)
    
    // 查询一条记录,返回对应的对象
    
    <T> T get(Class<T> clazz, String sql, Object ... args);
    
    // 查询多条记录,返回对应的对象
    
    <T> List<T> getForList(Class<T> clazz, String sql, Object ... args)
    
    // 返回某条记录的某一个字段的值或一个统计的值(比如,一共有多少条记录等)
    
    <E> E getForValue(String sql, Object ... args)

    在Java EE中,Java类的属性通过getter,setter来定义:get(或set)方法,去除get(或set)后,后面的字母小写即为Java类属性,而以前叫的那个属性,即成员变量,称之为字段。

    操作Java类的属性有一个工具包:beanutils

    需要同时加入commons-beanutils和commons-logging的jar包

    public class BeanUtilsTest {
    
        @Test
        public void testGetProperty() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException{
            Object object = new Student();
            System.out.println(object); 
            
            BeanUtils.setProperty(object, "idCard", "211121196509091876");
            System.out.println(object); 
            
            Object val = BeanUtils.getProperty(object, "idCard");
            System.out.println(val);
        }
        
        @Test
        public void testSetProperty() throws IllegalAccessException, InvocationTargetException {
            
            Object object = new Student();
            System.out.println(object); 
            
            BeanUtils.setProperty(object, "idCard2", "211121196509091876");
            System.out.println(object); 
            
        }
    
    }
    public class DAO {
    
        // INSERT, UPDATE, DELETE 操作都可以包含在其中
        public void update(String sql, Object... args) {
            Connection connection = null;
            PreparedStatement preparedStatement = null;
    
            try {
                connection = JDBCTools.getConnection();
                preparedStatement = connection.prepareStatement(sql);
    
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
    
                preparedStatement.executeUpdate();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                JDBCTools.releaseDB(null, preparedStatement, connection);
            }
        }
    
        // 查询一条记录, 返回对应的对象
        public <T> T get(Class<T> clazz, String sql, Object... args) {
            List<T> result = getForList(clazz, sql, args);
            if(result.size() > 0){
                return result.get(0);
            }
            
            return null;
        }
    
        /**
         * 传入 SQL 语句和 Class 对象, 返回 SQL 语句查询到的记录对应的 Class 类的对象的集合
         * @param clazz: 对象的类型
         * @param sql: SQL 语句
         * @param args: 填充 SQL 语句的占位符的可变参数. 
         * @return
         */
        public <T> List<T> getForList(Class<T> clazz, 
                String sql, Object... args) {
    
            List<T> list = new ArrayList<>();
    
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
    
            try {
                //1. 得到结果集
                connection = JDBCTools.getConnection();
                preparedStatement = connection.prepareStatement(sql);
    
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
    
                resultSet = preparedStatement.executeQuery();
                
                //2. 处理结果集, 得到 Map 的 List, 其中一个 Map 对象
                //就是一条记录. Map 的 key 为 reusltSet 中列的别名, Map 的 value
                //为列的值. 
                List<Map<String, Object>> values = 
                        handleResultSetToMapList(resultSet);
                
                //3. 把 Map 的 List 转为 clazz 对应的 List
                //其中 Map 的 key 即为 clazz 对应的对象的 propertyName, 
                //而 Map 的 value 即为 clazz 对应的对象的 propertyValue
                list = transfterMapListToBeanList(clazz, values);
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
    
            return list;
        }
    
        public <T> List<T> transfterMapListToBeanList(Class<T> clazz,
                List<Map<String, Object>> values) throws InstantiationException,
                IllegalAccessException, InvocationTargetException {
    
            List<T> result = new ArrayList<>();
    
            T bean = null;
    
            if (values.size() > 0) {
                for (Map<String, Object> m : values) {
                    bean = clazz.newInstance();
                    for (Map.Entry<String, Object> entry : m.entrySet()) {
                        String propertyName = entry.getKey();
                        Object value = entry.getValue();
    
                        BeanUtils.setProperty(bean, propertyName, value);
                    }
                    // 13. 把 Object 对象放入到 list 中.
                    result.add(bean);
                }
            }
    
            return result;
        }
    
        /**
         * 处理结果集, 得到 Map 的一个 List, 其中一个 Map 对象对应一条记录
         * 
         * @param resultSet
         * @return
         * @throws SQLException
         */
        public List<Map<String, Object>> handleResultSetToMapList(
                ResultSet resultSet) throws SQLException {
            // 5. 准备一个 List<Map<String, Object>>:
            // 键: 存放列的别名, 值: 存放列的值. 其中一个 Map 对象对应着一条记录
            List<Map<String, Object>> values = new ArrayList<>();
    
            List<String> columnLabels = getColumnLabels(resultSet);
            Map<String, Object> map = null;
    
            // 7. 处理 ResultSet, 使用 while 循环
            while (resultSet.next()) {
                map = new HashMap<>();
    
                for (String columnLabel : columnLabels) {
                    Object value = resultSet.getObject(columnLabel);
                    map.put(columnLabel, value);
                }
    
                // 11. 把一条记录的一个 Map 对象放入 5 准备的 List 中
                values.add(map);
            }
            return values;
        }
    
        /**
         * 获取结果集的 ColumnLabel 对应的 List
         * 
         * @param rs
         * @return
         * @throws SQLException
         */
        private List<String> getColumnLabels(ResultSet rs) throws SQLException {
            List<String> labels = new ArrayList<>();
    
            ResultSetMetaData rsmd = rs.getMetaData();
            for (int i = 0; i < rsmd.getColumnCount(); i++) {
                labels.add(rsmd.getColumnLabel(i + 1));
            }
    
            return labels;
        }
    
        // 返回某条记录的某一个字段的值 或 一个统计的值(一共有多少条记录等.)
        public <E> E getForValue(String sql, Object... args) {
            
            //1. 得到结果集: 该结果集应该只有一行, 且只有一列
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
    
            try {
                //1. 得到结果集
                connection = JDBCTools.getConnection();
                preparedStatement = connection.prepareStatement(sql);
    
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
    
                resultSet = preparedStatement.executeQuery();
                
                if(resultSet.next()){
                    return (E) resultSet.getObject(1);
                }
            } catch(Exception ex){
                ex.printStackTrace();
            } finally{
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
            //2. 取得结果
            
            return null;
        }
    
    }

    取得数据库自动生成的主键

    通过.getGeneratedKeys()获取包含了新生成主键的ResultSet对象

    ResultSet中只有一列GENERATED_KEY,用于存放新生成的主键值

    示例:

    Connection conn = JdbcUtil.getConnection();
    
    String sql = "insert into user(name,password,email,birthday) 
                values('abc','123','abc@sina.com','1978-08-08')";
    PreparedStatement st = conn.
                prepareStatement(sql,Statement.RETURN_GENERATED_KEYS );
    
    st.executeUpdate();
    ResultSet rs = st.getGeneratedKeys();  //得到插入行的主键
    if(rs.next())
        System.out.println(rs.getObject(1));

    七、JDBC处理BLOB

    Oracle LOB

    LOB,即Large Objects(大对象),是用来存储大量的二进制和文本数据的一种数据类型(一个LOB字段可存储可多达4GB的数据)。
    LOB 分为两种类型:内部LOB和外部LOB。

    • 内部LOB将数据以字节流的形式存储在数据库的内部。因而,内部LOB的许多操作都可以参与事务,也可以像处理普通数据一样对其进行备份和恢复操作。Oracle支持三种类型的内部LOB:
      • BLOB(二进制数据)  
        CLOB(单字节字符数据) 
        NCLOB(多字节字符数据)。
    • CLOB和NCLOB类型适用于存储超长的文本数据,BLOB字段适用于存储大量的二进制数据,如图像、视频、音频,文件等。
    • 目前只支持一种外部LOB类型,即BFILE类型。在数据库内,该类型仅存储数据在操作系统中的位置信息,而数据的实体以外部文件的形式存在于操作系统的文件系统中。因而,该类型所表示的数据是只读的,不参与事务。该类型可帮助用户管理大量的由外部程序访问的文件。

    MySQL BLOB 类型介绍 

    MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据。MySQL的四种BLOB类型(除了在存储的最大信息量上不同外,他们是等同的)。

     

    实际使用中根据需要存入的数据大小定义不同的BLOB类型。需要注意的是:如果存储的文件过大,数据库的性能会下降。

        /**
         * 读取 blob 数据: 
         * 1. 使用 getBlob 方法读取到 Blob 对象
         * 2. 调用 Blob 的 getBinaryStream() 方法得到输入流。再使用 IO 操作即可. 
         */
        @Test
        public void readBlob(){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT id, name customerName, email, birth, picture " 
                        + "FROM customers WHERE id = 13";
                preparedStatement = connection.prepareStatement(sql);
                resultSet = preparedStatement.executeQuery();
                
                if(resultSet.next()){
                    int id = resultSet.getInt(1);
                    String name = resultSet.getString(2);
                    String email = resultSet.getString(3);
                    
                    System.out.println(id + ", " + name  + ", " + email);
                    Blob picture = resultSet.getBlob(5);
                    
                    InputStream in = picture.getBinaryStream();
                    System.out.println(in.available()); 
                    
                    OutputStream out = new FileOutputStream("flower.jpg");
                    
                    byte [] buffer = new byte[1024];
                    int len = 0;
                    while((len = in.read(buffer)) != -1){
                        out.write(buffer, 0, len);
                    }
                    
                    in.close();
                    out.close();
                }
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
        }
        
        /**
         * 插入 BLOB 类型的数据必须使用 PreparedStatement:因为 BLOB 类型
         * 的数据时无法使用字符串拼写的。
         * 
         * 调用 setBlob(int index, InputStream inputStream)
         */
        @Test
        public void testInsertBlob(){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "INSERT INTO customers(name, email, birth, picture)" 
                        + "VALUES(?,?,?,?)";
                preparedStatement = connection.prepareStatement(sql);
                
                preparedStatement.setString(1, "ABCDE");
                preparedStatement.setString(2, "abcde@atguigu.com");
                preparedStatement.setDate(3, 
                        new Date(new java.util.Date().getTime()));
                
                InputStream inputStream = new FileInputStream("Hydrangeas.jpg");
                preparedStatement.setBlob(4, inputStream);
                
                preparedStatement.executeUpdate();
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, preparedStatement, connection);
            }
        }

    使用JDBC来写入Blob型数据到Oracle中

    Oracle的Blob字段比long字段的性能要好,可以用来保存如图片之类的二进制数据。Oracle的BLOB字段由两部分组成:数据(值)和指向数据的指针(定位器)。尽管值与表自身一起存储,但是一个BLOB列并不包含值,仅有它的定位指针。为了使用大对象,程序必须声明定位器类型的本地变量。当Oracle内部LOB被创建时,定位器被存放在列中,值被存放在LOB段中,LOB段是在数据库内部表的一部分。因为Blob自身有一个cursor,当写入Blob字段必须使用指针(定位器)对Blob进行操作,因而在写入Blob之前,必须获得指针(定位器)才能进行写入。如何获得Blob的指针(定位器) :需要先插入一个empty的blob,这将创建一个blob的指针,然后再把这个empty的blob的指针查询出来,这样通过两步操作,就获得了blob的指针,可以真正的写入blob数据了。

    步骤

    1、插入空blob
    insert into javatest(name,content) values(?,empty_blob());

    2、获得blob的cursor
    select content from javatest where name= ? for update;
    注意: 须加for update,锁定该行,直至该行被修改完毕,保证不产生并发冲突。

    3、利用 io,和获取到的cursor往数据库写数据流

    八、JDBC和数据库事务

    在数据库中,所谓事务是指一组逻辑操作单元,使数据从一种状态变换到另一种状态。为确保数据库中数据的一致性,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可以保持,而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态。
    事务的操作:先定义开始一个事务,然后对数据作修改操作,这时如果提交(COMMIT),这些修改就永久地保存下来,如果回退(ROLLBACK),数据库管理系统将放弃所作的所有修改而回到开始事务时的状态。

    事务的ACID(acid)属性

    • 1. 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。 
    • 2. 一致性(Consistency)事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
    • 3. 隔离性(Isolation)事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
    • 4. 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。

    JDBC事务处理

    事务:指构成单个逻辑工作单元的操作集合
    事务处理:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),要么整个事务回滚(rollback)到最初状态
    当一个连接对象被创建时,默认情况下是自动提交事务:每次执行一个 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚
    为了让多个 SQL 语句作为一个事务执行:

    • 调用 Connection 对象的 setAutoCommit(false); 以取消自动提交事务
    • 在所有的 SQL 语句都成功执行后,调用 commit(); 方法提交事务
    • 在出现异常时,调用 rollback(); 方法回滚事务
    • 若此时 Connection 没有被关闭, 则需要恢复其自动提交状态
    /**
         * Tom 给 Jerry 汇款 500 元.
         * 
         * 关于事务: 1. 如果多个操作, 每个操作使用的是自己的单独的连接, 则无法保证事务. 2. 具体步骤: 1). 事务操作开始前, 开始事务:
         * 取消 Connection 的默认提交行为. connection.setAutoCommit(false); 2). 如果事务的操作都成功,
         * 则提交事务: connection.commit(); 3). 回滚事务: 若出现异常, 则在 catch 块中回滚事务:
         */
        @Test
        public void testTransaction() {
    
            Connection connection = null;
    
            try {
    
                connection = JDBCTools.getConnection();
                System.out.println(connection.getAutoCommit());
    
                // 开始事务: 取消默认提交.
                connection.setAutoCommit(false);
    
                String sql = "UPDATE users SET balance = "
                        + "balance - 500 WHERE id = 1";
                update(connection, sql);
    
                int i = 10 / 0;
                System.out.println(i);
    
                sql = "UPDATE users SET balance = " + "balance + 500 WHERE id = 2";
                update(connection, sql);
    
                // 提交事务
                connection.commit();
            } catch (Exception e) {
                e.printStackTrace();
    
                // 回滚事务
                try {
                    connection.rollback();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            } finally {
                JDBCTools.releaseDB(null, null, connection);
            }
    
            /*
             * try {
             * 
             * //开始事务: 取消默认提交. connection.setAutoCommit(false);
             * 
             * //...
             * 
             * //提交事务 connection.commit(); } catch (Exception e) { //...
             * 
             * //回滚事务 try { connection.rollback(); } catch (SQLException e1) {
             * e1.printStackTrace(); } } finally{ JDBCTools.releaseDB(null, null,
             * connection); }
             */
    
            // DAO dao = new DAO();
            //
            // String sql = "UPDATE users SET balance = " +
            // "balance - 500 WHERE id = 1";
            // dao.update(sql);
            //
            // int i = 10 / 0;
            // System.out.println(i);
            //
            // sql = "UPDATE users SET balance = " +
            // "balance + 500 WHERE id = 2";
            // dao.update(sql);
    
        }
    
        public void update(Connection connection, String sql, Object... args) {
            PreparedStatement preparedStatement = null;
    
            try {
                preparedStatement = connection.prepareStatement(sql);
    
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
    
                preparedStatement.executeUpdate();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                JDBCTools.releaseDB(null, preparedStatement, null);
            }
        }

    数据库的隔离级别

    对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题:

    • 脏读: 对于两个事物 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段. 之后, 若 T2 回滚, T1读取的内容就是临时且无效的.
    • 不可重复读: 对于两个事物 T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段. 之后, T1再次读取同一个字段, 值就不同了.
    • 幻读: 对于两个事物 T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行. 之后, 如果 T1 再次读取同一个表, 就会多出几行.

    数据库事务的隔离性:数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问题.
    一个事务与其他事务隔离的程度称为隔离级别. 数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱

    数据库的隔离级别

    数据库提供的 4 种事务隔离级别:

        /**
         * 测试事务的隔离级别 在 JDBC 程序中可以通过 Connection 的 setTransactionIsolation 来设置事务的隔离级别.
         */
        @Test
        public void testTransactionIsolationUpdate() {
            
            Connection connection = null;
    
            try {
                connection = JDBCTools.getConnection();
                connection.setAutoCommit(false);
                
                String sql = "UPDATE users SET balance = "
                        + "balance - 500 WHERE id = 1";
                update(connection, sql);
                
                connection.commit();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
    
            }
        }
        
        @Test
        public void testTransactionIsolationRead() {
            String sql = "SELECT balance FROM users WHERE id = 1";
            Integer balance = getForValue(sql);
            System.out.println(balance); 
        }
    
        // 返回某条记录的某一个字段的值 或 一个统计的值(一共有多少条记录等.)
        public <E> E getForValue(String sql, Object... args) {
    
            // 1. 得到结果集: 该结果集应该只有一行, 且只有一列
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
    
            try {
                // 1. 得到结果集
                connection = JDBCTools.getConnection();
                System.out.println(connection.getTransactionIsolation()); 
                
    //            connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
                connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                
                preparedStatement = connection.prepareStatement(sql);
    
                for (int i = 0; i < args.length; i++) {
                    preparedStatement.setObject(i + 1, args[i]);
                }
    
                resultSet = preparedStatement.executeQuery();
    
                if (resultSet.next()) {
                    return (E) resultSet.getObject(1);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                JDBCTools.releaseDB(resultSet, preparedStatement, connection);
            }
            // 2. 取得结果
    
            return null;
        }

    Oracle 支持的 2 种事务隔离级别:READ COMMITED, SERIALIZABLE. Oracle 默认的事务隔离级别为: READ COMMITED
    Mysql 支持 4 中事务隔离级别. Mysql 默认的事务隔离级别为: REPEATABLE READ

    在MySQL中设置隔离级别

    每启动一个 mysql 程序, 就会获得一个单独的数据库连接. 每个数据库连接都有一个全局变量 @@tx_isolation, 表示当前的事务隔离级别. MySQL 默认的隔离级别为 Repeatable Read
    查看当前的隔离级别: SELECT @@tx_isolation;
    设置当前 mySQL 连接的隔离级别:

    set transaction isolation level read committed;

    设置数据库系统的全局的隔离级别:

    set global transaction isolation level read committed;

    九、批量处理JDBC语句提高处理速度

    当需要成批插入或者更新记录时。可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率
    JDBC的批量处理语句包括下面两个方法:

    • addBatch(String):添加需要批量处理的SQL语句或是参数;
    • executeBatch();执行批量处理语句;

    通常我们会遇到两种批量执行SQL语句的情况:

    • 多条SQL语句的批量处理;
    • 一个SQL语句的批量传参;

    多条SQL语句的批量处理

    一个SQL语句的批量传参

    情景:

    解决:

    @Test
        public void testBatch(){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            String sql = null;
            
            try {
                connection = JDBCTools.getConnection();
                JDBCTools.beginTx(connection);
                sql = "INSERT INTO customers VALUES(?,?,?)";
                preparedStatement = connection.prepareStatement(sql);
                Date date = new Date(new java.util.Date().getTime());
                
                long begin = System.currentTimeMillis();
                for(int i = 0; i < 100000; i++){
                    preparedStatement.setInt(1, i + 1);
                    preparedStatement.setString(2, "name_" + i);
                    preparedStatement.setDate(3, date);
                    
                    //"积攒" SQL 
                    preparedStatement.addBatch();
                    
                    //当 "积攒" 到一定程度, 就统一的执行一次. 并且清空先前 "积攒" 的 SQL
                    if((i + 1) % 300 == 0){
                        preparedStatement.executeBatch();
                        preparedStatement.clearBatch();
                    }
                }
                
                //若总条数不是批量数值的整数倍, 则还需要再额外的执行一次. 
                if(100000 % 300 != 0){
                    preparedStatement.executeBatch();
                    preparedStatement.clearBatch();
                }
                
                long end = System.currentTimeMillis();
                
                System.out.println("Time: " + (end - begin)); //569
                
                JDBCTools.commit(connection);
            } catch (Exception e) {
                e.printStackTrace();
                JDBCTools.rollback(connection);
            } finally{
                JDBCTools.releaseDB(null, preparedStatement, connection);
            }
        }
        
    
        @Test
        public void testBatchWithPreparedStatement(){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            String sql = null;
            
            try {
                connection = JDBCTools.getConnection();
                JDBCTools.beginTx(connection);
                sql = "INSERT INTO customers VALUES(?,?,?)";
                preparedStatement = connection.prepareStatement(sql);
                Date date = new Date(new java.util.Date().getTime());
                
                long begin = System.currentTimeMillis();
                for(int i = 0; i < 100000; i++){
                    preparedStatement.setInt(1, i + 1);
                    preparedStatement.setString(2, "name_" + i);
                    preparedStatement.setDate(3, date);
                    
                    preparedStatement.executeUpdate();
                }
                long end = System.currentTimeMillis();
                
                System.out.println("Time: " + (end - begin)); //9819
                
                JDBCTools.commit(connection);
            } catch (Exception e) {
                e.printStackTrace();
                JDBCTools.rollback(connection);
            } finally{
                JDBCTools.releaseDB(null, preparedStatement, connection);
            }
        }
        
        /**
         * 向  Oracle 的 customers 数据表中插入 10 万条记录
         * 测试如何插入, 用时最短. 
         * 1. 使用 Statement.
         */
        @Test
        public void testBatchWithStatement(){
            Connection connection = null;
            Statement statement = null;
            String sql = null;
            
            try {
                connection = JDBCTools.getConnection();
                JDBCTools.beginTx(connection);
                
                statement = connection.createStatement();
                
                long begin = System.currentTimeMillis();
                for(int i = 0; i < 100000; i++){
                    sql = "INSERT INTO customers VALUES(" + (i + 1) 
                            + ", 'name_" + i + "', '29-6月 -13')";
                    statement.addBatch(sql);
                }
                long end = System.currentTimeMillis();
                
                System.out.println("Time: " + (end - begin)); //39567
                
                JDBCTools.commit(connection);
            } catch (Exception e) {
                e.printStackTrace();
                JDBCTools.rollback(connection);
            } finally{
                JDBCTools.releaseDB(null, statement, connection);
            }
        }

    十、数据库连接池

    JDBC数据库连接池必要性

    在使用开发基于数据库的web程序时,传统的模式基本是按以下步骤:  

    • 在主程序(如servlet、beans)中建立数据库连接。
    • 进行sql操作
    • 断开数据库连接。

    这种模式开发,存在的问题:

    • 普通的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码(得花费0.05s~1s的时间)。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有得到很好的重复利用.若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃。
    • 对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。
    • 这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。

    为解决传统开发中的数据库连接问题,可以采用数据库连接池技术
    数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。
    数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
    数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

    数据库连接池的工作原理

    优点

    资源重用:

    • 由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增加了系统运行环境的平稳性。

    更快的系统反应速度

    • 数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间

    新的资源分配手段

    • 对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源

    统一的连接管理,避免数据库连接泄露

    • 在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄露

    两种开源的数据库连接池

    JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:

    • DBCP 数据库连接池
    • C3P0 数据库连接池

    DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接池

    DBCP数据源

    DBCP 是 Apache 软件基金组织下的开源连接池实现,该连接池依赖该组织下的另一个开源系统:Common-pool. 如需使用该连接池实现,应在系统中增加如下两个 jar 文件:

    • Commons-dbcp.jar:连接池的实现
    • Commons-pool.jar:连接池实现的依赖库

    Tomcat 的连接池正是采用该连接池来实现的。该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。

    使用范例

    数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但上面的代码并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。

    C3P0数据源

        @Test
        public void testJdbcTools() throws Exception{
            Connection connection = JDBCTools.getConnection();
            System.out.println(connection); 
        }
        
        /**
         * 1. 创建 c3p0-config.xml 文件, 
         * 参考帮助文档中 Appendix B: Configuation Files 的内容
         * 2. 创建 ComboPooledDataSource 实例;
         * DataSource dataSource = 
         *            new ComboPooledDataSource("helloc3p0");  
         * 3. 从 DataSource 实例中获取数据库连接. 
         */
        @Test
        public void testC3poWithConfigFile() throws Exception{
            DataSource dataSource = 
                    new ComboPooledDataSource("helloc3p0");  
            
            System.out.println(dataSource.getConnection()); 
            
            ComboPooledDataSource comboPooledDataSource = 
                    (ComboPooledDataSource) dataSource;
            System.out.println(comboPooledDataSource.getMaxStatements()); 
        }
        
        @Test
        public void testC3P0() throws Exception{
            ComboPooledDataSource cpds = new ComboPooledDataSource();
            cpds.setDriverClass( "com.mysql.jdbc.Driver" ); //loads the jdbc driver            
            cpds.setJdbcUrl( "jdbc:mysql:///atguigu" );
            cpds.setUser("root");                                  
            cpds.setPassword("1230");   
            
            System.out.println(cpds.getConnection()); 
        }
        
        /**
         * 1. 加载 dbcp 的 properties 配置文件: 配置文件中的键需要来自 BasicDataSource
         * 的属性.
         * 2. 调用 BasicDataSourceFactory 的 createDataSource 方法创建 DataSource
         * 实例
         * 3. 从 DataSource 实例中获取数据库连接. 
         */
        @Test
        public void testDBCPWithDataSourceFactory() throws Exception{
            
            Properties properties = new Properties();
            InputStream inStream = JDBCTest.class.getClassLoader()
                    .getResourceAsStream("dbcp.properties");
            properties.load(inStream);
            
            DataSource dataSource = 
                    BasicDataSourceFactory.createDataSource(properties);
            
            System.out.println(dataSource.getConnection()); 
            
    //        BasicDataSource basicDataSource = 
    //                (BasicDataSource) dataSource;
    //        
    //        System.out.println(basicDataSource.getMaxWait()); 
        }
        
        /**
         * 使用 DBCP 数据库连接池
         * 1. 加入 jar 包(2 个jar 包). 依赖于 Commons Pool
         * 2. 创建数据库连接池
         * 3. 为数据源实例指定必须的属性
         * 4. 从数据源中获取数据库连接
         * @throws SQLException 
         */
        @Test
        public void testDBCP() throws SQLException{
            final BasicDataSource dataSource = new BasicDataSource();
            
            //2. 为数据源实例指定必须的属性
            dataSource.setUsername("root");
            dataSource.setPassword("1230");
            dataSource.setUrl("jdbc:mysql:///atguigu");
            dataSource.setDriverClassName("com.mysql.jdbc.Driver");
            
            //3. 指定数据源的一些可选的属性.
            //1). 指定数据库连接池中初始化连接数的个数
            dataSource.setInitialSize(5);
            
            //2). 指定最大的连接数: 同一时刻可以同时向数据库申请的连接数
            dataSource.setMaxActive(5);
            
            //3). 指定小连接数: 在数据库连接池中保存的最少的空闲连接的数量 
            dataSource.setMinIdle(2);
            
            //4).等待数据库连接池分配连接的最长时间. 单位为毫秒. 超出该时间将抛出异常. 
            dataSource.setMaxWait(1000 * 5);
            
            //4. 从数据源中获取数据库连接,池中有5个连接,连续获取了5个,下面有一个线程等待三秒释放一个连接,于是另一个线程可以获取新的连接,如果设置为5500,超过了MaxWait,但是池子仍是满的,没法获取新连接,会抛异常
            Connection connection = dataSource.getConnection();
            System.out.println(connection.getClass()); 
            
            connection = dataSource.getConnection();
            System.out.println(connection.getClass()); 
            
            connection = dataSource.getConnection();
            System.out.println(connection.getClass()); 
            
            connection = dataSource.getConnection();
            System.out.println(connection.getClass()); 
            
            Connection connection2 = dataSource.getConnection();
            System.out.println(">" + connection2.getClass()); 
            
            new Thread(){
                public void run() {
                    Connection conn;
                    try {
                        conn = dataSource.getConnection();
                        System.out.println(conn.getClass()); 
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                };
            }.start();
            
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            connection2.close();
        }

    十一、Apache-DBUtils

    简介

    commons-dbutils 是 Apache 组织提供的一个开源 JDBC工具类库,它是对JDBC的简单封装,学习成本极低,并且使用dbutils能极大简化jdbc编码的工作量,同时也不会影响程序的性能。
    API介绍:

    org.apache.commons.dbutils.QueryRunner
    org.apache.commons.dbutils.ResultSetHandler

    工具类

    org.apache.commons.dbutils.DbUtils

    DbUtils类

    DbUtils :提供如关闭连接、装载JDBC驱动程序等常规工作的工具类,里面的所有方法都是静态的。主要方法如下:

    • public static void close(…) throws java.sql.SQLException:DbUtils类提供了三个重载的关闭方法。这些方法检查所提供的参数是不是NULL,如果不是的话,它们就关闭Connection、Statement和ResultSet。
    • public static void closeQuietly(…): 这一类方法不仅能在Connection、Statement和ResultSet为NULL情况下避免关闭,还能隐藏一些在程序中抛出的SQLEeception。
    • public static void commitAndCloseQuietly(Connection conn):用来提交连接,然后关闭连接,并且在关闭连接时不抛出SQL异常。
    • public static boolean loadDriver(java.lang.String driverClassName):这一方装载并注册JDBC驱动程序,如果成功就返回true。使用该方法,你不需要捕捉这个异常ClassNotFoundException。

    QueryRunner类

    该类简单化了SQL查询,它与ResultSetHandler组合在一起使用可以完成大部分的数据库操作,能够大大减少编码量。
    QueryRunner类提供了两个构造方法:

    • 默认的构造方法
    • 需要一个 javax.sql.DataSource 来作参数的构造方法。

     主要方法:

    public Object query(Connection conn, String sql, Object[] params, ResultSetHandler rsh) throws SQLException:执行一个查询操作,在这个查询中,对象数组中的每个元素值被用来作为查询语句的置换参数。该方法会自行处理 PreparedStatement 和 ResultSet 的创建和关闭。
    public Object query(String sql, Object[] params, ResultSetHandler rsh) throws SQLException: 几乎与第一种方法一样;唯一的不同在于它不将数据库连接提供给方法,并且它是从提供给构造方法的数据源(DataSource) 或使用的setDataSource 方法中重新获得 Connection。
    public Object query(Connection conn, String sql, ResultSetHandler rsh) throws SQLException : 执行一个不需要置换参数的查询操作。
    public int update(Connection conn, String sql, Object[] params) throws SQLException:用来执行一个更新(插入、更新或删除)操作。
    public int update(Connection conn, String sql) throws SQLException:用来执行一个不需要置换参数的更新操作。

    QueryRunner queryRunner = new QueryRunner();
    
        class MyResultSetHandler implements ResultSetHandler{
    
            @Override
            public Object handle(ResultSet resultSet) 
                    throws SQLException {
    //            System.out.println("handle....");
    //            return "atguigu";
                
                List<Customer> customers = new ArrayList<>();
                
                while(resultSet.next()){
                    Integer id = resultSet.getInt(1);
                    String name = resultSet.getString(2);
                    String email = resultSet.getString(3);
                    Date birth = resultSet.getDate(4);
                    
                    Customer customer = 
                            new Customer(id, name, email, birth);
                    customers.add(customer);
                }
                
                return customers; // 这是query方法返回的东西
            }
            
        }
        
        /**
         * QueryRunner 的 query 方法的返回值取决于其 ResultSetHandler 参数的
         * handle 方法的返回值
         * 
         */
        @Test
        public void testQuery(){
            Connection connection = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT id, name, email, birth " +
                        "FROM customers";
                Object obj = queryRunner.query(connection, sql, 
                                new MyResultSetHandler());
                
                System.out.println(obj); 
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, null, connection);
            }
        }


    queryRunner.query(connection, sql, new ResultSetHandler());

     返回的结果取决于ResultSetHandler,准确来讲是其中的handle方法,该方法接收了sql执行的resultSet,可以根据resultSet获得sql执行的结果进行处理,返回想返回的内容

    所以这个Handler很重要

    ResultSetHandler接口

    该接口用于处理 java.sql.ResultSet,将数据按要求转换为另一种形式。ResultSetHandler 接口提供了一个单独的方法:Object handle (java.sql.ResultSet .rs)。

    ResultSetHandler 接口的实现类

    ArrayHandler:把结果集中的第一行数据转成对象数组。
    ArrayListHandler:把结果集中的每一行数据都转成一个数组,再存放到List中。
    BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中。

        /**
         * BeanHandler: 把结果集的第一条记录转为创建 BeanHandler 对象时传入的 Class
         * 参数对应的对象. 
         */
        @Test
        public void testBeanHanlder(){
            Connection connection = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT id, name customerName, email, birth " +
                        "FROM customers WHERE id >= ?";
                
                Customer customer = queryRunner.query(connection, 
                        sql, new BeanHandler(Customer.class), 5);
                
                System.out.println(customer);
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, null, connection);
            }
        }


    BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。

    /**
         * BeanListHandler: 把结果集转为一个 List, 该 List 不为 null, 但可能为
         * 空集合(size() 方法返回 0)
         * 若 SQL 语句的确能够查询到记录, List 中存放创建 BeanListHandler 传入的 Class
         * 对象对应的对象. 
         */
        @Test
        public void testBeanListHandler(){
            Connection connection = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT id, name, email, birth " +
                        "FROM customers";
                
                List<Customer> customers = queryRunner.query(connection, 
                        sql, new BeanListHandler(Customer.class));
                
                System.out.println(customers);
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, null, connection);
            }
        }

    ColumnListHandler:将结果集中某一列的数据存放到List中。


    KeyedHandler(name):将结果集中的每一行数据都封装到一个Map里,再把这些map再存到一个map里,其key为指定的key。
    MapHandler:将结果集中的第一行数据封装到一个Map里,key是列名,value就是对应的值。

        /**
         * MapHandler: 返回 SQL 对应的第一条记录对应的 Map 对象.
         * 键: SQL 查询的列名(不是列的别名), 值: 列的值. 
         */
        @Test
        public void testMapHandler(){
            Connection connection = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT id, name, email, birth " +
                        "FROM customers";
                
                Map<String, Object> result = queryRunner.query(connection, 
                        sql, new MapHandler());
                
                System.out.println(result);
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, null, connection);
            }
        }

    MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List

    /**
         * MapListHandler: 将结果集转为一个 Map 的 List
         * Map 对应查询的一条记录: 键: SQL 查询的列名(不是列的别名), 值: 列的值. 
         * 而 MapListHandler: 返回的多条记录对应的 Map 的集合. 
         */
        @Test
        public void testMapListHandler(){
            Connection connection = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT id, name, email, birth " +
                        "FROM customers";
                
                List<Map<String, Object>> result = queryRunner.query(connection, 
                        sql, new MapListHandler());
                
                System.out.println(result);
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, null, connection);
            }
        }

    ScalarHandler

        /**
         * ScalarHandler: 把结果集转为一个数值(可以是任意基本数据类型和字符串, Date 等)返回
         */
        @Test
        public void testScalarHandler(){
            Connection connection = null;
            
            try {
                connection = JDBCTools.getConnection();
                String sql = "SELECT name, email " +
                        "FROM customers";
                
                Object result = queryRunner.query(connection, 
                        sql, new ScalarHandler());
                
                System.out.println(result);
            } catch (Exception e) {
                e.printStackTrace();
            } finally{
                JDBCTools.releaseDB(null, null, connection);
            }
        }

    十二、编写通用DAO

    接口

    /**
     * 访问数据的 DAO 接口. 
     * 里边定义好访问数据表的各种方法
     * @param T: DAO 处理的实体类的类型. 
     */
    public interface DAO<T> {
    
        /**
         * 批量处理的方法
         * @param connection
         * @param sql
         * @param args: 填充占位符的 Object [] 类型的可变参数.
         * @throws SQLException 
         */  
        void batch(Connection connection, 
                String sql, Object [] ... args) throws SQLException;
        
        /**
         * 返回具体的一个值, 例如总人数, 平均工资, 某一个人的 email 等.
         * @param connection
         * @param sql
         * @param args
         * @return
         * @throws SQLException 
         */
        <E> E getForValue(Connection connection,
                String sql, Object ... args) throws SQLException;
        
        /**
         * 返回 T 的一个集合
         * @param connection
         * @param sql
         * @param args
         * @return
         * @throws SQLException 
         */
        List<T> getForList(Connection connection,
                String sql, Object ... args) throws SQLException;
        
        /**
         * 返回一个 T 的对象
         * @param connection
         * @param sql
         * @param args
         * @return
         * @throws SQLException 
         */
        T get(Connection connection, String sql, 
                Object ... args) throws SQLException;
        
        /**
         * INSRET, UPDATE, DELETE
         * @param connection: 数据库连接
         * @param sql: SQL 语句
         * @param args: 填充占位符的可变参数.
         * @throws SQLException 
         */
        void update(Connection connection, String sql, 
                Object ... args) throws SQLException;
    
    }

    实现类

    /**
     * 使用 QueryRunner 提供其具体的实现
     * @param <T>: 子类需传入的泛型类型. 
     */
    public class JdbcDaoImpl<T> implements DAO<T> {
    
        private QueryRunner queryRunner = null;
        private Class<T> type;
        
        public JdbcDaoImpl() {
            queryRunner = new QueryRunner();
            type = ReflectionUtils.getSuperGenericType(getClass());
        }
        
        @Override
        public void batch(Connection connection, String sql, Object[]... args) throws SQLException {
            queryRunner.batch(connection, sql, args);
        }
    
        @Override
        public <E> E getForValue(Connection connection, String sql, Object... args) throws SQLException {
            return (E) queryRunner.query(connection, sql, new ScalarHandler(), args);
        }
    
        @Override 
        public List<T> getForList(Connection connection, String sql, Object... args) 
                throws SQLException {
            return queryRunner.query(connection, sql, 
                    new BeanListHandler<>(type), args);
        }
    
        @Override
        public T get(Connection connection, String sql, Object... args) throws SQLException { 
            return queryRunner.query(connection, sql, 
                    new BeanHandler<>(type), args);
        }
    
        @Override
        public void update(Connection connection, String sql, Object... args) throws SQLException {
            queryRunner.update(connection, sql, args);
        }
        
    }

    十三、JDBC调用函数和存储过程

    public class JDBCTest {
    
        /**
         * 如何使用 JDBC 调用存储在数据库中的函数或存储过程
         */
        @Test
        public void testCallableStatment() {
    
            Connection connection = null;
            CallableStatement callableStatement = null;
    
            try {
                connection = JDBCTools.getConnection();
    
                // 1. 通过 Connection 对象的 prepareCall()
                // 方法创建一个 CallableStatement 对象的实例.
                // 在使用 Connection 对象的 preparedCall() 方法时,
                // 需要传入一个 String 类型的字符串, 该字符串用于指明如何调用存储过程.
                String sql = "{?= call sum_salary(?, ?)}";
                callableStatement = connection.prepareCall(sql);
    
                // 2. 通过 CallableStatement 对象的 
                //reisterOutParameter() 方法注册 OUT 参数.
                callableStatement.registerOutParameter(1, Types.NUMERIC);
                callableStatement.registerOutParameter(3, Types.NUMERIC);
                
                // 3. 通过 CallableStatement 对象的 setXxx() 方法设定 IN 或 IN OUT 参数. 若想将参数默认值设为
                // null, 可以使用 setNull() 方法.
                callableStatement.setInt(2, 80);
                
                // 4. 通过 CallableStatement 对象的 execute() 方法执行存储过程
                callableStatement.execute();
                
                // 5. 如果所调用的是带返回参数的存储过程, 
                //还需要通过 CallableStatement 对象的 getXxx() 方法获取其返回值.
                double sumSalary = callableStatement.getDouble(1);
                long empCount = callableStatement.getLong(3);
                
                System.out.println(sumSalary);
                System.out.println(empCount);
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                JDBCTools.releaseDB(null, callableStatement, connection);
            }
        }
    
        @Test
        public void testC3P0() throws SQLException {
            DataSource dataSource = new ComboPooledDataSource("c3p0");
    
            System.out.println(dataSource.getConnection());
        }
    
    }
  • 相关阅读:
    快速排序算法(c#)
    NHibernate 中createSqlQuery的执行
    Asp.net页面下客户端按钮提交页面到其他Action
    希尔(插入)排序 c#代码
    Asp.net MVC 中冒号的作用
    Net注册JS的几种方式和区别
    Asp.net MVC 使用json数据格式交互
    DataSet的手工创建
    反射基础
    uva10082 WERTYU
  • 原文地址:https://www.cnblogs.com/aidata/p/12021854.html
Copyright © 2011-2022 走看看