木心

毕竟几人真得鹿,不知终日梦为鱼

导航

JDBC

录:

1. 获取数据库连接
2. 使用 Statement 执行更新操作
3. 使用 ResultSet 执行查询操作
4. 使用 PreparedStatement
5. 利用反射及 JDBC 元数据编写通用的查询方法
6. DAO 设计模式
7. JDBC 元数据
8. 获取插入记录的主键值
9. 处理 Blob
10. 处理事务 & 事务的隔离级别
11. 批量处理
12. 数据库连接池 & C3P0 & DBCP
13. 使用 DBUtils
14. 使用dbutils编写通用的DAO
15. 使用 JDBC 调用函数 & 存储过程

 

  数据持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以“固化”,而持久化的实现过程大多通过各种关系数据库来实现。 持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中。

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

 

1. 获取数据库连接    <--返回目录

1.1、通过 Driver 接口获取数据库连接

  java.sql.Driver 接口是所有 JDBC 驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供了不同的实现。

  在程序中不需要直接去访问实现了Driver接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现。

/**
 * Driver 是一个接口: 数据库厂商必须提供实现的接口. 能从其中获取数据库连接. 可以通过 Driver 的实现类对象获取数据库连接.
 * 1. 加入 mysql 驱动
 * 1). 解压 mysql-connector-java-5.1.7.zip
 * 2). 在当前项目下新建 lib 目录
 * 3). 把 mysql-connector-java-5.1.7-bin.jar 复制到 lib 目录下
 * 4). 右键 build-path , add to buildpath 加入到类路径下
 */
@Test
public void testDriver() throws SQLException {
    //1. 创建一个 Driver 实现类的对象
    Driver driver = new com.mysql.jdbc.Driver();

    //2. 准备连接数据库的基本信息: url, user, password
    String url = "jdbc:mysql://localhost:3306/test";
    Properties info = new Properties();
    info.put("user", "root");
    info.put("password", "123456");

    //3. 调用 Driver 接口的 connect(url, info) 获取数据库连接
    Connection connection = driver.connect(url, info);
    System.out.println(connection);
}

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

    //通过反射创建 Driver 对象
    Driver driver = (Driver) Class.forName(driverClass).newInstance();

    Properties info = new Properties();
    info.put("user", user);
    info.put("password", password);

    //通过 Driver 的 connect 方法获取数据库连接
    Connection connection = driver.connect(jdbcUrl, info);
    return connection;
}

@Test
public void testGetConnection() throws Exception{
    System.out.println(getConnection());
}

  在 src 目录下新建文件 jdbc.properties

#driver=oracle.jdbc.driver.OracleDriver
#jdbcUrl=jdbc:oracle:thin:@localhost:1521:orcl
#user=scott
#password=java

driver=com.mysql.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost:3306/test
user=root
password=123456

 

1.2、通过 DriverManager 获取数据库连接

@Test
public void testGetConnection2() throws Exception{
    System.out.println(getConnection2());
}

public Connection getConnection2() throws Exception{
    //1. 准备连接数据库的 4 个字符串.
    Properties properties = new Properties();
    InputStream in = this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");
    properties.load(in);

    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);
}

/**
 * DriverManager 是驱动的管理类.
 * 1). 可以通过重载的 getConnection() 方法获取数据库连接. 较为方便
 * 2). 可以同时管理多个驱动程序: 若注册了多个数据库连接, 则调用 getConnection()
 * 方法时传入的参数不同, 即返回不同的数据库连接。
 */
@Test
public void testDriverManager() throws Exception{
    //1. 准备连接数据库的 4 个字符串.
    String driverClass = "com.mysql.jdbc.Driver"; //驱动的全类名.
    String jdbcUrl = "jdbc:mysql:///test";
    String user = "root";
    String password = "123456";

    //2. 加载数据库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块.)
    Class.forName(driverClass);

    //3. 通过 DriverManager 的 getConnection() 方法获取数据库连接.
    Connection connection = DriverManager.getConnection(jdbcUrl, user, password);
    System.out.println(connection);
}


