JDBC学习笔记(二)
前面我们学习了获取数据库连接、使用JDBC中Statement与PreparedStatement实现增删改查,今天我们将继续学习JDBC相关的知识。
1.批处理
当需要成批插入或者更新记录时,可以采用Java的批量更新机制。这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更加具有效率。
通常会遇到两种批量执行SQL语句的情况:
- 多条SQL语句的批量处理
- 一个SQL语句的批量传参
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TestBatch {
//没有使用批处理来处理成批插入或者更新记录
@Test
public void test01() throws ClassNotFoundException, SQLException {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/school","root","root");
//3.编写sql语句
String sql="insert into books values(null,?,?)";
//4.创建prepareStatement对象
PreparedStatement pst = connection.prepareStatement(sql);
for (int i = 0; i < 100; i++) {
pst.setObject(1,"书籍名称"+i);
pst.setObject(2,i);
//5.执行sql语句
pst.executeUpdate();
}
//6.断开连接
pst.close();
connection.close();
}
//使用批处理来处理成批插入或者更新记录
@Test
public void test02() throws ClassNotFoundException, SQLException {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/school?rewriteBatchedStatements=true", "root", "root");
//3.编写sql语句
String sql="insert into books values(null,?,?)";
//4.创建PrepareStatement
PreparedStatement pst = connection.prepareStatement(sql);
//5.执行sql语句
for (int i = 0; i < 100; i++) {
pst.setObject(1,"书籍名称"+i);
pst.setObject(2,i);
//JDBC批处理需要使用的方法1:addBatch()方法,即添加需要处理的SQL语句或者参数
pst.addBatch();
}
//JDBC批处理需要使用的方法2:executeBatch()方法,执行批量处理语句
pst.executeBatch();
//6.断开连接
pst.close();
connection.close();
}
}
注意:JDBC连接MySQL时,如果要使用批处理功能,需要在url中加入参数?rewriteBatchedStatements=true PreparedStatement作批处理"插入"时使用values。
2.使用JDBC处理事务
JDBC程序中当一个连接对象被创建时,默认情况下是自动提交事务:每次执行一个SQL语句时,如果执行成功,就会向数据库中自动提交,而不能进行回滚。
JDBC程序中将让多个SQL语句作为一个事务执行:
- 调用Connection对象的setAutoCommit(flase); 取消自动提交事务
- 在所有的SQL语句中都成功执行后,调用commit(); 提交事务
- 在其中某个操作失败或者出现异常时,调用rollback();回滚事务
- 若此时Connection没有被彻底关闭,还可能被重复使用。则需要恢复自动提交状态setAutoCommit(true);
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TestTransaction {
Connection connection=null;
@Test
public void test() throws ClassNotFoundException, SQLException {
try {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/school", "root", "root");
//3.关闭默认自动提交事务,设置手动提交事务
connection.setAutoCommit(false);
//4.编写sql语句
String sql1="update books set bname = ? where bid=?";
String sql2="update books set price=? where bid=?";
//5.创建PrepareStatement对象,并执行sql语句
PreparedStatement pst1 = connection.prepareStatement(sql1);
pst1.setObject(1,"西游记");
pst1.setObject(2,1);
int len1= pst1.executeUpdate();
System.out.println(len1>0?"sql语句1更改数据成功":"sql语句1更改数据失败");
pst1.close();
PreparedStatement pst2 = connection.prepareStatement(sql2);
pst2.setObject(1,300);
pst2.setObject(2,3);
int len2 = pst2.executeUpdate();
System.out.println(len2 > 0 ? "sql语句2更改数据成功" : "sql语句2更改数据失败");
pst2.close();
//6.一个整体的事务写完后吗,将事务进行提交
connection.commit();
} catch (Exception e) {
try {
if (connection!=null) {
connection.rollback();
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
//最终无论如何都会执行
finally {
try {
if (connection!=null) {
//在释放连接之前,恢复自动提交事务
connection.setAutoCommit(true);
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
运行后的结果为:
需要注意的是:如果多个操作,每个操作都是自己单独的链接,则无法保证事务。即同一个事务的多个操作必须在同一个连接下。
3.数据库连接池
3.1 为什么要使用数据库连接池
不使用数据库连接池存在的问题:
-
普通的JDBC数据库连接使用DriverManager来获取,每次向数据库建立连接的时候需要向Connection加载到内存中,再验证IP地址,用户名与密码。需要数据库连接时,就向数据库请求一个,执行完成后再断开连接。数据库的连接资源没有获得很好的重复利用。
-
对于每一次数据库连接,使用结束后都需要断开。否则,如果程序出现异常而没有关闭,将会导致数据库系统中的内存泄露。
-
使DriverManager来获取数据库连接,这种方式不能控制被创建的连接对象数。
为解决传统开发中的数据库连接池问题,可以采用数据库连接技术。
3.2 数据库连接池的基本思想和特点
数据库连接池的基本思想就是为数据库连接创建一个"缓冲池"。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从"缓冲池"中取出一个,使用完毕之后再放回去。
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新创建一个。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将会被加入到等队列中。
数据库连接池技术的特点:
- 统一的连接管理,避免数据库连接泄露
在较为完整的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用的连接,从而避免了常规数据库连接操作中可能出现的资源泄露。
- 资源重用
由于数据库连接可以重用,避免了频繁创建、释放连接引起的大量性能开销。
- 更快的系统反应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接。对于业务请求而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间。
3.3 多种开源的数据库连接池–着重介绍Druid
JDBC的数据库连接池使用java.sql.DataSource来表示,DataSource只是一个接口,该接口通常是由服务器提供实现。
下面是常用数据库连接池的介绍。
常用数据库连接池介绍 |
---|
DBCP是Apache提供的数据库连接池,相对于C3P0较快,但自身存在一些bug |
C3P0是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以 |
Proxool是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较C3P0差 |
BoneCP 是一个开源组织提供的数据库连接池,速度快 |
Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池 |
DataSource通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上将DataSource称为连接池。
需要注意的是:
-
(1)数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。
-
(2)当数据库访问结束后,程序还是像之前一样关闭数据库连接:connection.close();但connection().close()并没有关闭数据库的物理连接,它仅仅把数据库连接释放掉,归还给数据库连接池。
Druid(德鲁伊)数据源
Druid是阿里巴巴开源平台上一个数据库连接池实现,它结合了C3P0、DBCP、Proxool等DB池的优点,同时具有日志监控,可以很好的监控数据据库连接和SQL的执行情况。
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.junit.Test;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Properties;
public class TestDruid {
@Test
public void test() throws Exception {
Properties properties = new Properties();
//使用反射读取配置文件
properties.load(TestDruid.class.getClassLoader().getResourceAsStream("druid.properties"));
//使用Druid数据源工厂创建数据源
DataSource ds = DruidDataSourceFactory.createDataSource(properties);
Connection connection = ds.getConnection();
System.out.println(connection);
System.out.println(ds);
}
}
配置文件druid.properties内容如下:
配置文件中详细配置参数 | 参数解释 |
---|---|
jdbc url | 连接数据库的url,不同数据库不一样。例如mysql数据库:jdbc:mysql://localhost:3306/数据库名 |
username | 连接数据库的用户名 |
password | 连接数据库的密码 |
driverClassName | 驱动类名,根据url自动识别,这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的DriverClassName(建议配置下) |
initialSize | 初始化建立物理连接的个数。默认值为0 |
maxActive | 最大连接池数量。默认值为8 |
minldle | 最小连接池数量 |
maxWait | 获取连接时最大等待时间,单位为毫秒。 |
4.封装JDBCTools版本1(V1.0)
封装JDBCTools版本V1.0
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
public class JDBCToolsV1 {
//将数据库的连接池对象设置为静态变量
private static DataSource ds;
static {
//静态代码块
try {
Properties pro = new Properties();
pro.load(JDBCToolsV1.class.getClassLoader().getResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}
//获取连接的方法
//抛出编译时异常
public static Connection getConnection() throws SQLException {
//方式一:使用DriverManager.getConnection()
//方式二:连接池对象.getConnection()
return ds.getConnection();
}
//关闭连接的方法
public static void free(Connection connection){
try {
if (connection!=null) {
connection.close();
}
} catch (SQLException e) {
//把编译时异常转换为运行时异常
throw new RuntimeException(e);
}
}
//注意:这个方法只能用于不需要处理事务的情况
//通用的更新的方法:增加、修改、删除
public static int update(String sql,Object... args) throws SQLException {
//获取连接
Connection connection = getConnection();
//创建PrepareStatemnt对象
PreparedStatement pst = connection.prepareStatement(sql);
//设置?
if (args!=null&&args.length>0) {
for (int i = 0; i < args.length; i++) {
pst.setObject(i+1,args[i]);
}
}
//执行sql语句
int len = pst.executeUpdate();
pst.close();
free(connection);
return len;
}
public static int update(Connection connection,String sql,Object...args) throws SQLException {
//创建PrepareStatement对象
PreparedStatement pst = connection.prepareStatement(sql);
//设置?
if (args!=null&&args.length>0) {
for (int i = 0; i < args.length; i++) {
pst.setObject(i+1,args[i]);
}
}
//执行sql语句
int len = pst.executeUpdate();
pst.close();
return len;
}
}
配置文件druid.properties的具体配置为:
测试JDBCToolsV1类中update方法以及处理事务
import org.junit.Test;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TestJDBCToolsV1 {
//测试JDBCToolsV1类中的update方法
@Test
public void test01() throws SQLException {
//添加一本书籍到school库的books表中
String sql="insert into books values(4,?,?)";
int len = JDBCToolsV1.update(sql, "红楼梦", 300);
System.out.println(len>0?"添加数据成功":"添加成功失败");
}
//测试JDBCToolsV1类处理事务
@Test
public void test02() throws SQLException {
String sql1="update books set bname= 'JAVA从入门到放弃' where price=50 ";
String sql2="update books set bname='数据库原理' where bid=3";
//希望这两条sql语句构成一个事务
Connection connection = JDBCToolsV1.getConnection();
//将事务自动提交进行关闭
connection.setAutoCommit(false);
//创建PrepareStatement对象
PreparedStatement pst1 = connection.prepareStatement(sql1);
PreparedStatement pst2 = connection.prepareStatement(sql2);
//执行sql语句
int len1 = pst1.executeUpdate();
int len2 = pst2.executeUpdate();
try {
if (len1>0&&len2>0) {
//对事务进行提交
connection.commit();
}else{
connection.rollback();
}
} catch (SQLException e) {
connection.rollback();
}
pst1.close();
pst2.close();
//重新设置为自动提交
connection.setAutoCommit(true);
JDBCToolsV1.free(connection);
}
@Test
public void test03() throws SQLException {
String sql1="update books set bname= 'JAVA从入门到放弃' where price=50 ";
String sql2="update books set bname='数据库原理' where bid=3";
//希望这两条sql语句构成一个事务
Connection connection = JDBCToolsV1.getConnection();
//将事务自动提交进行关闭
connection.setAutoCommit(false);
try {
int len1= JDBCToolsV1.update(connection,sql1);
int len2= JDBCToolsV1.update(connection,sql2);
if (len1>0&&len2>0) {
connection.commit();
}else{
connection.rollback();
}
} catch (SQLException e) {
connection.rollback();
}
//将事务又重新设置为自动提交
connection.setAutoCommit(true);
JDBCToolsV1.free(connection);
}
}
测试JDBCToolsV1类中update方法的运行结果
测试JDBCToolsV1类事务处理的结果
5.ThreadLocal
ThreadLocal用于保存某个线程的共享变量,原因在于JAVA中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal, Object>,其key就是一个ThreadLocal,而Object即为该线程的共享变量。而这个map是通过ThreadLocal的set和get方法操作的。对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
import java.util.Random;
public class TestTools{
public static void main(String[] args) {
MyThread m1 = new MyThread();
m1.start();
MyThread m2 = new MyThread();
m2.start();
}
}
class Tools {
private static Random rand=new Random();//产生随机数的工具类
//这里设置的是当前线程保存的共享变量,这个共享变量不是我们之前说的多个线程之间共享,说的是同一个线程在整个生命周期中,使用的共享变量。
private static ThreadLocal<Integer> th=new ThreadLocal<Integer>();
public static void setNum(){
th.set(rand.nextInt(100));
}
public static int getNum(){
return th.get();
}
}
class MyThread extends Thread{
@Override
public void run() {
Tools.setNum();
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
int num = Tools.getNum();
System.out.println(Thread.currentThread().getName()+":"+num);
}
}
}
运行后的结果:
6.封装JDBCTools版本2(V2.0)
封装JDBCTools版本V2.0
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.westos.demo3.JDBCToolsV1;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
public class JDBCToolsV2 {
//将数据库的连接池对象设置为静态变量
private static DataSource ds;
private static ThreadLocal<Connection> th;
static {
//静态代码块
try {
Properties pro = new Properties();
pro.load(JDBCToolsV1.class.getClassLoader().getResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
th = new ThreadLocal<>();
} catch (Exception e) {
e.printStackTrace();
}
}
//获取连接的方法
//抛出编译时异常
public static Connection getConnection() throws SQLException {
//方式一:使用DriverManager.getConnection()
//方式二:连接池对象.getConnection()
Connection connection = th.get();//获取当前线程中的共享的连接对象
if (connection == null) { //当前线程没有拿过连接,第一次获取连接
connection = ds.getConnection();//从连接池中拿一个新的
th.set(connection);//放到当前线程共享变量之中
}
return connection;
}
//关闭连接的方法
public static void free(Connection connection) {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
//把编译时异常转换为运行时异常
throw new RuntimeException(e);
}
}
public static int update(String sql, Object... args) throws SQLException {
Connection connection = getConnection();
PreparedStatement pst = connection.prepareStatement(sql);
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
pst.setObject(i + 1, args[i]);
}
}
int len = pst.executeUpdate();
pst.close();
return len;
}
}
测试JDBCV2工具类中的事务处理
public class JDBCToolsV2 {
//将数据库的连接池对象设置为静态变量
private static DataSource ds;
private static ThreadLocal<Connection> th;
static {
//静态代码块
try {
Properties pro = new Properties();
pro.load(JDBCToolsV1.class.getClassLoader().getResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
th = new ThreadLocal<>();
} catch (Exception e) {
e.printStackTrace();
}
}
//获取连接的方法
//抛出编译时异常
public static Connection getConnection() throws SQLException {
//方式一:使用DriverManager.getConnection()
//方式二:连接池对象.getConnection()
Connection connection = th.get();//获取当前线程中的共享的连接对象
if (connection == null) { //当前线程没有拿过连接,第一次获取连接
connection = ds.getConnection();//从连接池中拿一个新的
th.set(connection);//放到当前线程共享变量之中
}
return connection;
}
//关闭连接的方法
public static void free(Connection connection) {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
//把编译时异常转换为运行时异常
throw new RuntimeException(e);
}
}
public static int update(String sql, Object... args) throws SQLException {
Connection connection = getConnection();
PreparedStatement pst = connection.prepareStatement(sql);
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
pst.setObject(i + 1, args[i]);
}
}
int len = pst.executeUpdate();
pst.close();
return len;
}
}
运行后的结果为:
7.封装的BasicDAOImpl
DAO:Data Access Object访问数据信息的类和接口,包括了对数据的CRUD(Create、Retrival、Update、Delete),而不包含任何业务相关的信息
作用:为了实现功能的模块化,更有利于代码的维护和升级。
import java.lang.reflect.Field;
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 com.atguigu.utils.JdbcUtils;
public class BasicDAOImpl {
//适用于insert,update,delete语句
public int update(String sql,Object... args) {
//1、获取连接
Connection conn = JdbcUtils.getConnection();
//2、创建PreparedStatement
PreparedStatement pst = null;
try {
pst = conn.prepareStatement(sql);
//3、设置?的值
if(args!=null && args.length>0){
for (int i = 0; i < args.length; i++) {//数组的下标从0开始
pst.setObject(i+1, args[i]);//?的序号从1开始
}
}
//4、执行更新sql
return pst.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}finally{
try {
//5、关闭
pst.close();
JdbcUtils.free();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//T可能代表Department,Employee等各种Javabean的对象
//clazz是决定你查询的结果是Department,Employee类型中的那个对象
public <T> ArrayList<T> getBeanList(Class<T> clazz, String sql,Object... args){
//1、获取连接
Connection conn = JdbcUtils.getConnection();
//2、创建PreparedStatement
PreparedStatement pst = null;
try {
pst = conn.prepareStatement(sql);
//3、设置?的值
if(args!=null && args.length>0){
for (int i = 0; i < args.length; i++) {//数组的下标从0开始
pst.setObject(i+1, args[i]);//?的序号从1开始
}
}
//4、执行查询
ResultSet rs = pst.executeQuery();
//获取结果集的元数据对象,该对象有对结果集的数据进行描述的相关信息
ResultSetMetaData rsm = rs.getMetaData();
//(1)获取结果集的列数
int count = rsm.getColumnCount();
ArrayList<T> list = new ArrayList<T>();
//5、把ResultSet结果集中的数据封装到一个一个JavaBean对象中,并且存到list中
while(rs.next()){//循环一次,代表一行,一行就是一个JavaBean对象
//(2)创建一个JavaBean的对象
T obj = clazz.newInstance();
//有几列,就代表有几个属性
//为obj的每一个属性赋值
for (int i = 0; i < count; i++) {
//通过反射设置属性的值
//(3)从结果集的元数据对象中获取第几列的字段名
String columnName = rsm.getColumnLabel(i+1);//mysql的序号从1开始
//(4)获取属性对象
Field field = clazz.getDeclaredField(columnName);//根据字段名称,获取属性对象
//(5)设置属性可以被访问
field.setAccessible(true);
//(6)设置属性的值
field.set(obj, rs.getObject(i+1));//从结果集中获取第i+1的值,赋值给该属性
}
list.add(obj);
}
//7、返回结果
return list;
} catch (Exception e) {
throw new RuntimeException(e);
}finally{
try {
//6、关闭
pst.close();
JdbcUtils.free();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public <T> T getBean(Class<T> type,String sql, Object... params) {
return getBeanList(type,sql,params).get(0);
}
}
总结
本节对JDBC的批处理,使用JDBC进行处理事务,数据库连接池、JDBC工具类以及封装的BasicDAOImpl进行介绍,需要对其进行重点掌握。