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来引用这个结果集;- 第四步,反复调用
ResultSet的next()方法并读取每一行结果。
以下是简单查询方法的代码
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 (目前我们用的是这个)
网上参考资料:
功能全面的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);
}
- 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语句以及相应的参数信息
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中的。
到底引用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的使用
参考资料: