【Java基础(应用篇)】JDBC

JDBC

文章目录

  • JDBC
    • 1. 概述
    • 2. 核心类
    • 3. 使用步骤
    • 4. 三元运算符与if判断的不同点
      • 4.1 自增主键回显
      • 4.2 批量插入
      • 4.3 事务使用
    • 5. 连接池
    • 6. JDBC优化及工具类封装
      • 6.1 JDBCTools
      • 6.2 高级应用层封装BaseDao

1. 概述

jdbc由java语言的规范(接口,存储在java.sql和javax.sql包中的api)和各个数据库厂商的实现驱动(jar)组成。

2. 核心类

  • DriverManager:将第三方数据库厂商的实现驱动jar注册到程序中
  • Connection:和数据库建立的连接,可以获取 statement、preparedstatement 和 callablestatement 对象
  • 具体发送SQL到数据库管理软件的对象
    • statement:存在注入风险,一般不使用
    • preparedstatement:预编译执行,一般SQL调用只使用该对象
    • callablestatement:调用存储过程使用的对象

3. 使用步骤

  1. 注册驱动
    • Driver中有静态代码块会执行注册驱动,故只要用Class.forName加载对应类就可以
  2. 获取连接
  3. 创建发送sql语句对象
  4. 发送sql语句,并获取返回结果
  5. 结果集解析
  6. 资源关闭
/**
 * 查询全部数据!
 *   将数据存到List中
 *   map -> 对应一行数据
 *      map key -> 数据库列名或者别名
 *      map value -> 数据库列的值
 * TODO: 思路分析
 *    1.先创建一个List集合
 *    2.遍历resultSet对象的行数据
 *    3.将每一行数据存储到一个map对象中!
 *    4.将对象存到List中
 *    5.最终返回
 *注:如何获取列名?!!!!!!!!!
 * TODO:
 *    初体验,结果存储!
 *    学习获取结果表头信息(列名和数量等信息)
 */
@Test
public void testQueryMap() throws Exception{

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

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

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

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

    //发送查询语句
    ResultSet resultSet = preparedStatement.executeQuery();

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

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

    System.out.println(mapList);

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

4. 三元运算符与if判断的不同点

4.1 自增主键回显

  1. PreparedStatement加参数Statement.RETURN_GENERATED_KEYS
  2. statement.getGeneratedKeys()获取主键值
/**
 * 返回插入的主键!
 * 主键:数据库帮助维护的自增长的整数主键!
 * @throws Exception
 */
@Test
public void  returnPrimaryKey() throws Exception{

    //1.注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");
    //2.获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?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();
}

4.2 批量插入

  1. url加参数 rewriteBatchedStatements=true
  2. insert 语句必写 values ,且后面不加;
  3. 装货addBatch()最后executeBatch()
/**
 *改动了三处:(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:///atguigu?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();
}

4.3 事务使用

事务类型

  • 自动提交 : 每条语句自动存储一个事务中,执行成功自动提交,执行失败自动回滚! (MySQL),推荐
  • 手动提交: 手动开启事务,添加语句,手动提交或者手动回滚即可。不推荐

自动提交步骤:

  1. connection.setAutoCommit(false); 关闭自动提交
  2. commit 或 rollback
// 呼应jdbc技术 
  try{
    connection.setAutoCommit(false); //关闭自动提交。相当于执行SET autocommit = off
    //注意,只要当前connection对象,进行数据库操作,都不会自动提交事务
    //数据库动作!
    //statement - 单一的数据库动作 c u r d 
    //connection - 操作事务 
    connection.commit();
  }catch(Execption e){
    connection.rollback();
  }

5. 连接池

数据库连接池用于管理及复用数据库连接。

JDBC 的数据库连接池使用 javax.sql.DataSource 接口进行规范,所有的第三方连接池都实现此接口。也就是说,所有连接池获取连接的和回收连接方法都一样,不同的只有性能和扩展功能。

产品比对:

  • DBCP 是Apache提供的数据库连接池,速度相对c3p0较快,但因自身存在BUG
  • C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以
  • Proxool 是sourceforge 下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
  • Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池,推荐使用
# druid连接池需要的配置参数,key固定命名
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=root
url=jdbc:mysql://localhost:3306/database
/**
 * 不直接在java代码编写配置文件
 * 利用工厂模式,传入配置文件对象,创建连接池
 */
@Test
public void druidSoft() throws Exception {
    Properties properties = new Properties();
    InputStream ips = DruidDemo.class.getClassLoader().getResourceAsStream("druid.properties");
    properties.load(ips);
    DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
    Connection connection = dataSource.getConnection();
    // JDBC的步骤 正常curd
    // 连接池连接的close方法是回收连接,而不是关闭连接
    connection.close();
}
配置 缺省 说明
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。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter
driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下)
initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8 最大连接池数量
maxIdle 8 已经不再使用,配置了也没效果
minIdle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxOpenPreparedStatements -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testWhileIdle false 建议配置为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,是组合关系,并非替换关系

6. JDBC优化及工具类封装

6.1 JDBCTools

利用ThreadLocal完成线程级Connection重用

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

6.2 高级应用层封装BaseDao

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

针对DQL查询和非DQL进行,分成两类

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();
        //这里检查下是否开启事务,开启不关闭连接,业务方法关闭!
        //没有开启事务的话,直接回收关闭即可!
        if (connection.getAutoCommit()) {
            //回收
            JDBCTools.free();
        }
        return len;
    }

    /*
    通用的查询多个Javabean对象的方法,例如:多个员工对象,多个部门对象等
    这里的clazz接收的是T类型的Class对象,
    如果查询员工信息,clazz代表Employee.class,
    如果查询部门信息,clazz代表Department.class,
    返回List 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);
    }
}

你可能感兴趣的:(#,01,JAVA基础,java)