JDBC【java全端课24】

63 阅读21分钟

一 JDBC

1.1 JDBC概述

JDBC:Java Database Connectivity,它是代表一组独立于任何数据库管理系统(DBMS)的API,声明在java.sql与javax.sql包中,是SUN(现在Oracle)提供的一组接口规范。由各个数据库厂商来提供实现类,这些实现类的集合构成了数据库驱动jar。

即JDBC技术包含两个部分:

(1)java.sql包和javax.sql包中的API

因为为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种DBMS软件,Java代码可以保持一致性。

(2)各个数据库厂商提供的jar

因为各个数据库厂商的DBMS软件各有不同,那么内部如何通过sql实现增、删、改、查等管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。

1.2 JDBC连接基本步骤

1.2.1 引入mysql驱动jar

(1)在模块路径下建一个文件夹“lib”,把mysql的驱动jar放到里面
	MySQL8.0:mysql-connector-java-8.0.25.jar
(2)在jdbclibs文件夹上右键-->Add as Library...
(3)填写库名称、选择这个库应用范围(模块)-->选择使用的具体模块

1.2.2 Java代码连接MySQL数据库

步骤:

1. 注册驱动【依赖的jar包 进行安装】
2. 获取连接【connection建立连接】
3. 创建发送sql语句对象【statement 创建发送sql语句的statement】
4. 发送sql语句,并获取返回结果【statement发送sql语句到数据库 并且取得返回结构】
5. 结果集解析【将result结果解析出来】
6. 资源关闭【释放resultset、statement、connection】

准备:

CREATE DATABASE mytest;

USE mytest;

CREATE TABLE t_user(
   id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户主键',
   account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
   PASSWORD VARCHAR(64) NOT NULL COMMENT '密码',
   nickname VARCHAR(20) NOT NULL COMMENT '昵称');
   
INSERT INTO t_user(account,PASSWORD,nickname) VALUES
  ('root','123456','经理'),('admin','666666','管理员');

1.3 JDBC连接基本实现

###1.3.1 DQL操作

/**
 * Description: 利用jdbc技术,完成用户数据查询工作
 *
 * TODO: 步骤总结 (6步)
 *    1. 注册驱动
 *    2. 获取连接
 *    3. 创建statement
 *    4. 发送SQL语句,并获取结果
 *    5. 结果集解析
 *    6. 关闭资源
 */
public class JdbcBasePart {

    public static void main(String[] args) throws SQLException {

        //1.注册驱动
        /**
         * TODO: 注意
         *   8+ Driver -> com.mysql.cj.jdbc.Driver
         */
        DriverManager.registerDriver(new Driver());
        //2.获取连接
        /**
         *   面向接口编程
         *   java.sql 接口 = 实现类
         */
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mytest",
                "root",
                "root");
        //3.创建小车
        Statement statement = connection.createStatement();

        //4.发送SQL语句
        String sql = "select id,account,password,nickname from t_user ;";
        ResultSet resultSet =  statement.executeQuery(sql);

        //5.结果集解析
        while (resultSet.next()){
            int id = resultSet.getInt("id");
            String account = resultSet.getString("account");
            String password = resultSet.getString("password");
            String nickname = resultSet.getString("nickname");
            System.out.println(id+"::"+account+"::"+password+"::"+nickname);
        }

        //6.关闭资源  【先开后关】
        resultSet.close();
        statement.close();
        connection.close();
    }
}

1.3.2 DML操作

@Test
public void testDml() throws SQLException, ClassNotFoundException {
    //1.注册驱动
    /**
    * DriverManager.registerDriver(new Driver());注册两次驱动!
    */
    Class.forName("com.mysql.cj.jdbc.Driver");
    //2.获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mytest",
    "root",
    "root");
    //3.创建小车
    Statement statement = connection.createStatement();

    //4.发送SQL语句
    String sql = "delete from t_user where id = 1 ;";
    int rows  =  statement.executeUpdate(sql);
    System.out.println("rows = " + rows);
    statement.close();
    connection.close();
}

二 轻松处理各种问题

2.1 产生sql拼接问题(Statement)

模拟登录,控制台输入账号和密码,判断是否登陆成功成功!

/**
 * Description: 输入账号密码,模拟用户登录!
 */
public class JdbcStatementLoginPart {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        //1.输入账号和密码
        Scanner scanner = new Scanner(System.in);
        String account = scanner.nextLine();
        String password = scanner.nextLine();
        scanner.close();

        //2.jdbc的查询使用     
         //1:DriverManager.registerDriver(new Driver());调用两次。不用
         //2:new Driver()  频繁修改不优雅
        //注册一次驱动 方法三
        Class.forName("com.mysql.cj.jdbc.Driver");
        //获取连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
        //固定方法固定剂
        //创建statement
        Statement statement = connection.createStatement();

        //执行SQL语句 [动态SQL语句,需要字符串拼接]
        String sql = "select * from t_user where account = '"+account+"' and password = '"+password+"' ;";

        /**
         *  sql分类: DDL(容器创建,修改,删除) DML(插入,修改,删除) DQL(查询) DCL(权限控制) TPL(事务控制)
         *  ResultSet 结果集对象 = executeQuery(DQL语句)
         *  int响应行数  = executeUpdate(非DQL语句)
         */
        ResultSet resultSet = statement.executeQuery(sql);

        //进行结果集对象解析
        if (resultSet.next()){
            //只要向下移动,就是有数据 就是登录成功!
            System.out.println("登录成功!");
        }else{
            System.out.println("登录失败!");
        }

        //关闭资源
        resultSet.close();
        statement.close();
        connection.close();
    }
}

2.2 避免sql注入问题(PreparedStatement)

/**
 * Description: 使用预编译Statement解决注入攻击问题
 */
public class JdbcPreparedStatementLoginPart {

    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        //1.输入账号和密码
        Scanner scanner = new Scanner(System.in);
        String account = scanner.nextLine();
        String password = scanner.nextLine();
        scanner.close();

        //2.jdbc的查询使用
        //注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //获取连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");
        //创建preparedStatement
        //connection.createStatement();
        //TODO 需要传入SQL语句结构
        //TODO 要的是SQL语句结构,动态值的部分使用 ? ,  占位符!
        //TODO ?  不能加 '?'  ? 只能替代值,不能替代关键字和容器名
        String sql = "select * from t_user where account = ? and password = ? ;";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        //占位符赋值
        //给占位符赋值! 从左到右,从1开始!
        /**
         *  int 占位符的下角标
         *  object 占位符的值
         */
        preparedStatement.setObject(2,password);
        preparedStatement.setObject(1,account);

        //这哥们内部完成SQL语句拼接!
        //执行SQL语句即可
        ResultSet resultSet = preparedStatement.executeQuery();
        //preparedStatement.executeUpdate()

        //进行结果集对象解析
        if (resultSet.next()){
            //只要向下移动,就是有数据 就是登录成功!
            System.out.println("登录成功!");
        }else{
            System.out.println("登录失败!");
        }

        //关闭资源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}

2.3 基于preparedStatement演示curd

/**
 * 插入一条用户数据!
 * 账号: test
 * 密码: test
 * 昵称: 测试
 */
@Test
public void testInsert() throws Exception{

    //注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    //获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");

    //TODO: 切记, ? 只能代替 值!!!!!  不能代替关键字 特殊符号 容器名
    String sql = "insert into t_user(account,password,nickname) values (?,?,?);";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);

    //占位符赋值
    preparedStatement.setString(1, "test");
    preparedStatement.setString(2, "test");
    preparedStatement.setString(3, "测试");

    //发送SQL语句
    int rows = preparedStatement.executeUpdate();

    //输出结果
    System.out.println(rows);

    //关闭资源close
    preparedStatement.close();
    connection.close();
}

/**
 * 修改一条用户数据!
 * 修改账号: test的用户,将nickname改为tomcat
 */
@Test
public void testUpdate() throws Exception{

    //注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    //获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");

    //TODO: 切记, ? 只能代替 值!!!!!  不能代替关键字 特殊符号 容器名
    String sql = "update t_user set nickname = ? where account = ? ;";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);

    //占位符赋值
    preparedStatement.setString(1, "tomcat");
    preparedStatement.setString(2, "test");

    //发送SQL语句
    int rows = preparedStatement.executeUpdate();

    //输出结果
    if(rows>0){
      System.out.println("修改成功”);
    }else{
      System.out.println("修改失败”);
    }

    //关闭资源close
    preparedStatement.close();
    connection.close();
}
                    
/**
 * 删除一条用户数据!
 * 根据账号: test
 */
@Test
public void testDelete() throws Exception{

    //注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    //获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");

    //TODO: 切记, ? 只能代替 值!!!!!  不能代替关键字 特殊符号 容器名
    String sql = "delete from t_user where account = ? ;";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);

    //占位符赋值
    preparedStatement.setString(1, "test");

    //发送SQL语句
    int rows = preparedStatement.executeUpdate();

    //输出结果
    System.out.println(rows);

    //关闭资源close
    preparedStatement.close();
    connection.close();
}
                                           
/**
 * 查询全部数据!
 *   将数据存到List<Map>中
 *   map -> 对应一行数据
 *      map key -> 数据库列名或者别名
 *      map value -> 数据库列的值
 * TODO: 思路分析
 *    1.先创建一个List<Map>集合
 *    2.遍历resultSet对象的行数据
 *    3.将每一行数据存储到一个map对象中!
 *    4.将对象存到List<Map>中
 *    5.最终返回
 *注:如何获取列名?!!!!!!!!!????重要!!!
 * TODO:
 *    初体验,结果存储!
 *    学习获取结果表头信息(列名和数量等信息)
 */
@Test
public void testQueryMap() throws Exception{
    //注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");
    //获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql:///mytest", "root", "root");

    //TODO: 切记, ? 只能代替 值!!!!!  不能代替关键字 特殊符号 容器名
    String sql = "select id,account,password,nickname from t_user ;";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);

    //占位符赋值 本次没有占位符,省略

    //发送查询语句
    ResultSet resultSet = preparedStatement.executeQuery();
    //注意只有查询是executeQuery(); 其他都是executeUpdate();

    //创建一个集合
    List<Map> mapList = new ArrayList<>();

    //获取列信息对象
    //metaData装的是当前列的信息对象(通过他可以获取列对应的下角标,或者是列的数量)
    // 也可以通过下角标获取列的名称
    ResultSetMetaData metaData = resultSet.getMetaData();
    
    int columnCount = metaData.getColumnCount();
    //可以水平遍历列
    
    while (resultSet.next()) {
    //一行数据对应一个map
        //取值用resultSet
         //取位置(下角标)用metadata
        Map map = new HashMap();
        for (int i = 1; i <= columnCount; i++) {
        //笨蛋写法:
        //map.put("id",resultSet.getInt("id"));好几行,写法固定
        //value:获取指定下角标的值 用resultSet        key;用metaData
        //getColumnLabel先获取列的别名,没有别名用列名,而getColumnName只是列名
        //getObject获得当前行行的数据作为一个对象
            map.put(metaData.getColumnLabel(i), resultSet.getObject(i));
        }
        mapList.add(map);
    }

    System.out.println(mapList);

    //关闭资源close
    preparedStatement.close();
    connection.close();
    resultSet.close();
}                         

2.4 获取自增长键值

/**
 * 返回插入的主键!
 * 主键:数据库帮助维护的自增长的整数主键!
 * @throws Exception
 */
