JDBC 基础 与 其在 MyBatis 的里使用

191 阅读5分钟

JDBC 简介

JDBC代表Java数据库连接(Java Database Connectivity),它是用于Java编程语言和数据库之间的数据库无关连接的标准Java API,换句话说:JDBC是用于在Java语言编程中与数据库连接的API。

而其中常用的有以下几个API

  • 连接到数据库
  • 创建SQL或MySQL语句
  • 在数据库中执行SQL或MySQL查询
  • 查看和修改结果记录

JDBC 简单使用

获取到JDBC连接后,下一步我们就可以查询数据库了。查询数据库分以下几步:

  • 第一步,获取数据库连接,DriverManager驱动管理器通过getConnection方法返回连接,在方法内部主要就是循环遍历所有的已注册的驱动程序,尝试建立连接,如果建立连接成功,就返回这个连接;
  • 第二步,通过Connection提供的prepareStatement()方法创建一个Statement对象,用于执行一个查询;
  • 第三步,执行Statement对象提供的executeQuery/executeUpdate并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集;
  • 第四步,反复调用ResultSetnext()方法并读取每一行结果。

以下是简单查询方法的代码

package com.lzf.learnjava.jdbc;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class LearnJdbc {
    // JDBC连接的URL, 不同数据库有不同的格式:
    private static String JDBC_URL = "jdbc:mysql://localhost:3372/default?useSSL=false&characterEncoding=utf8";
    private static String JDBC_USER = "default";
    private static String JDBC_PASSWORD = "secret";

    public List<Student> getStudentList() {
        List<Student> studentList = new ArrayList<>();
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
            ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?");
            // 注意:索引从1开始
            ps.setObject(1, 1);
            ps.setObject(2, 1);
            rs = ps.executeQuery();
            while (rs.next()) {
                Student studentInfo = new Student();
                Integer id = rs.getInt( "id");
                String name = rs.getString("name");
                Integer grade = rs.getInt("grade");
                studentInfo.setId(id);
                studentInfo.setName(name);
                studentInfo.setGrade(grade);
                studentList.add(studentInfo);
            }
        } catch (SQLException e){
            System.out.println("数据库异常:" + e.getMessage());
        } finally {
          // 关闭资源
            try{
                if(null != rs) rs.close();
                if(null != ps) ps.close();
                if(null != conn) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return studentList;
    }
}

JDBC 事务

Connection conn = openConnection();
try {
    // 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条SQL语句:
    insert(); update(); delete();
    // 提交事务:
    conn.commit();
} catch (SQLException e) {
    // 回滚事务:
    conn.rollback();
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

其中,开启事务的关键代码是conn.setAutoCommit(false),表示关闭自动提交。提交事务的代码在执行完指定的若干条SQL语句后,调用conn.commit()。要注意事务不是总能成功,如果事务提交失败,会抛出SQL异常(也可能在执行SQL语句的时候就抛出了),此时我们必须捕获并调用conn.rollback()回滚事务。最后,在finally中通过conn.setAutoCommit(true)Connection对象的状态恢复到初始值。

思考: 使用 JDBC 事务 如何再多个方法内部共用一个事务,使其能够达到同时提交,同时回滚? 关键:将链接存放在ThreadLocal中 blog.csdn.net/daijin88888…

现在我们日常开发时用的注解 @Transactional来给一个方法加上事务,这个属于声明式事务是由Spring 使用 AOP代理的方式实现的,这个会在后面深入学习。

JDBC 连接池

在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。

而连接池的实现通常是使用一个全局变量存储DataSource作为连接池实例。创建DataSource时会传入配置库的各种信息。

 conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
 
 // 改为从连接池获取链接
 
 conn = ds.getConnection();

当我们在finally调用conn.close()方法时,不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。

JDBC连接池有一个标准的接口javax.sql.DataSource,注意这个类位于Java标准库中,但仅仅是接口。要使用JDBC连接池,我们必须选择一个JDBC连接池的实现。常用的JDBC连接池有:

  • HikariCP
  • C3P0
  • BoneCP
  • Druid (目前我们用的是这个)

网上参考资料:

java各种数据库连接池对比

对比各大数据库连接池技术

功能全面的Druid

  • 提供性能卓越的连接池功能
  • 还集成了sql监控,黑名单拦截等功能,用它自己的话说,druid是“为监控而生”
  • druid另一个比较大的优势,就是中文文档比较全面(毕竟是国人的项目么)在github的wiki页面

性能无敌的HikariCP

  • 字节码精简:优化代码,直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码
  • 优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一
  • 自定义数组类型(FastStatementList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描
  • 自定义集合类型(ConcurrentBag):提高并发读写的效率
  • 其他针对BoneCP缺陷的优化,比如对于耗时超过一个CPU时间片的方法调用的研究

以上讲的是传统JDBC编程给我们带来了连接数据库的功能,但其工作量相对较大,首先连接,然后处理JDBC底层事务,处理数据类型,还要对可能产生的异常进行捕捉处理并正确的关闭资源,所以实际工作中,很少使用JDBC进行编程。

MyBatis

而我们常用的就是 MyBatis。ORM:Object-Relational Mapping。ORM 既可以把记录转换成Java对象,也可以把 Java 对象转换为行记录。MyBatis 是一个半自动 ORM 映射框架,因为其在查询关联对象或关联集合对象时,需要手动编写 sql 来完成。

public void testMyBatis() throws Exception {
    String resource = "mybatis-config.xml";
    InputStream resourceAsStream = Resources.getResourceAsStream(resource);

    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    // 生成 session 实例
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 获取mapper 代理类
    QcEngineerDAO mapper = sqlSession.getMapper(QcEngineerDAO.class);

    // 查询数据
    QcEngineerPO qcEngineerPO = mapper.selectByPrimaryKey(1);

    System.out.println(qcEngineerPO);
    
    Object o =  sqlSession.selectOne("com.xxxx.dao.mapper.QcEngineerDAO.selectByPrimaryKey", 1);
    System.out.println(o);


}

image.png

image.png

  • Configuration MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中
  • SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
  • Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  • StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等
  • ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所对应的数据类型
  • ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
  • TypeHandler 负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换
  • MappedStatement MappedStatement维护一条<select|update|delete|insert>节点的封装
  • SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
  • BoundSql 表示动态生成的SQL语句以及相应的参数信息

image.png

XML 的解析

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    SqlSessionFactory var5;
    try {
        MybatisXMLConfigBuilder parser = new MybatisXMLConfigBuilder(inputStream, environment, properties);
        var5 = this.build(parser.parse());
    } catch (Exception var14) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
    } finally {
        ErrorContext.instance().reset();

        try {
            inputStream.close();
        } catch (IOException var13) {
        }

    }

    return var5;
}

public Configuration parse() {
    if (this.parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    } else {
        this.parsed = true;
        this.parseConfiguration(this.parser.evalNode("/configuration"));
        return this.configuration;
    }
}

private void parseConfiguration(XNode root) {
    try {
        this.propertiesElement(root.evalNode("properties"));
        Properties settings = this.settingsAsProperties(root.evalNode("settings"));
        this.loadCustomVfs(settings);
        this.loadCustomLogImpl(settings);
        this.typeAliasesElement(root.evalNode("typeAliases"));
        this.pluginElement(root.evalNode("plugins"));
        this.objectFactoryElement(root.evalNode("objectFactory"));
        this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
        this.settingsElement(settings);
        this.environmentsElement(root.evalNode("environments"));
        this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        this.typeHandlerElement(root.evalNode("typeHandlers"));
        this.mapperElement(root.evalNode("mappers"));
    } catch (Exception var3) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
    }
}