2. 使用 Statement 执行更新操作    <--返回目录

  sql脚本

CREATE TABLE `customers` (
`id` bigint NOT NULL auto_increment,
`name` varchar(50) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`birth` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
View Code

  

@Test
public void testUpdate() {
    update("UPDATE customers SET name = 'bbb' WHERE id = 2");
}

/**
 * 通用的更新的方法: 包括 INSERT、UPDATE、DELETE
 */
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 向指定的数据表中插入一条记录.
 * <p>
 * 1. Statement: 用于执行 SQL 语句的对象
 * 1). 通过 Connection 的 createStatement() 方法来获取
 * 2). 通过 executeUpdate(sql) 可以执行 SQL 语句.
 * 3). 传入的 SQL 可以是 INSRET, UPDATE 或 DELETE. 但不能是 SELECT
 * <p>
 * 2. Connection、Statement 都是应用程序和数据库服务器的连接资源. 使用后一定要关闭.
 * 需要在 finally 中关闭 Connection 和 Statement 对象.
 * <p>
 * 3. 关闭的顺序是: 先关闭后获取的. 即先关闭 Statement 后关闭 Connection
 */
@Test
public void testStatement() {
    //1. 获取数据库连接
    Connection conn = null;
    Statement statement = null;
    try {
        conn = JDBCTools.getConnection();
        //3. 准备插入的 SQL 语句
        String sql = null;
        sql = "INSERT INTO customers (NAME, EMAIL, BIRTH) VALUES('aaa', 'aaa@xxx.com', '1990-12-12')";
        //sql = "UPDATE customers SET name = 'bbb' WHERE id = 1";
        //sql = "DELETE FROM customers WHERE id = 1";
        System.out.println(sql);

        //4. 执行插入/更新/删除
        //获取操作 SQL 语句的 Statement 对象:
        statement = conn.createStatement();
        //调用 Statement 对象的 executeUpdate(sql) 执行 SQL 语句进行插入
        statement.executeUpdate(sql);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 关闭 Statement 对象 和 关闭连接
        JDBCTools.release(statement, conn);
    }
}

  JDBCTools

public class JDBCTools {

    public static void release(Statement statement, Connection conn) {
        if (statement != null) {
            try {
                statement.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static Connection getConnection() throws Exception {
        Properties properties = new Properties();
        InputStream in = JDBCTools.class.getClassLoader().getResourceAsStream("jdbc.properties");
        properties.load(in);

        String driver = properties.getProperty("driver");
        String jdbcUrl = properties.getProperty("jdbcUrl");
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");

        // 加载数据库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
        Class.forName(driver);
        // 通过 DriverManager 的 getConnection() 方法获取数据库连接
        return DriverManager.getConnection(jdbcUrl, user, password);
    }
}
View Code


3. 使用 ResultSet 执行查询操作    <--返回目录

  通过 ResultSet 执行查询操作

/**
 * 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=2 的 customers 数据表的记录, 并打印
    Connection conn = null;
    Statement statement = null;
    ResultSet rs = null;
    try {
        //1. 获取 Connection
        conn = JDBCTools.getConnection();
        //2. 获取 Statement
        statement = conn.createStatement();
        //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{
        JDBCTools.release(rs, statement, conn);
    }
}
View Code


4. 使用 PreparedStatement    <--返回目录

@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, "xxx");
        preparedStatement.setString(2, "xxx@163.com");
        preparedStatement.setDate(3, new java.sql.Date(new java.util.Date().getTime()));

        preparedStatement.executeUpdate();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        JDBCTools.release(null, preparedStatement, connection);
    }
}


5. 利用反射及 JDBC 元数据编写通用的查询方法    <--返回目录

  元数据 ResultSetMetaData

@Test
public void testResultSetMetaData() {
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    try {
        connection = JDBCTools.getConnection();
        String sql = "SELECT id, name as username, email, birth FROM customers WHERE id = ? OR id = ?";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, 2);
        preparedStatement.setInt(2, 3);
        resultSet = preparedStatement.executeQuery();
        ResultSetMetaData rsmd = resultSet.getMetaData();

        List<Map<String, Object>> values = new ArrayList<>();
        while(resultSet.next()){
            Map<String, Object> value = new HashMap<>();
            for(int i = 0; i < rsmd.getColumnCount(); i++){
                String columnLabel = rsmd.getColumnLabel(i + 1);
                Object columnValue = resultSet.getObject(columnLabel);
                value.put(columnLabel, columnValue);
            }
            values.add(value);
        }
        System.out.println(values);

        /*Class clazz = Student.class;
        Object object = clazz.newInstance();
        for(Map.Entry<String, Object> entry: values.entrySet()){
            String fieldName = entry.getKey();
            Object fieldValue = entry.getValue();
            System.out.println(fieldName + ": " + fieldValue);
            ReflectionUtils.setFieldValue(object, fieldName, fieldValue);
        }*/
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        JDBCTools.release(resultSet, preparedStatement, connection);
    }
}

 

6. DAO 设计模式    <--返回目录