@Test
public void  returnPrimaryKey() throws Exception{

    //1.注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");
    //2.获取连接
    Connection connection = DriverManager.getConnection(
                          "jdbc:mysql:///mytest?user=root&password=root");
    //3.编写SQL语句结构
    String sql = "insert into t_user (account,password,nickname) values (?,?,?);";
    //4.创建预编译的statement,传入SQL语句结构
    /**
     * TODO: 第二个参数填入 1 | Statement.RETURN_GENERATED_KEYS
     *       告诉statement携带回数据库生成的主键!
     */
    PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    //5.占位符赋值
    statement.setObject(1,"towgog");
    statement.setObject(2,"123456");
    statement.setObject(3,"二狗子");
    //6.执行SQL语句 【注意:不需要传入SQL语句】 DML
    int i = statement.executeUpdate();
    //7.结果集解析
    System.out.println("i = " + i);

    //一行一列的数据!里面就装主键值!固定用getGeneratedKeys!!!
    ResultSet resultSet = statement.getGeneratedKeys();
    resultSet.next();
    //移动光标
    int anInt = resultSet.getInt(1);//指向第一列
    System.out.println("anInt = " + anInt);

    //8.释放资源
    statement.close();
    connection.close();
}

2.5 批量数据插入处理

/**
 *改动了三处:(1)路径(2)必写values,且后面不加;(3)装货addBatch()最后executeBatch();
 * 批量细节:
 *    1.url?rewriteBatchedStatements=true
 *    2.insert 语句必须使用 values
 *    3.语句后面不能添加分号;
 *    4.语句不能直接执行,每次需要装货  addBatch() 最后 executeBatch();
 *
 * 批量插入优化!
 * @throws Exception
 */
@Test
public void  batchInsertYH() throws Exception{
    //1.注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");
    //2.获取连接
    Connection connection = DriverManager.getConnection
          ("jdbc:mysql:///mytest?rewriteBatchedStatements=true","root","root");
    //3.编写SQL语句结构
    String sql = "insert into t_user (account,password,nickname) values (?,?,?)";
    //4.创建预编译的statement,传入SQL语句结构
    /**
     * TODO: 第二个参数填入 1 | Statement.RETURN_GENERATED_KEYS
     *       告诉statement携带回数据库生成的主键!
     */
    long start = System.currentTimeMillis();
    PreparedStatement statement = connection.prepareStatement(sql);
    for (int i = 0; i < 10000; i++) {

        //5.占位符赋值
        statement.setObject(1,"ergouzi"+i);
        statement.setObject(2,"lvdandan");
        statement.setObject(3,"驴蛋蛋"+i);
        //6.装车
        statement.addBatch();
    }

    //发车! 批量操作!
    statement.executeBatch();

    long end = System.currentTimeMillis();

    System.out.println("消耗时间:"+(end - start));

    //7.结果集解析

    //8.释放资源
    connection.close();
}

2.6 事务处理

  1. 数据库表结构

    -- 继续在mytest的库中创建银行表
    CREATE TABLE t_bank(
       id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
       account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
       money  INT UNSIGNED COMMENT '金额,不能为负值') ;
       
    INSERT INTO t_bank(account,money) VALUES
      ('ergouzi',1000),('lvdandan',1000);
    
  2. 代码结构设计

  1. 测试类