我们可以看出这个方法是对的所有子标签挨个解析。比如常在配置文件中出现的settings属性配置,在settings会配置缓存,日志之类的。还有typeAliases是配置别名。environments是配置数据库链接和事务。这些子节点会被一个个解析并且把解析后的数据封装在Configuration 这个类中,可以看第二步方法的返回值就是Configuration对象。在这里我们重点分析的解析mappers这个子标签,这个标签里面还会有一个个的mapper标签去映射mapper所对应的mapper.xml,可以回头看看主配置文件

然而又是我们去看我们的项目代码时却看不到该标签,这个是为什么呢?

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
   <!-- 全局参数 -->
   <settings>
      <!-- 使全局的映射器启用或禁用缓存。 -->
      <setting name="cacheEnabled" value="true" />
      <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
      <setting name="lazyLoadingEnabled" value="false" />
      <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
      <setting name="aggressiveLazyLoading" value="false" />
      <!-- 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true -->
      <setting name="multipleResultSetsEnabled" value="true" />
      <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
      <setting name="useColumnLabel" value="true" />
      <!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false -->
      <setting name="useGeneratedKeys" value="false" />
      <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部 -->
      <setting name="autoMappingBehavior" value="PARTIAL" />
      <!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新) -->
      <setting name="defaultExecutorType" value="REUSE" />
      <!-- 使用驼峰命名法转换字段。 -->
      <setting name="mapUnderscoreToCamelCase" value="true" />
      <!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
      <setting name="localCacheScope" value="SESSION" />
      <!-- 设置当JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型 -->
      <setting name="jdbcTypeForNull" value="NULL" />
      <!--设定Mapper文件SQL打印日志前缀 -->
      <setting name="logPrefix" value="mapper." />
      <setting name="logImpl" value="STDOUT_LOGGING" />

    </settings>

   <typeAliases>
      <package name="xxxx.po"/>
   </typeAliases>
   <plugins>
      <plugin interceptor="com.github.pagehelper.PageInterceptor">
         <property name="helperDialect" value="mysql" />
         <property name="offsetAsPageNum" value="true" />
      </plugin>
   </plugins>
</configuration>

原因是我们使用的是Spring项目,用MyBatis-Spring会将MyBatis整合到Spring当中,之后通过注解@MapperScan 扫描生成bean 的方式注入到Spring中的。

image.png

到底引用JDBC的地方在哪里呢

selectOne (org.apache.ibatis.session.SqlSession)
 ->selectList (org.apache.ibatis.session.defaults.DefaultSqlSession)
    ->executor.query (org.apache.ibatis.executor.BaseExecutor)
      ->queryFromDatabase  (org.apache.ibatis.executor.BaseExecutor)
        -> doQuery (org.apache.ibatis.executor.SimpleExecutor)
         -> query (org.apache.ibatis.executor.statement.PreparedStatementHandler)
          -> execute (java.sql.PreparedStatement)

以上是mybatis 一次查询的主要链路, 可以看到最后的 execute 引用的正式JDBC jar 里的 execute方法。 这个层级找得我好苦呀!

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement)statement;
    ps.execute();
    return this.resultSetHandler.handleResultSets(ps);
}

执行完语句之后就是将结果集进行处理。ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。

总结:
1、JDBC 基础知识学习
2、JDBC 常用连接池
3、MyBatis 执行流程以及对JDBC的使用

参考资料:

zhuanlan.zhihu.com/p/97879019