文章目录
- JDBC
- 概念
- 优势
- 总结
- JDBC核心api和使用路线
- 涉及具体核心类和接口
- DriverManager
- Connection
- statement、preparedstatement、callablestatement
- Result
- 核心API
- 使用步骤总结
- 基于statement演示查询
- 基于statement方式问题
- 基于preparedstatement的优化
- 基于preparedstatement的curd操作
- preparedstatement使用方法总结
- 使用步骤
- 使用API总结
- JDBC扩展提升
- 主键回显
- 批量插入性能提升
- JDBC中数据库事务实现
- 事务
- 事务概念
- 优势
- 性质
- 事务类型
- 案例
- 代码结构设计
- 代码
- 总结
- Druid连接池技术
- 目前存在的问题
- 数据库连接池技术
- 多种开源的数据库连接池
- Druid的使用
- 改进Druid连接池的使用(工具类的封装)
- 工具类封装v1.0
- 工具类封装v2.0
- 高级应用层封装BaseDao
- 非DQL语句
- DQL语句
JDBC
概念
JDBC(Java Database Connectivity)Java数据库连接技术,在Java代码中,使用JDBC提供的方法,可以发送字符串类型的SQL语句到数据库管理软件,并获取语句的执行结果。
优势
我们只需要学习JDBC接口的规定方法,即可操作所有数据库软件
项目中需要切换数据库,我们只需要更新第三方驱动jar包,不需要更改代码
总结
1、JDBC是Java连接数据库技术的统称
2、JDBC由两部分组成
①一是Java提供的JDBC的规范(接口)
②各个数据库厂商的实现驱动jar包
3、JDBC技术是典型的面向接口的编程
JDBC核心api和使用路线
涉及具体核心类和接口
DriverManager
1、将第三方数据库厂商的实现驱动jar注册到程序中
2、可以根据数据库连接信息获取connection
Connection
1、和数据库建立的连接,在连接对象上,可以执行多次数据库curd动作
2、可以获取statement、preparedstatement、callablestatement对象
statement、preparedstatement、callablestatement
1、具体发送SQL语句到数据库管理软件的对象
2、不同发送方式稍有不同!preparedstatement使用为重点
Result
1、面共享对象思维的产物
2、存储DQL查询数据库结果的对象
3、需要我们进行解析,获取具体的数据库数据
核心API
使用步骤总结
1、注册驱动:把依赖的jar包,进行安装
2、建立Java程序与数据库之间的连接
3、创建发送SQL语句的对象(statement)
4、statement对象,发送SQL语句到数据库,并获取返回结果
5、解析结果集(resultset)
6、释放资源(连接、statement对象、resultset)
基于statement演示查询
1、创建一个数据库:atguigu
2、创建一个表t_user,并插入2条数据
3、通过IDEA根据前面的六个步骤,实现连接数据库,并输出表t_user中的内容
mysql> 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 '昵称');
mysql>INSERT INTO t_user(account,PASSWORD,nickname)
-> VALUES('root','123456','经理'),
-> ('admin','666666','管理员');
mysql>SELECT * FROM t_user;
+----+---------+----------+----------+
| id | account | PASSWORD | nickname |
+----+---------+----------+----------+
| 1 | root | 123456 | 经理 |
| 2 | admin | 666666 | 管理员 |
+----+---------+----------+----------+
Java代码:
package com.atguigu.api;
import com.mysql.cj.jdbc.Driver;
import java.sql.*;
/**
* 查询t_user数据表下全部的数据
*/
public class statement {
/**
* API:
* DriverManager
* Connection
* Statement
* ResultSet
*
* @param args
*/
public static void main(String[] args) throws SQLException {
//1、注册驱动
//创建依赖:如果版本是8+,应该导入com.mysql.cj.jdbc.Driver这个包(加了cj的)
//如果版本是8+,应该导入com.mysql.jdbc.Driver这个包(不加cj的)
DriverManager.registerDriver(new Driver());
//2、获取链接
//肯定要包含数据库的基本信息:数据库ip地址(127.0.0.1)、数据库端口号、账号、密码、连接数据库的名称
/**
* 参数1:
* 格式: jdbc:数据库厂商名://IP地址:port/数据库名
* 参数2和3:数据库软件的账号和密码
* 注意:如果是在本机上安装mysql,并且没有更改端口号3306时,可以使用省略写法:
* jdbc:数据库厂商名:///数据库名
*/
//java.sql下的 接口=实现类
Connection connection=
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/atguigu","root","hahaha");
//3、创建statement
Statement statement= connection.createStatement();
//4、发送sql语句,并获取返回结果
String sql="SELECT * 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();
}
}
输出结果为:
基于statement方式问题
模拟登录数据库,在控制台上输入账户和密码,判断是否登陆成功
public class StatementUserLogin {
/**
* 模拟用户登录
*
* 明确JDBC的使用流程,并详细讲解API
* 发现问题,引入PreparedStatement
*
* 步骤:
* 输入用户、密码
* 查询数据库信息
* 返回登陆成功/失败的信息
*/
public static void main(String[] args) throws SQLException, ClassNotFoundException {
//1.获取用户输入信息
Scanner scanner=new Scanner(System.in);
System.out.println("输入账户:");
String account=scanner.nextLine();
System.out.println("输入密码:");
String password=scanner.nextLine();
//2.注册驱动
/**
* 方案1:DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
* 问题:会注册两次驱动
* 1、DriverManager.registerDriver()方法本身会注册一次
* 2、Driver内部的静态代码块也会注册一次
* 解决:只注册一次
* 即:只触发Driver中的静态代码块即可
* 触发静态代码块:
* 类加载机制:类加载的时刻,会出发静态代码块
* 加载[class文件——>jvm虚拟机的class对象]
* 连接[验证(检查文件类型)->准备(静态变量默认值)->解析(出发静态代码块)]
* 初始化(静态属性赋真实值)
* 触发类加载:
* 1.new关键字
* 2.调用静态方法
* 3.调用静态属性
* 4.接口1.8 default默认实现
* 5.反射
* 6.子类触发父类
* 7.程序的入口main
*
*/
//DriverManager.registerDriver(new Driver());
//方案2:字符串->提取到外部的配置文件->可以在不改变代码的情况下,完成数据库驱动的切换
//使用Class类的中静态forName()方法获得与字符串相应的Class对象
Class.forName("com.mysql.cj.jdbc.Driver");//触发类加载,调用静态代码块
//2.建立连接
//getConnection()方法是一个重载方法,允许开发者以不同方式输入连接数据库的参数
//核心属性:1.数据库软件所在的IP地址 2.端口号 3.连接的库名 4.账号、密码
//通常选择三个参数的
/**
* 两个参数:
* String url:和三个参数的url一样
* Properties info:写账号和密码,Properties类似于Map,只不过key和value都是字符串类型
*
*/
Properties properties = new Properties();
properties.put("user","root");
properties.put("PASSWORD","hahaha");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/atguigu", properties);
/**
* 一个参数:jdbc:mysql://127.0.0.1:3306/atguigu?user=user&password=password
*
* 可选属性:
* 8.0.27版本中,下面是可选属性:
* serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true
* 8.0.25版本后,自动识别时区,可省略:serverTimezone=Asia/Shanghai
* 8版本后,默认使用utf8格式,可省略:useUnicode=true&characterEncoding=utf8&useSSL=true
*
*/
//Connection connection1 = DriverManager.getConnection("jdbc.mysql://127.0.0.1:3306/atguigu?user=root&password=hahaha");
//3.创建statement对象,statement可以发送SQL语句,并获取返回结果
Statement statement = connection.createStatement();
//4.发送SQL语句(编写语句、发送)
String sql="SELECT * FROM t_user WHERE account='"+account+"' AND PASSWORD='"+password+"';";
/**
* SQL分类:DDL(容器创建、修改、删除)、DML(插入、修改、删除)、DQL(查询)、DCL(权限控制)、TPL(事务控制语言)
*
* 参数:sql 非DQL
* 返回:int
* 情况1:DML 返回影响的行数,例如:删除了三条数据——return 3
* 情况2:非DML 返回0
*int row=executeUpdate(sql);
*
* 参数:sql DQL
* 返回:ResultSet 结果封装对象
* ResultSet resultset=executequery(sql);
*
*/
ResultSet resultSet=statement.executeQuery(sql);
//5.查询结果集解析 resultSet
/**
* resultSet:逐行获取数据,先拿到行,再拿到列
* 想要进行数据解析,需要两件事情:
* 1、移动游标获取指定数据行
* ResultSet包含一个游标
* 默认位置是第一行数据之前
* 每次可以调用next方法,向后移动一行,若有多行数据,可以使用while循环
* 移动光标的方法很多,只需记next与while即可
*
* 2、获取数据行的列数据(获取光标指定行的数据)
* resultSet.get类型(String columnLabel | int columnIndex)
* columnLabel:列名 如果有别名,写别名
* columnIndex:列的下角标获取,从左到右,从1开始
*/
// while(resultSet.next()){
// int id=resultSet.getInt(1);
// String account1 = resultSet.getString("account");
// String password1 = resultSet.getString(3);
// String nickname = resultSet.getString("nickname");
// System.out.println("nickname"+nickname);
// }
//只要能移动光标一次,就代表登陆成功
if(resultSet.next()){
System.out.println("登陆成功!");
}else{
System.out.println("登陆失败!");
}
//6.关闭资源
resultSet.close();
statement.close();
connection.close();
}
}
存在的问题:
1、字符串拼接比较麻烦。例子中查询语句的用户名和密码都是字符串拼接形成
2、并且在拼接过程中只支持字符串类型,其他类型不支持
3、可能发生注入攻击,即,动态值充当了SQL语句结构,影响了原有的查询结果,假如在输入密码时输入:
’ or '1'='1'
意思就是,password为空,因为输入这个语句后,password=’ ‘,然后又添加了一个条件’1’=‘1’,这个条件是true,所以输入后会显示登陆成功
基于preparedstatement的优化
import java.sql.*;
import java.util.Scanner;
/**
* 使用预编译的statement(preparedstatement)完成用户登录
* 防止注入攻击
* 演示preparedstatement的使用流程,以下简称ps
*/
public class UserLogin {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//1.收集用户信息
Scanner scanner=new Scanner(System.in);
System.out.println("请输入账号:");
String account =scanner.nextLine();
System.out.println("请输入密码:");
String password =scanner.nextLine();
//2.preparedstatement的流程
//①注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//②获取链接
Connection connection= DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=root");
//③编写SQL语句
/**
* statement
* 1.创建statement
* 2.拼接SQL语句
* 3.发送SQL语句,并且获取返回结果
*
* preparedstatement
* 1.缩写SQL语句,即不包括动态值部分的语句,动态值用占位符?代替
* 2.创建preparedstatement,并且传入动态值
* 3.对占位符?进行赋值
* 4.发送SQL语句,并获取返回值
*/
String sql="SELECT * FROM t_user WHERE account=? AND password=?;";
//④创建预编译preparedstatement
PreparedStatement preparedStatement=connection.prepareStatement(sql);
//⑤对每个占位符进行赋值
/**
*参数1:index——占位符的位置,从左向右,从1开始
* 参数2:object:占位符的值,可以设置任何类型数据,避免了拼接,而且类型更加丰富
*/
preparedStatement.setObject(1,account);
preparedStatement.setObject(2,password);
//6.发送SQL语句,并返回结果
//statement.executeUpdate | executeQuery(String sql);
//preparedstatement.executeUpdate | executeQuery(); 因为preparedstatement已经知道语句,且知道动态值
ResultSet resultSet= preparedStatement.executeQuery();
//7.解析结果集
if(resultSet.next()){
System.out.println("登陆成功!");
}else{
System.out.println("登陆失败!");
}
//8.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
基于preparedstatement的curd操作
import org.junit.Test;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 使用preparedstatement进行增删改查
*/
public class PsCurd {
//测试方法需要导入junit的测试包
@Test
public void testInsert() throws ClassNotFoundException, SQLException {
/**
* 插入一条数据:
* account test
* password test
* nickname 仙女
*/
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取链接
//连接成功的前提:打开MySQL80服务
Connection connection= DriverManager.getConnection("jdbc:mysql:///atguigu","root","hahaha");
//3.编写SQL语句,使用占位符?
String sql="INSERT INTO t_user(account,password,nickname)VALUES(?,?,?);";
//4.创建preparedstatement,传入SQL结构
PreparedStatement preparedStatement=connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,"test");
preparedStatement.setObject(2,"test");
preparedStatement.setObject(3,"仙女");
//6.发送SQL语句
int rows=preparedStatement.executeUpdate();
//7.输出结果
if(rows>0){
System.out.println("插入成功!");
}else{
System.out.println("插入失败!");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
@Test
public void testUpdate() throws ClassNotFoundException, SQLException {
/**
* 修改id=3的用户nickname=Queen
*/
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取链接
Connection connection=DriverManager.getConnection("jdbc:mysql:///atguigu","root","hahaha");
//3.编写SQL语句结果,动态值的部分用?代替
String sql="UPDATE t_user set nickname=? AND password=?;";
//4.创建preparedstatement,并传入SQL语句
PreparedStatement preparedStatement=connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,"Queen");
preparedStatement.setObject(2,3);
//6.发送SQL语句
int i=preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("修改成功");
}else{
System.out.println("修改失败");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
@Test
public void testDelete() throws ClassNotFoundException, SQLException {
/**
* 删除id=3的数据
*/
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取链接
Connection connection=DriverManager.getConnection("jdbc:mysql:///atguigu","root","hahaha");
//3.编写SQL语句结果,动态值的部分用?代替
String sql="DELETE FROM t_user WHERE id=?;";
//4.创建preparedstatement,并传入SQL语句
PreparedStatement preparedStatement=connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,3);
//6.发送SQL语句
int i=preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("删除成功");
}else{
System.out.println("删除失败");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
@Test
public void testSelect() throws ClassNotFoundException, SQLException {
/**
* 目标:查询所有用户数据,并且封装到一个List<Map> List集合中
* 实现思路:
* 遍历行数据,一行对应一个map,获取一行的列名和对应的属性
* 将map装到一个集合
*
* 难点:获取列的名称
*/
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取链接
Connection connection=DriverManager.getConnection("jdbc:mysql:///atguigu","root","hahaha");
//3.编写SQL语句结果,动态值的部分用?代替
String sql="SELECT id,account AS ac FROM t_user ;";
//4.创建preparedstatement,并传入SQL语句
PreparedStatement preparedStatement= connection.prepareStatement(sql);
//5.占位符赋值(省略)
//6.发送SQL语句
ResultSet resultSet = preparedStatement.executeQuery();
//6.1解析结果集
/**
* 回顾:
* resultSet:有行有列,获取数据时是一行为单位
* 内部有游标,指向第一行数据之前
* 可以用next()方法移动游标
*
*
*/
//方法一:纯手动取值(不推荐)
// List<Map> list=new ArrayList<>();
// while(resultSet.next()){
// Map map=new HashMap();
// map.put("id",resultSet.getInt("id"));
// map.put("account",resultSet.getString("account"));//resultSet.getString("account")代表取这一行中account的值
// map.put("password",resultSet.getString("password"));
// map.put("nickname",resultSet.getString("nickname"));
// list.add(map);
// }
//方法二:自动遍历列
//获取列的信息对象
ResultSetMetaData metaData=resultSet.getMetaData();
//获取列数
int columnCount = metaData.getColumnCount();
List<Map> list=new ArrayList<>();
while(resultSet.next()) {
Map map = new HashMap();
//列数从1开始
for(int i=1;i<=columnCount;i++){
//获取指定的第i列的值————ResultSet
Object object = resultSet.getObject(i);
//获取第i列的列名————ResultSetMetaData
//getColumnLabel:获取别名,如果没有别名,获取列的名称
//getColumnName(不要使用):只能获取列的名称
String columnLabel = metaData.getColumnLabel(i);
//map中,列的名称作为key,一行数据中该列存储的值作为value
map.put(columnLabel,object);
}
list.add(map);
}
//7.输出结果
System.out.println("list="+list);
//8.关闭资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
运行结果:
preparedstatement使用方法总结
使用步骤
1.注册驱动
2.建立连接
3.编写SQL语句
4.创建preparedstatement对象,并传入SQL语句
5.占位符赋值
6.发送SQL语句,并获取结果集
7.(如果是查询语句)解析结果集
8.关闭资源
使用API总结
- 注册驱动
//方式1:调用静态方法,但会注册两次
DriverManager drivermanager=DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
//方式2:反射
Class.forName("com.mysql.cj.jdbc.Driver");
- 获取连接
Connection connection=DriverManager.getConnection("jdbc:mysql:///atguigu","root","password");
//getConnection是重载方法
//方法1
connection=DriverManager.getConnection(String url,String user,String password);
//方法2
connection=DriverManager.getConnection(String url,Properties info(user,password));
//方法3
connection=DriverManager.getConnection(String url?user=账号&password=密码);
-
编写SQL语句
-
创建statement
//静态
Statement statement=connection.createStatement();
//预编译,preparedstatement是statement的一个子类
PreparedStatement preparedstatement=connection.preparedstatement(sql语句结构);
- 占位符赋值
preparedstatment.setObject(?位置,值);
//位置是从左到右,从1开始
- 发送sql语句,获取结果
//非DQL
int rows=preparedstatement.executeUpdate();
//DQL
ResultSet resultSet=preparedstatement.executeQuery();
- 结果及解析
//①移动光标 next()
next()
//获取列的数据 get类型(列的下角标),从左到右,从1开始
Object object = resultSet.getObject(i);
//获取列的信息
ResultSetMetaData metaData=resultSet.getMetaData();
//获取列的数量
int columnCount = metaData.getColumnCount();
- 关闭资源
close();
JDBC扩展提升
主键回显
数据库一般会将逐渐设置为int类型并且自增长,但是在一些程序中需要得到自增长的主键,拿到主键的方法就是主键回显
作用举例:在多表关联的情况下,一般主表的主键都是自动生成的,所以在插入数据之前无法获得这条数据的主键,但从表需要在插入数据之前就绑定主表的主键
public class PsOther {
/**
* 在t_user中插入一条数据,并获取其自增长的主键
*
* 使用总结:
* 1、在创建statement对象,说明要带回数据库自增长的主键(sql,Statement.RETURN_GENERATED_KEYS)
* 2、获取装主键结果集,其结构是一行一列的,使用ResultSet resultSet = preparedStatement.getGeneratedKeys();获取即可
*/
@Test
public void retuenPrimaryKey() throws ClassNotFoundException, SQLException {
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取链接
Connection connection= DriverManager.getConnection("jdbc:mysql:///atguigu","root","hahaha");
//3.编写SQL语句
String sql="INSERT INTO t_user(account,password,nickname) VALUES(?,?,?);";
//4.创建statement对象
PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
//5.占位符赋值
preparedStatement.setObject(1,"test1");
preparedStatement.setObject(2,"123456");
preparedStatement.setObject(3,"Superman");
//6.发送SQL语句
int i = preparedStatement.executeUpdate();
//7.结果解析
if(i>0){
System.out.println("success");
//可以获取回显的主键,获取装主键的结果集,是一行一列的
ResultSet resultSet = preparedStatement.getGeneratedKeys();
resultSet.next();//先将光标移动到这行数据上
int id = resultSet.getInt(1);//获取主键值
System.out.println("id="+id);
}else{
System.out.println("default");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
运行结果:
mysql> SELECT * FROM t_user;
+----+---------+----------+----------+
| id | account | PASSWORD | nickname |
+----+---------+----------+----------+
| 1 | root | 123456 | 经理 |
| 2 | admin | 666666 | 管理员 |
| 3 | test | test | 仙女 |
| 4 | test1 | 123456 | Superman |
+----+---------+----------+----------+
批量插入性能提升
/**
* 使用批量插入的方式插入10000条数据
*
* 总结:
* 1、连接中的url部分添加 ?rewriteBatchedStatements=true 代表允许批量插入
* 2、INSERT INTO语句中 必须写 VALUES,而且语句的末尾不能写分号;结束
* 3、每条语句不是执行,是批量添加 addBatch()
* 4、在所有数据添加完毕后,统一批量执行 executeBatch()
*/
@Test
public void testBatchInsert() throws ClassNotFoundException, SQLException {
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取链接
Connection connection= DriverManager.getConnection("jdbc:mysql:///atguigu?rewriteBatchedStatements=true","root","hahaha");
//3.编写SQL语句
String sql="INSERT INTO t_user(account,password,nickname) VALUES(?,?,?)";
//4.创建statement对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
long start = System.currentTimeMillis();
//5.占位符赋值
for(int i=0;i<1000;i++){
preparedStatement.setObject(1,"ddd"+i);
preparedStatement.setObject(2,"ddd"+i);
preparedStatement.setObject(3,"ddd"+i);
preparedStatement.addBatch();//不执行,追加到values后
}
//6.发送SQL语句
preparedStatement.executeBatch();//批量执行操作
long end = System.currentTimeMillis();
//7.结果解析
System.out.println("运行时间为"+(end-start));
//8.关闭资源
preparedStatement.close();
connection.close();
}
JDBC中数据库事务实现
事务
事务概念
数据库事务就是一种SQL语句的缓存机制,不会单条执行完毕就更新数据库,最终根据缓存内的多条语句执行结果统一判定
一个事务内所有语句都执行成功,即事务成功,可以触发commit提交事务来结束事务,更新数据
一个事务内任意一条语句都执行失败,即事务失败,触发回滚结束事务,数据回到事务之前的状态
优势
允许在失败的情况下,回到之前的状态
性质
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
隔离性(Isolation)
事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
事务类型
自动提交(推荐):每条语句自动存储在一个事务中,执行成功自动提交,执行失败自动回滚
手动提交:手动开启事务,添加语句,手动提交或回滚
一个事务的基本要求,必须是同一个链接对象connection
案例
一个账户给另一个账户转钱
代码结构设计
DAO中存储某个表中数据操作的方法
代码
1、创建表
CREATE TABLE t_bank(
-> id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
-> account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
-> money INT UNSIGNED COMMENT '金额,不能为负值');
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO t_bank(account,money) VALUES('ergouzi',1000),('lvdandan',1000);
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM t_bank;
+----+----------+-------+
| id | account | money |
+----+----------+-------+
| 1 | ergouzi | 1000 |
| 2 | lvdandan | 1000 |
+----+----------+-------+
2、Javadaima实现
/**
* bank业务方法,调用BankDAO
*/
public class BankService {
/**
*在transfer中建立事务,因为加钱减钱需要在一个事务中,而transfer调用了这两个方法
*
* 同时还要注意,在创建事务后,事务中的方法也不应该再各自创建链接,而应该使用事务的链接
* 同样,在每个方法中也不能再有关闭连接的语句,连接由事务统一关闭
*/
public void transfer(String addAccount,String subAccount,int money) throws SQLException, ClassNotFoundException {
//1.建立驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.创建链接
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "hahaha");
//对事务进行操作
try{
//开启事务(关闭事务提交,关闭后,后面的内容会自动加到一个事务里)
connection.setAutoCommit(false);
//执行数据库操作
BankDao bankDao=new BankDao();
bankDao.add(addAccount,money,connection);
System.out.println("------------");
bankDao.sub(subAccount,money,connection);
//事务提交
connection.commit();
}catch (Exception e){
//事务回滚
connection.rollback();
//抛出异常信息
throw e;
}finally {
connection.close();
}
}
@Test
public void start() throws SQLException, ClassNotFoundException {
/**
* ergouzi给lvdandan转账500元
*/
transfer("ergouzi","lvdandan",500);
}
}
/**
* bank表中数据操作方法的存储类
*/
public class BankDao {
/**
* 加钱的操作方法
* @param account 被加钱的账号
* @param money 金额
*/
public void add(String account,int money,Connection connection) throws ClassNotFoundException, SQLException {
// //1.注册驱动
// Class.forName("com.mysql.cj.jdbc.Driver");
// //2.获取链接
// Connection connection= DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=hahaha");
//3.编写SQL语句
String sql="UPDATE t_bank SET money=money+? WHERE account=?;";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送SQL语句
int i = preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
// connection.close();
System.out.println("加钱成功");
}
/**
* 减钱的操作方法
* @param account 被减钱的账号
* @param money 金额
*/
public void sub(String account,int money,Connection connection) throws ClassNotFoundException, SQLException {
// //1.注册驱动
// Class.forName("com.mysql.cj.jdbc.Driver");
// //2.获取链接
// Connection connection= DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=hahaha");
//3.编写SQL语句
String sql="UPDATE t_bank SET money=money-? WHERE account=?;";
//4.创建statement
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,account);
//6.发送SQL语句
int i = preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
// connection.close();
System.out.println("减钱成功");
}
}
总结
1、利用try catch代码块,开启事务,提交事务,事务回滚
2、事务中涉及的方法,统一使用事务创建的连接,不能各自创建链接
3、事务中的每个方法,最后不能关闭连接,由事务统一关闭,在try……catch……finally的 finally中
Druid连接池技术
节约了创建和销毁链接的性能消耗,提升了时间的响应
JDBC的数据库连接池使用 javax.sql.DataSource 接口进行规范,所有第三方连接池都是用此接口
目前存在的问题
- 普通的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码(得花费0.05s~1s的时间)。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有得到很好的重复利用。若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃。
- 对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。
- 这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。
数据库连接池技术
为解决传统开发中的数据库连接问题,可以采用数据库连接池技术。
数据库连接池的基本思想:就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。
多种开源的数据库连接池
JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:
DBCP 是Apache提供的数据库连接池。tomcat 服务器自带dbcp数据库连接池。速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持。
C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以。hibernate官方推荐使用
Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
BoneCP 是一个开源组织提供的数据库连接池,速度快
Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池,但是速度不确定是否有BoneCP快
DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接池
DataSource用来取代DriverManager来获取Connection,获取速度快,同时可以大幅度提高数据库访问速度。
特别注意:
数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。
当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但conn.close()并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。
Druid的使用
推荐使用配置文件的方法,即软编码,好处是,可以通过对配置文件的修改,来修改连接池的属性
硬编码方式:直接使用代码设置连接池参数
/**
* 使用硬连接方式,即用代码设置连接池属性
*
* 步骤:
* 1、创建一个druid连接池对象
* 2、设置连接池参数
* 3、获取链接(通用方法,所有连接池都一样)
* 4、回收链接(通用方法,所有连接池都一样)
*/
@Test
public void testHard() throws SQLException {
//1、创建一个druid连接池对象
DruidDataSource druidDataSource=new DruidDataSource();
//2、设置连接池参数
//必须:连接数据库的全限定符(创建驱动)、url、user、password
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql:///atguigu");
druidDataSource.setUsername("root");
druidDataSource.setPassword("hahaha");
//非必须:初始化连接数量 最大连接数……
druidDataSource.setInitialSize(5);
druidDataSource.setMaxActive(10);
//3、获取链接(通用方法,所有连接池都一样)
DruidPooledConnection connection = druidDataSource.getConnection();
//4、回收链接(通用方法,所有连接池都一样)
connection.close();
}
软编码方式:
在src下新建一个File,名字随意取,但是后缀必须是properties,这样可以被Java中的Properties读取
内容是:key=value形式
注意:
1、druid配置的key要固定命名(因为在读取过程中,读取的是特定名称,所以要和读取的特定名称相同,才能读取成功)
2、在使用连接池的工厂模式时,注意选择第一个
/**
* 通过读取外部配置文件的方式,获取Druid连接池对象
*
* 步骤:
* 1、读取外部配置文件到Properties
* 2、使用连接池的工具类的工程模式,创建连接池
*/
@Test
public void testSoft() throws Exception {
//1、读取外部配置文件到Properties
Properties properties=new Properties();
//src下的文件,可以使用类加载器提供的方法
InputStream ips = DruidUse.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(ips);
//2、使用连接池的工具类的工厂模式,创建连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
Connection connection = dataSource.getConnection();
connection.close();
}
改进Druid连接池的使用(工具类的封装)
工具类封装v1.0
/**
* v1.0版本的工具类
* 内部包含一个连接池对象,并且对外提供获取链接和回收链接的方法
*
* 小建议:
* 工具类提供的方法,推荐写成静态的,外部调用会更加方便
*
* 实现:
* 属性 连接池对象 只实例化一次(可以使用 单例模式、静态代码块 实现)
* 方法 对外提供链接的方法
* 回收外部传入链接的方法
*/
public class JDBCutils {
private static DataSource dataSource=null;//连接池对象
static{
//连接池对象初始化
Properties properties=new Properties();
InputStream ips = JDBCutils.class.getClassLoader().getResourceAsStream("druid.properties");
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
DataSource dataSource1 = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 对外提供连接的方法
* @return
*/
public static Connection getConnect() throws SQLException {
return dataSource.getConnection();
}
/**
* 回收链接的方法
* @return
*/
public static void freeConnect(Connection connection) throws SQLException {
connection.close();
}
}
使用方法:
/**
* 基于工具类的CURD
*/
public class JDBCCurd {
public void testInsert() throws SQLException {
//1、通过工具类获取链接
Connection connection=JDBCutils.getConnect();
//2、数据库的CURD
//3、通过工具类实现链接的回收
JDBCutils.freeConnect(connection);
}
}
存在问题,以BankDao为例,如果在BankDao中调用工具类,会获取一个链接,但是在具体的方法中,比如add方法中,再次调用工具类的getConnect方法,还会获取一个链接,但是两个链接并不是同一个,而我们希望两个位置在调用工具类获取链接时,能获取同一个链接
工具类封装v2.0
希望在同一个线程中的不同方法中调用工具类,能够获得同一个链接
需要使用ThreadLocal(线程本地变量),作用是为同一个线程存储共享变量
/**
* v2.0版本的工具类
* 利用线程本地变量ThreadLocal,存储连接信息,确保同一个线程的多个方法获取同一个链接
*/
public class JDBCutilsv2 {
private static DataSource dataSource=null;//连接池对象
private static ThreadLocal<Connection> tl=new ThreadLocal<>();
static{
//连接池对象初始化
Properties properties=new Properties();
InputStream ips = JDBCutilsv2.class.getClassLoader().getResourceAsStream("druid.properties");
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
DataSource dataSource1 = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 对外提供连接的方法
* @return
*/
public static Connection getConnect() throws SQLException {
//查看线程本地变量中是否存在
Connection connection=tl.get();
//第一次是没有连接的,需要通过连接池获取一下,并向线程本地变量中传入
if(connection==null){
connection=dataSource.getConnection();
tl.set(connection);
}
return connection;
}
/**
* 回收链接的方法
* @return
*/
public static void freeConnect() throws SQLException {
Connection connection=tl.get();
if(connection!=null){
tl.remove();//清空线程本地变量数据
connection.setAutoCommit(true);//事务状态回归,默认状态是true,开启事务时将其设置为了false
connection.close();
}
}
}
高级应用层封装BaseDao
非DQL语句
/**
* 封装两个方法:
* 一个是简化非DQL
* 一个简化DQL
*/
public abstract class BaseDao {
/**
* 封装简化非DQL语句
* @param sql 带占位符的语句
* @param params 占位符的值
* @return 执行影响的行数
*/
public int executeUpdate(String sql,Object... params) throws SQLException {
//获取链接
Connection connection=JDBCutilsv2.getConnect();
PreparedStatement preparedStatement=connection.prepareStatement(sql);
//占位符赋值
//可变参数可以当作数组使用
for(int i=1;i<=params.length;i++){
preparedStatement.setObject(i,params[i]);
}
//发送SQL语句,DML类型
int i = preparedStatement.executeUpdate();
preparedStatement.close();
//是否回收连接,需要考虑是不是事务
if(connection.getAutoCommit()){
//没有开启事务,正常回收链接
JDBCutilsv2.freeConnect();
//若开启事务,则不用管,让事务去回收
}
return i;
}
}
引入BaseDao后的应用:
/**
* 使用preparedstatement进行增删改查
*/
public class PsCurd extends BaseDao {
//测试方法需要导入junit的测试包
@Test
public void testInsert() throws ClassNotFoundException, SQLException {
/**
* 插入一条数据:
* account test
* password test
* nickname 仙女
*/
String sql="INSERT INTO t_user(account,password,nickname)VALUES(?,?,?);";
int rows = executeUpdate(sql, "测试333", "333", "ergouzi");
System.out.println("i="+rows);
}
@Test
public void testUpdate() throws ClassNotFoundException, SQLException {
/**
* 修改id=3的用户nickname=Queen
*/
String sql="UPDATE t_user set nickname=? AND password=?;";
int rows = executeUpdate(sql, "new nickname", 3);
}
@Test
public void testDelete() throws ClassNotFoundException, SQLException {
/**
* 删除id=3的数据
*/
String sql="DELETE FROM t_user WHERE id=?;";
int i = executeUpdate(sql, 3);
}
DQL语句
BaseDao中的方法:
/**
* 非DQL语句封装方法——返回值固定为int
* DQL语句封装方法——返回值是某个类型的集合 :List<T>
*
*
* @param clazz 实体类集合的对象
* @param sql 查询语句,要求数据的列名或列的别名和对Java象的属性名相同
* @param params 占位符的值
* @return 实体类对象的集合
* @param <T>
* @throws SQLException
* @throws InstantiationException
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
public <T> List<T> executeQuery(Class<T> clazz, String sql, Object... params) throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//获取链接
Connection connection = JDBCutilsv2.getConnect();
PreparedStatement preparedStatement= connection.prepareStatement(sql);
//占位符赋值
if(params!=null&¶ms.length!=0){
for(int i=1;i<=params.length;i++){
preparedStatement.setObject(i,params[i-1]);
}
}
//发送SQL语句
ResultSet resultSet = preparedStatement.executeQuery();
//1.解析结果集
//获取列的信息对象
ResultSetMetaData metaData=resultSet.getMetaData();
//获取列数
int columnCount = metaData.getColumnCount();
List<T> list=new ArrayList<>();
while(resultSet.next()) {
//一行数据对应一个T类型的对象
T t=clazz.newInstance();//调用类的无参构造函数实例化对象
//列数从1开始
for(int i=1;i<=columnCount;i++){
//对象的属性值
Object value = resultSet.getObject(i);
//对象的属性名
String propertyName = metaData.getColumnLabel(i);
//通过反射,给对象t的属性值赋值
Field field = clazz.getDeclaredField(propertyName);//先通过属性名找到对象t对应的属性
field.setAccessible(true);//打破可能存在的private的修饰限制,使t属性可以赋值
/**
* 参数1:需要赋值的对象,若属性是静态的,可以为null
* 参数2:要赋的值
*/
field.set(t,value);//将属性值赋给t中对应的属性
}
list.add(t);
}
//关闭资源
resultSet.close();
preparedStatement.close();
if(connection.getAutoCommit()){
JDBCutilsv2.freeConnect();
}
return list;
}