public class BankTest {
   @Test
   public void testBank() throws Exception {
       BankService bankService = new BankService();
       bankService.transfer("ergouzi", "lvdandan",
               500);
   }
}
  1. 业务类
   public class BankService {
   
   //一个事物最基本的是在同一个连接中connection,一个转账方法是一个事物,将connection传入dao
   //实现层即可,dao层不用关闭connection,由事物统一关闭
       /**
        * 转账业务方法
        * @param addAccount  加钱账号
        * @param subAccount  减钱账号
        * @param money  金额
        */
       public void transfer(String addAccount,String subAccount, int money) throws ClassNotFoundException, SQLException {
   
           System.out.println("addAccount = " + addAccount + ", subAccount = " + subAccount + ", money = " + money);
   
           //注册驱动
           Class.forName("com.mysql.cj.jdbc.Driver");
   
           //获取连接
           Connection connection = DriverManager.getConnection
                                   ("jdbc:mysql:///mytest", "root", "root");
   
           int flag = 0;
   
           //利用try代码块,调用dao
           try {
               //开启事务(关闭事务自动提交)
               connection.setAutoCommit(false);
   
               BankDao bankDao = new BankDao();
               //调用加钱 和 减钱
               bankDao.addMoney(addAccount,money,connection);
               System.out.println("--------------");
               bankDao.subMoney(subAccount,money,connection);
               flag = 1;
               //不报错,提交事务
               connection.commit();
           }catch (Exception e){
   
               //报错回滚事务
               connection.rollback();
               throw e;
           }finally {
               connection.close();
           }
   
           if (flag == 1){
               System.out.println("转账成功!");
           }else{
               System.out.println("转账失败!");
           }
       }
   
   }
  1. 持久类
   public class BankDao {
   
       /**
        * 加钱方法
        * @param account
        * @param money
        * @param connection 业务传递的connection和减钱是同一个! 才可以在一个事务中!
        * @return 影响行数
        */
       public int addMoney(String account, int money,Connection connection) throws ClassNotFoundException, SQLException {
   
   
           String sql = "update t_bank set money = money + ? where account = ? ;";
           PreparedStatement preparedStatement = connection.prepareStatement(sql);
   
           //占位符赋值
           preparedStatement.setObject(1, money);
           preparedStatement.setString(2, account);
   
           //发送SQL语句
           int rows = preparedStatement.executeUpdate();
   
           //输出结果
           System.out.println("加钱执行完毕!");
   
           //关闭资源close
           preparedStatement.close();
   
           return rows;
       }
   
       /**
        * 减钱方法
        * @param account
        * @param money
        * @param connection 业务传递的connection和加钱是同一个! 才可以在一个事务中!
        * @return 影响行数
        */
       public int subMoney(String account, int money,Connection connection) throws ClassNotFoundException, SQLException {
   
           String sql = "update t_bank set money = money - ? where account = ? ;";
           PreparedStatement preparedStatement = connection.prepareStatement(sql);
   
           //占位符赋值
           preparedStatement.setObject(1, money);
           preparedStatement.setString(2, account);
   
           //发送SQL语句
           int rows = preparedStatement.executeUpdate();
   
           //输出结果
           System.out.println("减钱执行完毕!");
   
           //关闭资源close
           preparedStatement.close();
   
           return rows;
       }
   }

三 数据库连接池

3.1 什么是数据库连池

连接对象的缓冲区。负责申请,分配管理,释放连接的操作。

3.2 为什么使用数据库连接池

(1)不使用数据库连接池,每次都通过DriverManager获取新连接,用完直接抛弃断开,连接的利用率太低,太浪费。 (2)对于数据库服务器来说,压力太大了。我们数据库服务器和Java程序对连接数也无法控制,很容易导致数据库服务器崩溃。

我们就希望能管理连接。

  • 我们可以建立一个连接池,这个池中可以容纳一定数量的连接对象,一开始,我们可以先替用户先创建好一些连接对象,等用户要拿连接对象时,就直接从池中拿,不用新建了,这样也可以节省时间。然后用户用完后,放回去,别人可以接着用。
  • 可以提高连接的使用率。当池中的现有的连接都用完了,那么连接池可以向服务器申请新的连接放到池中。 直到池中的连接达到“最大连接数”,就不能在申请新的连接了,如果没有拿到连接的用户只能等待。

3.3 数据库连接池技术种类

  • JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口(通常被称为数据源),该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:
    • DBCP 是Apache提供的数据库连接池,速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持
    • C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以
    • Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
    • BoneCP 是一个开源组织提供的数据库连接池,速度快
    • Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池

3.4 如何使用德鲁伊数据库连接池

1)引入jar包和引入mysql驱动jar方式一样
(2)创建数据库连接池对象
(3)获取连接
package com.mytest.pool;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class TestPool {
    static int count = 0;

    public static void main(String[] args) throws Exception {
        Properties pro = new Properties();
        pro.setProperty("driverClassName","com.mysql.cj.jdbc.Driver");
        pro.setProperty("url","jdbc:mysql://localhost:3306/mytest");
        pro.setProperty("username","root");
        pro.setProperty("password","123456");
        pro.setProperty("initialSize","5");//表示预先缓存5个连接
        pro.setProperty("maxActive","10");//最多的连接数量10
        pro.setProperty("maxWait","1000");//等待连接的时间,超过时间就报异常
        DataSource ds = DruidDataSourceFactory.createDataSource(pro);
/*        Connection connection = ds.getConnection();
        System.out.println("connection = " + connection);*/

/*        for(int i=1; i<=15; i++){
            Connection connection = ds.getConnection();
            System.out.println("第" + i+ "个connection = " + connection);

            //这里没有写connection.close(),连接没有关闭,没有还给连接池,一直占用
//            connection.close();
        }*/

        for(int i=1; i<=15; i++){

            new Thread(){
                @Override
                public void run() {
                    try {
                        Connection connection = ds.getConnection();
                        System.out.println("第" + (count++)+ "个connection = " + connection);

                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        connection.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }.start();

        }
    }
}
配置缺省说明
name配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:”DataSource-” + System.identityHashCode(this)
jdbcUrl连接数据库的url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username连接数据库的用户名
password连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:github.com/alibaba/dru…
driverClassName根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下)
initialSize0初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive8最大连接池数量
maxIdle8已经不再使用,配置了也没效果
minIdle最小连接池数量
maxWait获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatementsfalse是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxOpenPreparedStatements-1要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
testOnBorrowtrue申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturnfalse归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testWhileIdlefalse建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
timeBetweenEvictionRunsMillis有两个含义: 1)Destroy线程会检测连接的间隔时间2)testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis
connectionInitSqls物理连接初始化的时候执行的sql
exceptionSorter根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
proxyFilters类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系

四 JDBCTools工具类

4.1 druid.properties文件

#key=value
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mytest?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=123456
initialSize=5
maxActive=10
maxWait=1000

4.2 JDBCTools工具类1.0版

4.2.1 JDBCToolsVersion1类

package com.mytest.tools;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class JDBCToolsVersion1 {
    private static DataSource ds;
    static{//静态代码块,JDBCToolsVersion1类初始化执行
        try {
            Properties pro = new Properties();
            pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
            ds = DruidDataSourceFactory.createDataSource(pro);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }

    public static void free(Connection conn) throws SQLException {
        conn.close();//还给连接池
    }
}

4.3 ThreadLocal类

其中ThreadLocal的介绍如下:

JDK 1.2的版本中就提供java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。通常用来在在多线程中管理共享数据库连接、Session等

ThreadLocal用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal, Object>,其key就是一个ThreadLocal,而Object即为该线程的共享变量。而这个map是通过ThreadLocal的set和get方法操作的。对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。

1、ThreadLocal对象.get: 获取ThreadLocal中当前线程共享变量的值。

2、ThreadLocal对象.set: 设置ThreadLocal中当前线程共享变量的值。

3、ThreadLocal对象.remove: 移除ThreadLocal中当前线程共享变量的值。

线程(事务)结束后别忘了移除共享对象。

package com.mytest.tools;

public class TestThreadLocal {
    private static ThreadLocal<String> tl = new ThreadLocal<>();

    public static void print(){
        System.out.println(Thread.currentThread().getName() +"->" +tl.get());
    }

    /*
               这里有两个线程,每一个线程都有自己的ThreadLocalMap对象,
               它们此时都有一个key为tl的键值对,但是两个map中tl对应的value不一样,一个是mytest,一个是mysql
                */
    public static void main(String[] args) {
        new Thread("线程1"){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"set之前:" );
                print();

                tl.set("mytest");

                System.out.println(Thread.currentThread().getName()+"set之后:" );
                print();

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName()+"休眠1秒之后:" );
                print();

                tl.remove();

                System.out.println(Thread.currentThread().getName()+"移出之后:" );
                print();
            }
        }.start();

        new Thread("线程2"){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"set之前:" );
                print();

                tl.set("mysql");

                System.out.println(Thread.currentThread().getName()+"set之后:" );
                print();

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName()+"休眠1秒之后:" );
                print();

                tl.remove();

                System.out.println(Thread.currentThread().getName()+"移出之后:" );
                print();
            }
        }.start();
    }
}