  DAO:Data Access Object。why:实现功能的模块化,更有利于代码的维护和升级。 what:访问数据信息的类,包含了对数据的CRUD,而不包含任何业务相关的信息。 how:使用JDBC编写DAO可能会包含的方法。

  JavaBean:在JavaEE中,java类的属性通过getter、setter来定义,getXxx或setXxx后首字母小写即为java类的属性。而以前叫的那个属性,即成员变量,称之为字段。一般情况下,字段名和属性名都一致。

  操作java类的属性可以使用工具beanutils:commons-beanutils.jar+commons-logging.jar

  DAO封装

package com.oy;

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

import org.apache.commons.beanutils.BeanUtils;

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.release(null, preparedStatement, connection);
        }
    }

    // 查询一条记录, 返回对应的对象
    public <T> T get(Class<T> clazz, String sql, Object... args) {
        List<T> result = getForList(clazz, sql, args);
        return (result.size() > 0) ? result.get(0) : 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 = transferMapListToBeanList(clazz, values);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCTools.release(resultSet, preparedStatement, connection);
        }
        return list;
    }

    public <T> List<T> transferMapListToBeanList(Class<T> clazz, List<Map<String, Object>> values) throws Exception {
        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);
                }
                // 把 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 {
        // 准备一个 List<Map<String, Object>>:
        // 键: 存放列的别名, 值: 存放列的值. 其中一个 Map 对象对应着一条记录
        List<Map<String, Object>> values = new ArrayList<>();

        List<String> columnLabels = getColumnLabels(resultSet);
        Map<String, Object> map = null;

        // 处理 ResultSet, 使用 while 循环
        while (resultSet.next()) {
            map = new HashMap<>();

            for (String columnLabel : columnLabels) {
                Object value = resultSet.getObject(columnLabel);
                map.put(columnLabel, value);
            }

            // 把一条记录的一个 Map 对象放入准备的 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 {
            //得到结果集
            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.release(resultSet, preparedStatement, connection);
        }
        return null;
    }
}
View Code

  测试

package com.oy;

import java.sql.Date;
import java.util.List;
import org.junit.Test;

public class DAOTest {
    DAO dao = new DAO();

    @Test
    public void testUpdate() {
        String sql = "INSERT INTO customers(name, email, birth) VALUES(?,?,?)";
        dao.update(sql, "XiaoMing", "xiaoming@atguigu.com", new Date(new java.util.Date().getTime()));
    }

    @Test
    public void testGet() {
        String sql = "SELECT id,name,email,birth FROM customers WHERE id = ?";
        Customer customer = dao.get(Customer.class, sql, 2);
        System.out.println(customer);
    }

    @Test
    public void testGetForList() {
        String sql = "SELECT id,name,email,birth FROM customers";
        List<Customer> customers = dao.getForList(Customer.class, sql);
        System.out.println(customers);
    }

    @Test
    public void testGetForValue() {
        String sql = "SELECT name FROM customers WHERE id = ?";
        String examCard = dao.getForValue(sql, 2);
        System.out.println(examCard); 
        
        sql = "SELECT max(id) FROM customers";
        long max = dao.getForValue(sql);
        System.out.println(max);
    }
}
View Code

  Customer

package com.oy;

import java.sql.Date;
public class Customer {
    private Long id;
    private String name;
    private String email;
    private Date birth;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @Override
    public String toString() {
        return "Customers{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", birth=" + birth +
                '}';
    }
}
View Code

  JDBCTools

package com.oy;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

public class JDBCTools {

    public static void release(ResultSet rs, Statement statement, Connection conn) {
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (statement != null) {
            try {
                statement.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void release(Statement statement, Connection conn) {
        if (statement != null) {
            try {
                statement.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static Connection getConnection() throws Exception {
        Properties properties = new Properties();
        InputStream in = JDBCTools.class.getClassLoader().getResourceAsStream("jdbc.properties");
        properties.load(in);

        String driver = properties.getProperty("driver");
        String jdbcUrl = properties.getProperty("jdbcUrl");
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");

        // 加载数据库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
        Class.forName(driver);
        // 通过 DriverManager 的 getConnection() 方法获取数据库连接
        return DriverManager.getConnection(jdbcUrl, user, password);
    }

}
View Code

  jdbc.properties

#driver=oracle.jdbc.driver.OracleDriver
#jdbcUrl=jdbc:oracle:thin:@localhost:1521:orcl
#user=scott
#password=java

driver=com.mysql.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost:3306/test
user=root
password=123456
View Code

  建表sql

CREATE TABLE `customers` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `email` varchar(50) DEFAULT NULL,
  `birth` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
View Code

 

7. JDBC 元数据    <--返回目录

  使用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):指示是否自动为指定列进行编号,这样这些列仍然是只读的。 

  测试

/**
 * 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.release(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.release(resultSet, null, connection);
    }
}
View Code

 

8. 获取插入记录的主键值    <--返回目录

/**
 * 取得数据库自动生成的主键
 */
@Test
public void testGetKeyValue() {

    Connection connection = null;
    PreparedStatement preparedStatement = null;

    try {
        connection = JDBCTools.getConnection();
        String sql = "INSERT INTO customers(name, email, birth) VALUES(?,?,?)";
        //使用重载的 prepareStatement(sql, flag) 来生成 PreparedStatement 对象
        preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

        preparedStatement.setString(1, "aaa");
        preparedStatement.setString(2, "aaa@xxx.com");
        preparedStatement.setDate(3, new Date(new java.util.Date().getTime()));
        preparedStatement.executeUpdate();

        //通过 getGeneratedKeys() 获取包含了新生成的主键的 ResultSet 对象
        //在 ResultSet 中只有一列 GENERATED_KEY, 用于存放新生成的主键值.
        ResultSet rs = preparedStatement.getGeneratedKeys();
        if(rs.next()){
            System.out.println(rs.getObject(1));
        }

        ResultSetMetaData rsmd = rs.getMetaData();
        for(int i = 0; i < rsmd.getColumnCount(); i++){
            System.out.println(rsmd.getColumnName(i + 1));
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally{
        JDBCTools.release(null, preparedStatement, connection);
    }
}

 

9. 处理 Blob    <--返回目录

   MySQL BLOB 类型介绍:

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

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

 

  alter table customers add picture longblob;

 

/**
 * 插入 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("a.jpeg");
        preparedStatement.setBlob(4, inputStream);

        preparedStatement.executeUpdate();
    } catch (Exception e) {
        e.printStackTrace();
    } finally{
        JDBCTools.release(null, preparedStatement, connection);
    }
}

/**
 * 读取 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 = 10";
        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.release(resultSet, preparedStatement, connection);
    }
}

 

10. 处理事务 & 事务的隔离级别    <--返回目录

10.1.处理事务

  数据库事务:

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

  事务的ACID属性

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

  

  JDBC处理事务

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

调用 Connection 对象的 setAutoCommit(false); 以取消自动提交事务
在所有的 SQL 语句都成功执行后,调用 commit(); 方法提交事务
在出现异常时,调用 rollback(); 方法回滚事务
若此时 Connection 没有被关闭, 则需要恢复其自动提交状态

  

  准备测试数据,创建表

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT NULL,
  `money` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

 

  测试代码

/**
 * jack 给 rose 汇款 500 元.
 * <p>
 * 关于事务:
 * 1. 如果多个操作, 每个操作使用的是自己的单独的连接, 则无法保证事务.
 * 2. 具体步骤:
 * 1). 事务操作开始前, 开始事务: 取消 Connection 的默认提交行为. connection.setAutoCommit(false);
 * 2). 如果事务的操作都成功,则提交事务: connection.commit();
 * 3). 回滚事务: 若出现异常, 则在 catch 块中回滚事务: connection.rollback();
 */
@Test
public void testTransaction() {
    Connection connection = null;

    try {
        connection = JDBCTools.getConnection();
        System.out.println(connection.getAutoCommit());

        // 开始事务: 取消默认提交.
        connection.setAutoCommit(false);

        String sql = "UPDATE account SET money = money - 500 WHERE id = 1";
        update(connection, sql);

        int i = 10 / 0;
        System.out.println(i);

        sql = "UPDATE account SET money = money + 500 WHERE id = 2";
        update(connection, sql);

        // 提交事务
        connection.commit();
    } catch (Exception e) {
        e.printStackTrace();

        // 回滚事务
        try {
            connection.rollback();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    } finally {
        JDBCTools.release(null, null, connection);
    }
}

// 改写 DAO 中原来定义的 update 方法
// 要使用事务,多个数据库操作应该使用的是同一个连接
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.release(null, preparedStatement, null);
    }
}

 

10.2.数据库隔离级别

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

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

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

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

  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 程序中可以通过 Connection 的 setTransactionIsolation 来设置事务的隔离级别.
 */
@Test
public void testTransactionIsolationUpdate() {
    Connection connection = null;
    try {
        connection = JDBCTools.getConnection();
        connection.setAutoCommit(false);

        String sql = "UPDATE account SET money = money - 500 WHERE id = 1";
        update(connection, sql);

        connection.commit(); // 1)测试时,首先debug跑testTransactionIsolationUpdate方法,并在这里打断点
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        JDBCTools.release(null, null, connection);
    }
}
// 2)然后跑testTransactionIsolationRead方法,当设置隔离级别为“读未提交”时,会读到testTransactionIsolationUpdate()未提交的数据。
@Test
public void testTransactionIsolationRead() {
    String sql = "SELECT money FROM account WHERE id = 1";
    Integer money = getForValue(sql);
    System.out.println("money: " + money);
}

// 返回某条记录的某一个字段的值 或 一个统计的值(一共有多少条记录等.)
public <E> E getForValue(String sql, Object... args) {
    // 得到结果集: 该结果集应该只有一行, 且只有一列
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;

    try {
        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.release(resultSet, preparedStatement, connection);
    }
    return null;
}

// 改写 DAO 中原来定义的 update 方法
// 要使用事务,多个数据库操作应该使用的是同一个连接
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.release(null, preparedStatement, null);
    }
}

 

11. 批量处理    <--返回目录

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

  当需要成批插入或者更新记录时。可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率
  JDBC的批量处理语句包括下面两个方法:
    addBatch(String):添加需要批量处理的SQL语句或是参数;
    executeBatch();执行批量处理语句;
  通常我们会遇到两种批量执行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(name,birth) 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.setString(1, "name_" + i);
            preparedStatement.setDate(2, 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.release(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(name,birth) 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.setString(1, "name_" + i);
            preparedStatement.setDate(2, date);
            preparedStatement.executeUpdate();
        }
        long end = System.currentTimeMillis();

        System.out.println("Time: " + (end - begin)); // 逐条插入 7563
        JDBCTools.commit(connection);
    } catch (Exception e) {
        e.printStackTrace();
        JDBCTools.rollback(connection);
    } finally{
        JDBCTools.release(null, preparedStatement, connection);
    }
}

/**
 * 向 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(name, birth) VALUES(" + "'name_" + i + "', '2022-03-19')";
            //statement.executeUpdate(sql);
            statement.addBatch(sql);
        }
        statement.executeBatch();
        long end = System.currentTimeMillis();
        System.out.println("Time: " + (end - begin)); // 逐条插入: 7556 批量插入: 8155

        JDBCTools.commit(connection);
    } catch (Exception e) {
        e.printStackTrace();
        JDBCTools.rollback(connection);
    } finally{
        JDBCTools.release(null, statement, connection);
    }
}
View Code

 

12. 数据库连接池 & C3P0 & DBCP    <--返回目录

  JDBC数据库连接池的必要性

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

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

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

  

  数据库连接池(connection pool)

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

  数据库连接池技术的优点:

    1)资源重用:由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增加了系统运行环境的平稳性。
    2)更快的系统反应速度:数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间
    3)新的资源分配手段:对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源
    4)统一的连接管理,避免数据库连接泄露:在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄露

 

12.1.DBCP

  DBCP的使用

/**
 * 使用 DBCP 数据库连接池
 * 1. 加入jar 包(2个jar 包): commons-dbcp-1.2.2.jar + commons-pool-1.3.jar
 * 2. 创建数据库连接池
 * 3. 为数据源实例指定必须的属性
 * 4. 从数据源中获取数据库连接
 * @throws SQLException
 */
@Test
public void testDBCP() throws SQLException{
    final BasicDataSource dataSource = new BasicDataSource();

    //2. 为数据源实例指定必须的属性
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    dataSource.setUrl("jdbc:mysql:///test");
    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. 从数据源中获取数据库连接
    Connection connection = dataSource.getConnection();
    System.out.println(connection.getClass()); // 1

    connection = dataSource.getConnection();
    System.out.println(connection.getClass()); // 2

    connection = dataSource.getConnection();
    System.out.println(connection.getClass()); // 3

    connection = dataSource.getConnection();
    System.out.println(connection.getClass()); // 4
    connection = dataSource.getConnection();
    System.out.println(connection.getClass()); // 5

//        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(5500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

//        connection2.close();
}

/**
 * 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());
}
View Code

  dbcp.properties

username=root
password=123456
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///test

initialSize=10
maxActive=50
minIdle=5
maxWait=5000
View Code

 

12.2.C3P0

  测试使用C3P0

@Test
public void testC3P0() throws Exception {
    ComboPooledDataSource dataSource = new ComboPooledDataSource();
    dataSource.setDriverClass("com.mysql.jdbc.Driver"); //loads the jdbc driver
    dataSource.setJdbcUrl("jdbc:mysql:///test");
    dataSource.setUser("root");
    dataSource.setPassword("123456");
    System.out.println(dataSource.getConnection());
}

/**
 * 使用 c3p0-config.xml 配置文件
 */
@Test
public void testC3poWithConfigFile() throws Exception {
    // 在创建连接池对象时,自动加载配置文件(有默认文件名和默认位置)
    // 配置文件可以给出多组配置参数,通过命名name="helloc3p0"配置,进行指定使用哪组参数
    DataSource dataSource = new ComboPooledDataSource("helloc3p0");
    System.out.println(dataSource.getConnection());

    ComboPooledDataSource comboPooledDataSource = (ComboPooledDataSource) dataSource;
    System.out.println(comboPooledDataSource.getMaxStatements());
}
View Code

  C3P0配置文件必须叫c3p0-config.xml,位置必须时类路径(classpath)下

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>

    <named-config name="helloc3p0">
        <!-- 指定连接数据源的基本属性 -->
        <property name="user">root</property>
        <property name="password">123456</property>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql:///test</property>
        
        <!-- 若数据库中连接数不足时, 一次向数据库服务器申请多少个连接 -->
        <property name="acquireIncrement">5</property>
        <!-- 初始化数据库连接池时连接的数量 -->
        <property name="initialPoolSize">5</property>
        <!-- 数据库连接池中的最小的数据库连接数 -->
        <property name="minPoolSize">5</property>
        <!-- 数据库连接池中的最大的数据库连接数 -->
        <property name="maxPoolSize">10</property>

        <!-- C3P0 数据库连接池可以维护的 Statement 的个数 -->
        <property name="maxStatements">20</property>
        <!-- 每个连接同时可以使用的 Statement 对象的个数 -->
        <property name="maxStatementsPerConnection">5</property>
    </named-config>
        
</c3p0-config>
View Code

  

  改写JDBCTools的获取连接的方法:使用C3P0获取连接

public class JDBCTools {
    // 使用C3P0获取连接
    // 数据库连接池应只被初始化一次
    private static DataSource dataSource = null;

    static {
        dataSource = new ComboPooledDataSource("helloc3p0");
    }

    public static Connection getConnection() throws Exception {
        return dataSource.getConnection();
    }
}

 

13. 使用 DBUtils    <--返回目录

   引入commons-dbutils-1.7.jar依赖。QueryRunner的使用

QueryRunner queryRunner = new QueryRunner();
    
@Test
public void testUpdate() {
    Connection connection = null;
    try {
        connection = JDBCTools.getConnection();
        String sql = "UPDATE customers SET name = ? WHERE id = ?";
        queryRunner.update(connection, sql, "MIKE", 11);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        JDBCTools.release(null, null, connection);
    }
}

/**
 * 自定义 ResultSetHandler
 * 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.release(null, null, connection);
    }
}

class MyResultSetHandler implements ResultSetHandler {

    @Override
    public Object handle(ResultSet resultSet) throws SQLException {
        List<Customer> customers = new ArrayList<>();
        while (resultSet.next()) {
            Long id = resultSet.getLong(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;
    }
}

  其他结果集处理器:ScalarHandler,BeanHandler,BeanListHandler,MapListHandler,MapHandler

package com.oy;

import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.junit.Test;

public class DBUtilsTest {

    QueryRunner queryRunner = new QueryRunner();

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

    /**
     * BeanHandler: 把结果集的第一条记录转为创建 BeanHandler 对象时传入的 Class
     * 参数对应的对象.
     */
    @Test
    public void testBeanHanlder() {
        Connection connection = null;
        try {
            connection = JDBCTools.getConnection();
            String sql = "SELECT id, name, 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.release(null, null, connection);
        }
    }

    /**
     * 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 limit 10";
            List<Customer> customers = queryRunner.query(connection, sql, new BeanListHandler<>(Customer.class));
            System.out.println(customers);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCTools.release(null, null, connection);
        }
    }

    /**
     * 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 limit 10";
            List<Map<String, Object>> result = queryRunner.query(connection, sql, new MapListHandler());
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCTools.release(null, null, connection);
        }
    }

    /**
     * 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.release(null, null, connection);
        }
    }

}
View Code

 

14.使用dbutils编写通用的DAO    <--返回目录

  Customer

package com.oy;

import java.sql.Date;

/**
 * CREATE TABLE `customers` (
 *   `id` bigint(20) NOT NULL AUTO_INCREMENT,
 *   `name` varchar(50) DEFAULT NULL,
 *   `email` varchar(50) DEFAULT NULL,
 *   `birth` date DEFAULT NULL,
 *   PRIMARY KEY (`id`)
 * ) ENGINE=InnoDB DEFAULT CHARSET=utf8
 */
public class Customer {
    private Long id;
    private String name;
    private String email;
    private Date birth;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    public Customer() {
    }

    public Customer(Long id, String name, String email, Date birth) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.birth = birth;
    }

    @Override
    public String toString() {
        return "Customers{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", birth=" + birth +
                '}';
    }
}
View Code

  CustomerDao

package com.oy;

public class CustomerDao extends DaoImpl<Customer> {
    
}
View Code

  CustomerDaoTest

package com.oy;

import java.sql.Connection;
import org.junit.Test;

public class CustomerDaoTest {

    CustomerDao customerDao = new CustomerDao();

    @Test
    public void testGet() {
        Connection connection = null;
        try {
            connection = JDBCTools.getConnection();
            String sql = "SELECT id, name customerName, email, birth FROM customers WHERE id = ?";
            Customer customer = customerDao.get(connection, sql, 5);
            System.out.println(customer); 
        } catch (Exception e) {
            e.printStackTrace();
        } finally{
            JDBCTools.release(null, null, connection);
        }
    }
}
View Code

  DAO

package com.oy;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

/**
 * 访问数据的 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;

}
View Code

  DaoImpl

package com.oy;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

/**
 * 使用 QueryRunner 提供其具体的实现
 * @param <T>: 子类需传入的泛型类型.
 */
public class DaoImpl<T> implements DAO<T> {
    private QueryRunner queryRunner = null;
    private Class<T> type;

    public DaoImpl() {
        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);
    }

}
View Code

  JDBCTools

package com.oy;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;

import com.mchange.v2.c3p0.ComboPooledDataSource;

/**
 * JDBC 的工具类
 * <p>
 * 其中包含: 获取数据库连接, 关闭数据库资源等方法.
 */
public class JDBCTools {

    private static DataSource dataSource = null;

    //数据库连接池应只被初始化一次.
    static {
        dataSource = new ComboPooledDataSource("helloc3p0");
    }

    public static Connection getConnection() throws Exception {
        return dataSource.getConnection();
    }

    //处理数据库事务的
    //提交事务
    public static void commit(Connection connection) {
        if (connection != null) {
            try {
                connection.commit();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    //回滚事务
    public static void rollback(Connection connection) {
        if (connection != null) {
            try {
                connection.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    //开始事务
    public static void beginTx(Connection connection) {
        if (connection != null) {
            try {
                connection.setAutoCommit(false);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public static void release(ResultSet resultSet, Statement statement, Connection connection) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                //数据库连接池的 Connection 对象进行 close 时
                //并不是真的进行关闭, 而是把该数据库连接会归还到数据库连接池中.
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}
View Code

  ReflectionUtils

package com.oy;

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;
    }
}
View Code

  c3p0-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>

    <named-config name="helloc3p0">
        <!-- 指定连接数据源的基本属性 -->
        <property name="user">root</property>
        <property name="password">123456</property>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql:///test</property>
        
        <!-- 若数据库中连接数不足时, 一次向数据库服务器申请多少个连接 -->
        <property name="acquireIncrement">5</property>
        <!-- 初始化数据库连接池时连接的数量 -->
        <property name="initialPoolSize">5</property>
        <!-- 数据库连接池中的最小的数据库连接数 -->
        <property name="minPoolSize">5</property>
        <!-- 数据库连接池中的最大的数据库连接数 -->
        <property name="maxPoolSize">10</property>

        <!-- C3P0 数据库连接池可以维护的 Statement 的个数 -->
        <property name="maxStatements">20</property>
        <!-- 每个连接同时可以使用的 Statement 对象的个数 -->
        <property name="maxStatementsPerConnection">5</property>
    </named-config>
        
</c3p0-config>
View Code

 

15. 使用 JDBC 调用函数 & 存储过程    <--返回目录

/**
 * 如何使用 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);
    }
}
View Code

 

posted on 2020-11-04 14:22  wenbin_ouyang  阅读(153)  评论(0编辑  收藏  举报