4.4 JDBCTools工具类2.0版

4.4.1 JDBCToolsVersion1类

import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
//事物时,Service和dao属于同一线程,不用再传参数了
/*
这个工具类的作用就是用来给所有的SQL操作提供“连接”,和释放连接。
这里使用ThreadLocal的目的是为了让同一个线程,在多个地方getConnection得到的是同一个连接。
这里使用DataSource的目的是为了(1)限制服务器的连接的上限(2)连接的重用性等
 */
public class JDBCTools {
    private static DataSource ds;
    private static ThreadLocal<Connection> tl = new ThreadLocal<>();
    static{//静态代码块,JDBCToolsVersion1类初始化执行
        try {
            Properties pro = new Properties();
            pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
            ds = DruidDataSourceFactory.createDataSource(pro);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
    //获取连接
         Connection connection = tl.get();
         if(connection  == null){//当前线程还没有拿过连接,就给它从数据库连接池拿一个
             connection = ds.getConnection();
             //连接存入共享变量中
             tl.set(connection);
         }
         return connection;
    }

    public static void free() throws SQLException {
        Connection connection = tl.get();
        if(connection != null){
            tl.remove();
            //避免还给数据库连接池的连接不是自动提交模式(建议)
            //因为其在业务类中会关闭事务的自动提交
            connection.setAutoCommit(true);
            connection.close();
        }
    }
}

五 封装DAO层代码

5.1 DAO层类

Java是面向对象语言,数据在Java中通常以对象的形式存在。

数据库中的记录 <-- DAO类 --> Java的对象对应起来。

我们把访问数据库的代码封装起来,方便数据库访问的复用等,封装的这些类称为DAO(Data Access Object)。它相当于是一个数据访问接口,夹在业务逻辑与数据库资源中间。

5.2 反射封装BaseDAOImpl类

基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,我们称为BaseDAOImpl.

public abstract class BaseDao {
    /*
    通用的增、删、改的方法
    String sql:sql
    Object... args:给sql中的?设置的值列表,可以是0~n
     */
    protected int update(String sql,Object... args) throws SQLException {
//        创建PreparedStatement对象,对sql预编译
        Connection connection = JDBCTools.getConnection();
        PreparedStatement ps = connection.prepareStatement(sql);
        //设置?的值
        if(args != null && args.length>0){
            for(int i=0; i<args.length; i++) {
                ps.setObject(i+1, args[i]);//?的编号从1开始,不是从0开始,数组的下标是从0开始
            }
        }

        //执行sql
        int len = ps.executeUpdate();
        ps.close();
        //这里检查下是否开启事务,开启不关闭连接,业务方法关闭!
        //connection.getAutoCommit()为false,不要在这里回收connection,由开启事务的地方回收
        //connection.getAutoCommit()为true,正常回收连接
        //没有开启事务的话,直接回收关闭即可!
        if (connection.getAutoCommit()) {
            //回收
            JDBCTools.free();
        }
        return len;
    }

    /*
    通用的查询多个Javabean对象的方法,例如:多个员工对象,多个部门对象等
    这里的clazz接收的是T类型的Class对象,
    如果查询员工信息,clazz代表Employee.class,
    如果查询部门信息,clazz代表Department.class,
    返回List<T> list
     */
protected <T> ArrayList<T> query(Class<T> clazz,String sql, Object... args) throws Exception {
        //        创建PreparedStatement对象,对sql预编译
        Connection connection = JDBCTools.getConnection();
        PreparedStatement ps = connection.prepareStatement(sql);
        //设置?的值
        if(args != null && args.length>0){
            for(int i=0; i<args.length; i++) {
                ps.setObject(i+1, args[i]);//?的编号从1开始,不是从0开始,数组的下标是从0开始
            }
        }

        ArrayList<T> list = new ArrayList<>();
        ResultSet res = ps.executeQuery();

        /*
        获取结果集的元数据对象。
        元数据对象中有该结果集一共有几列、列名称是什么等信息
         */
         ResultSetMetaData metaData = res.getMetaData();
        int columnCount = metaData.getColumnCount();//获取结果集列数

        //遍历结果集ResultSet,把查询结果中的一条一条记录,变成一个一个T 对象,放到list中。
        while(res.next()){
            //循环一次代表有一行,代表有一个T对象
            T t = clazz.newInstance();//要求这个类型必须有公共的无参构造

            //把这条记录的每一个单元格的值取出来,设置到t对象对应的属性中。
            for(int i=1; i<=columnCount; i++){
                //for循环一次,代表取某一行的1个单元格的值
                Object value = res.getObject(i);

                //这个值应该是t对象的某个属性值
                //获取该属性对应的Field对象
                //String columnName = metaData.getColumnName(i);//获取第i列的字段名
                //这里再取别名可能没办法对应上
                String columnName = metaData.getColumnLabel(i);//获取第i列的字段名或字段的别名
                Field field = clazz.getDeclaredField(columnName);
                field.setAccessible(true);//这么做可以操作private的属性

                field.set(t, value);
            }

            list.add(t);
        }

        res.close();
        ps.close();
        //这里检查下是否开启事务,开启不关闭连接,业务方法关闭!
        //没有开启事务的话,直接回收关闭即可!
        if (connection.getAutoCommit()) {
            //回收
            JDBCTools.free();
        }
        return list;
    }

protected <T> T queryBean(Class<T> clazz,String sql, Object... args) throws Exception {
        ArrayList<T> list = query(clazz, sql, args);
        if(list == null || list.size() == 0){
            return null;
        }
        return list.get(0);
    }
}

补充问题: jar包版本不兼容

使用低版本jar,例如:mysql-connector-java-8.0.19.jar

Exception in thread "main" com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server.
  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  at com.mysql.jdbc.Util.handleNewInstance(Util.java:408)
  at com.mysql.jdbc.Util.getInstance(Util.java:383)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1023)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:997)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:983)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:928)
  at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2576)
  at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2309)
  at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:834)
  at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:46)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  at com.mysql.jdbc.Util.handleNewInstance(Util.java:408)
  at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:419)
  at com.mysql.jdbc.NonRegisteringDriver.connect(Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
Exception in thread "main" java.sql.SQLException: The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73)
  at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:76)
  at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:835)
  at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:455)
  at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:240)
  at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:199)
  at java.sql.DriverManager.getConnection(DriverManager.java:664)
  at java.sql.DriverManager.getConnection(DriverManager.java:247)
.java:344)
  at java.sql.DriverManager.getConnection(DriverManager.java:664)
  at java.sql.DriverManager.getConnection(DriverManager.java:247)

看异常好像是无事务连接异常,无法创建连接。将MySQL驱动改为了最新的8.0版本的MySQL驱动。显示那个驱动类已经过时了,新的驱动类是“com.mysql.cj.jdbc.Driver”,而不是“com.mysql.jdbc.Driver”了,并且还说我没有配置时区,查了一下,原来从JDBC6.0开始驱动类使用了新的,并且url中必须要设置时区,否侧会报错。

如果继续使用旧版jar包,注意驱动类名改为:com.mysql.cj.jdbc.Driver
url后加入时区设置:jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC