JDBC
第1章 原生JDBC
1.1 JDBC概述
JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API。JDBC是Java访问数据库的标准规范,可以为不同的关系型数据库提供统一访问,它由一组用Java语言编写的接口和类组成。
JDBC需要连接驱动,驱动是两个设备要进行通信,满足一定通信数据格式,数据格式由设备提供商规定,设备提供商为设备提供驱动软件,通过软件可以与该设备进行通信。
JDBC规范(掌握四个核心对象):
- DriverManager:用于注册驱动
- Connection: 表示与数据库创建的连接
- Statement: 操作数据库sql语句的对象
- ResultSet: 结果集或一张虚拟表
1.2 JDBC原理
Java提供访问数据库规范称为JDBC,而生产厂商提供规范的实现类称为驱动。
JDBC是接口,驱动是接口的实现,没有驱动将无法完成数据库连接,从而不能操作数据库!每个数据库厂商都需要提供自己的驱动,用来连接自己公司的数据库,也就是说驱动一般都由数据库生成厂商提供。
1.3 JDBC入门案例
准备数据
之前我们学习了sql语句的使用,并创建的分类表category,今天我们将使用JDBC对分类表进行增删改查操作。
#创建数据库
create database day04;
#使用数据库
use day04;
#创建分类表
create table category(
cid int PRIMARY KEYAUTO_INCREMENT ,
cname varchar(100)
);
#初始化数据
insert into category (cname) values('家电');
insert into category (cname) values('服饰');
insert into category (cname) values('化妆品');
导入驱动jar包(IDEA)
创建lib目录,存放mysql的驱动mysql-connector-java-5.1.37-bin.jar
选中mysql的jar包,右键选择“ Add as Library…” 完成jar导入
在java项目中,只需要引入mysql-connector-java-x.x.x-bin.jar就可以运行java项目。在web项目中,当Class.forName(“om.mysql.jdbc.Driver”);时eclipse是不会去查找字符串,不会去查找驱动的。所以只需要把mysql-connector-java-x.x.x-bin.jar拷贝到tomcat下lib目录就可以了。或者,放在WEB-INF目录下lib中
开发步骤
- 注册驱动.
- 获得连接.
- 获得执行sql语句的对象
- 执行sql语句,并返回结果
- 处理结果
- 释放资源.
案例实现
//查询所有的分类信息
@Test
public void demo1() throws Exception{
// 注意:使用JDBC规范,采用都是 java.sql包下的内容
//1 注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2 获得连接
String url = "jdbc:mysql://localhost:3306/mydb";
Connection conn = DriverManager.getConnection(url, "root", "root");
//3获得执行sql语句的对象
Statement stmt = conn.createStatement();
//4执行SQL语句
ResultSet rs = stmt.executeQuery("select * from category");
//5处理结果集
while(rs.next()){
// 获得一行数据
Integer cid = rs.getInt("cid");
String cname = rs.getString("cname");
System.out.println(cid + " , " + cname);
}
//6释放资源
rs.close();
stmt.close();
conn.close();
}
1.4 API详解
API详解:注册驱动
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
不建议使用,原因有2个:
(1)导致驱动被注册2次
(2)强烈依赖数据库的驱动jar
- 解决办法:
Class.forName("com.mysql.jdbc.Driver");
- 注意:
注册JDBC驱动6.0
后使用Class.forName("com.mysql.cj.jdbc.Driver");
注册JDBC驱动5.0
后使用Class.forName("com.mysql.jdbc.Driver");
两个版本的区别:首先驱动换了,不是com.mysql.jdbc.Driver而是’com.mysql.cj.jdbc.Driver’ ; 再时连接数据库地址有变化mysql8.0是不需要建立ssl连接的,你需要显示关闭,即url中的&useSSL=false;
API详解:获得链接
static Connection getConnection(String url, String user, String password)
:试图建立到给定数据库 URL的连接。
- 参数说明:
- url 需要连接数据库的位置(网址)
- user用户名
- password 密码
- 例如:
getConnection("jdbc:mysql://localhost:3306/day04", "root", "root");
- url数据库地址
String url="jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Hongkong&characterEncoding=utf-8&autoReconnect=true";
连接地址+ssl连接关闭+时区为Hongkong+字符集为utf-8+数据库自动连接
扩展:
URL:SUN公司与数据库厂商之间的一种协议。
jdbc:mysql://localhost:3306/day04
协议子协议 IP :端口号数据库 mysql: jdbc:mysql://localhost:3306/day04 或者 jdbc:mysql:///day04(默认
本机连接) oracle数据库: jdbc:oracle:thin:@localhost:1521:sid
API详解:java.sql.Connection接口:一个连接
接口的实现在数据库驱动中。所有与数据库交互都是基于连接对象的。
Statement createStatement();
创建操作sql语句的对象
API详解:java.sql.Statement接口: 操作sql语句,并返回相应结果
String sql = "某SQL语句";
获取Statement语句执行平台:Statement stmt =con.createStatement();
常用方法:
int executeUpdate(String sql);
--执行insert update delete语句.ResultSet executeQuery(String sql);
--执行select语句.boolean execute(String sql); --仅当执行select
并且有结果时才返回true,执行其他的语句返回false.
API详解:处理结果集(注:执行insert、update、delete无需处理)
ResultSet实际上就是一张二维的表格,我们可以调用其boolean next()
方法指向某行记录,当第一次调用next()
方法时,便指向第一行记录的位置,这时就可以使用ResultSet提供的 getXXX(int col)
方法来获取指定列的数据:(与数组索引从0开始不同,这里索引从1开始)
rs.next();//指向第一行
rs.getInt(1);//获取第一行第一列的数据
常用方法:
Object getObject(int index) / Object getObject(String name)
获得任意对象String getString(int index) / String getString(String name)
获得字符串int getInt(int index) / int getInt(String name)
获得整形double getDouble(int index) / double getDouble(String name)
获得双精度浮点型
API详解:释放资源
与IO流一样,使用后的东西都需要关闭!关闭的顺序是先得到的后关闭,后得到的先关闭。
rs.close();
stmt.close();
con.close();
1.5 JDBC工具类
获得数据库连接
操作,将在以后的增删改查所有功能中都存在,可以封装工具类JDBCUtils。提供获取连接对象的方法,从而达到代码的重复利用。
该工具类提供方法:public static Connection getConnection()
。代码如下:
public class JdbcUtils {
private static String driver = "com.mysql.jdbc.Driver";
private static String url = "jdbc:mysql://localhost:3306/webdb_4";
private static String user = "root";
private static String password = "root";
static{
try {
//注册驱动
Class.forName(driver);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 获得连接
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException{
//获得连接
Connection conn = DriverManager.getConnection(url, user, password);
return conn;
}
/**
* 释放资源
* @param conn
* @param st
* @param rs
*/
public static void closeResource(Connection conn , Statement st , ResultSet rs){
if(rs != null){
try {
rs.close();
} catch (SQLException e) {
}
}
if(st != null){
try {
st.close();
} catch (SQLException e) {
}
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
}
}
}
}
1.6 JDBC增删改查操作
插入
@Test
public void demo01(){
//添加
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
//1 获得连接
conn = JdbcUtils.getConnection();
//操作
//1) 获得语句执行者
st = conn.createStatement();
//2) 执行sql语句
int r = st.executeUpdate("insert into category(cname) values('测试')");
//3) 处理结果
System.out.println(r);
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
//释放资源
JdbcUtils.closeResource(conn, st, rs);
}
}
修改
@Test
public void demo02(){
//修改
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
st = conn.createStatement();
int r = st.executeUpdate("update category set cname='测试2' where cid = 4");
System.out.println(r);
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
JdbcUtils.closeResource(conn, st, rs);
}
}
删除
@Test
public void demo03(){
//删除
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
//操作
st = conn.createStatement();
int r = st.executeUpdate("delete from category where cid = 4");
System.out.println(r);
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
JdbcUtils.closeResource(conn, st, rs);
}
}
通过id查询详情
@Test
public void demo04(){
//通过id查询详情
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
//操作
st = conn.createStatement();
rs = st.executeQuery("select * from category where cid = 30");
if(rs.next()){
String cid = rs.getString("cid");
String cname = rs.getString("cname");
System.out.println(cid + " @ " + cname );
} else {
System.out.println("没有数据");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
JdbcUtils.closeResource(conn, st, rs);
}
}
查询所有
@Test
public void demo05(){
//查询所有
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
//操作
st = conn.createStatement();
rs = st.executeQuery("select * from category");
while(rs.next()){
String cid = rs.getString("cid");
String cname = rs.getString("cname");
System.out.println(cid + " @ " + cname );
}
} catch (Exception e) {
throw new RuntimeException(e);
}finally{
JdbcUtils.closeResource(conn, st, rs);
}
}
第2章 PreparedStatement
2.1 SQL注入问题
SQL注入:用户输入的内容作为了SQL语句语法的一部分,改变了原有SQL真正的意义。 假设有登录案例SQL语句
如下:
SELECT * FROM 用户表 WHERE NAME = 用户输入的用户名 AND PASSWORD = 用户输的密码;
此时,当用户输入正确的账号与密码后,查询到了信息则让用户登录。但是当用户输入的账号为XXX 密码为:XXX’ OR ‘a’=’a
时,则真正执行的代码变为:
SELECT * FROM 用户表 WHERE NAME = ‘XXX’ AND PASSWORD =’ XXX’ OR ’a’=’a’;
此时,上述查询语句时永远可以查询出结果的。那么用户就直接登录成功了,显然我们不希望看到这样的结果,这
便是SQL注入问题。 为此,我们使用PreparedStatement来解决对应的问题。
2.2 API详解:预处理对象
preparedStatement:预编译对象,是Statement对象的子类。
特点:
- 性能高
- 会把sql语句先编译
- 能过滤掉用户输入的关键字。
PreparedStatement预处理对象,处理的每条sql语句中所有的实际参数,都必须使用占位符?替换。
String sql = "select * from user where username = ?
PreparedStatement使用,需要通过以下3步骤完成:
- PreparedStatement预处理对象代码:
// 获得预处理对象,需要提供已经使用占位符处理后的SQL语句
PreparedStatement psmt = conn.prepareStatement(sql)
- 设置实际参数
void setXxx(int index, Xxx xx) 将指定参数设置指定类型的值
参数1:index 实际参数序列号,从1开始。
参数2:xxx 实际参数值,xxx表示具体的类型。
例如:
setString(2, "1234") 把SQL语句中第2个位置的占位符?替换成实际参数 "1234"
- 执行SQL语句:
int executeUpdate(); --执行insert update delete语句.
``ResultSet executeQuery(); --执行select语句.
``boolean execute(); --执行select返回true 执行其他的语句返回false.
2.3 插入
@Test
public void demo01(){
//添加:向分类表中添加数据
Connection conn = null;
PreparedStatement psmt = null;
ResultSet rs = null;
try {
//1 获得连接
conn = JdbcUtils.getConnection();
//2 处理sql语句
String sql = "insert into category(cname) values(? )";
//3获得预处理对象
psmt = conn.prepareStatement(sql);
//4设置实际参数
psmt.setString(1,"预处理");
//5执行
int r = psmt.executeUpdate();
System.out.println(r);
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
//6释放资源
JdbcUtils.closeResource(conn, psmt, rs);
}
}
2.4 更新
@Test
public void demo02(){
//修改
Connection conn = null;
PreparedStatement psmt = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
//1 sql语句
String sql = "update category set cname = ? where cid = ?";
//2 获得预处理对象
psmt = conn.prepareStatement(sql);
//3设置实际参数
psmt.setString(1, "测试数据");
psmt.setInt(2, 4);
//4执行
int r = psmt.executeUpdate();
System.out.println(r);
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
JdbcUtils.closeResource(conn, psmt, rs);
}
}
2.5 通过id查询详情
@Test
public void demo05(){
//通过id查询
Connection conn = null;
PreparedStatement psmt = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "select * from category where cid = ?";
psmt = conn.prepareStatement(sql);
psmt.setInt(1, 2);
rs = psmt.executeQuery();
if(rs.next()){
System.out.println("查询到");
} else {
System.out.println("查询不到");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally{
JdbcUtils.closeResource(conn, psmt, rs);
}
}
第3章 使用连接池重写工具类
3.1 连接池原理
连接池理解为存放多个连接的集合。
使用连接池技术的目的:解决建立数据库连接耗费资源和时间很多的问题,提高性能。
3.2 编写标准的数据源(规范)
Java为数据库连接池提供了公共的接口:javax.sql.DataSource,各个厂商需要让自己的连接池实现这个接口。这样应用程序可以方便的切换不同厂商的连接池!
常见的连接池:C3P0、DRUID。
3.3 C3P0连接池
C3P0开源免费的连接池!目前使用它的开源项目有:Spring、Hibernate等。使用C3P0连接池需要导入jar包,c3p0使用时还需要添加配置文件“c3p0-config.xml”
使用步骤
- 添加jar包
- 编写配置文件 c3p0-config.xml,放在src中(注:文件名一定不要写错)
- 编写工具类
-
编写配置文件 c3p0-config.xml
<c3p0-config> <!-- 使用默认的配置读取连接池对象 --> <default-config> <!-- 连接参数 --> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/数据库名</property> <property name="user">root</property> <property name="password">root</property> <!-- 连接池参数 --> <property name="initialPoolSize">5</property> <property name="maxPoolSize">10</property> <property name="checkoutTimeout">2000</property> <property name="maxIdleTime">1000</property> </default-config> </c3p0-config>
c3p0连接池常用的配置参数:
参数 | 说明 |
---|---|
initialPoolSize | 初始连接数 |
maxPoolSize | 最大连接数 |
checkoutTimeout | 最大等待时间 |
maxIdleTime | 最大空闲回收时间 |
初始连接数
:刚创建好连接池的时候准备的连接数量
最大连接数
:连接池中最多可以放多少个连接
最大等待时间
:连接池中没有连接时最长等待时间
最大空闲回收时间
:连接池中的空闲连接多久没有使用就会回收
编写C3P0工具类
public class JdbcUtils {
//创建一个C3P0的连接池对象(使用c3p0-config.xml中default-config标签中对应的参数)
public static DataSource ds = new ComboPooledDataSource();
//从池中获得一个连接
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
//释放资源
public static void closeAll(ResultSet rs, Statement stmt, Connection conn){
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
rs = null;
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
stmt = null;
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
conn = null;
}
}
}
C3P0连接池工具类的使用
public class Demo {
public static void main(String[] args) throws Exception {
// 拿到连接
Connection conn = JdbcUtils.getConnection();
// 执行sql语句
String sql = "INSERT INTO student VALUES (NULL, ?, ?, ?);";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "李四");
pstmt.setInt(2, 30);
pstmt.setDouble(3, 50);
int i = pstmt.executeUpdate();
System.out.println("影响的函数: " + i);
// 关闭资源
JdbcUtils.closeAll(null,pstmt,conn);
}
}
第4章 DBUtils
如果只使用JDBC进行开发,我们会发现冗余代码过多,为了简化JDBC开发,本案例我们讲采用apache commons组件一个成员:DBUtils。
DBUtils就是JDBC的简化开发工具包。需要项目导入commons-dbutils-1.6.jar才能够正常使用DBUtils工具。
4.1 概述
DBUtils是java编程中的数据库操作实用工具,小巧简单实用。DBUtils封装了对JDBC的操作,简化了JDBC操作,可以少写代码。
Dbutils三个核心功能介绍
- QueryRunner中提供对sql语句操作的API.
- ResultSetHandler接口,用于定义select操作后,怎样封装结果集.
- DbUtils类,它就是一个工具类,定义了关闭资源与事务处理的方法
4.2 准备数据
-
创建表:
create table product( pid int primary key, pname varchar(20), price double, category_id varchar(32) );
-
插入表记录
INSERT INTO product(pid,pname,price,category_id) VALUES(1,'联想',5000,'c001'); INSERT INTO product(pid,pname,price,category_id) VALUES(2,'海尔',3000,'c001'); INSERT INTO product(pid,pname,price,category_id) VALUES(3,'雷神',5000,'c001'); INSERT INTO product(pid,pname,price,category_id) VALUES(4,'JACK JONES',800,'c002'); INSERT INTO product(pid,pname,price,category_id) VALUES(5,'真维斯',200,'c002'); INSERT INTO product(pid,pname,price,category_id) VALUES(6,'花花公子',440,'c002'); INSERT INTO product(pid,pname,price,category_id) VALUES(7,'劲霸',2000,'c002'); INSERT INTO product(pid,pname,price,category_id) VALUES(8,'香奈儿',800,'c003'); INSERT INTO product(pid,pname,price,category_id) VALUES(9,'相宜本草',200,'c003'); INSERT INTO product(pid,pname,price,category_id) VALUES(10,'面霸',5,'c003'); INSERT INTO product(pid,pname,price,category_id) VALUES(11,'好想你枣',56,'c004'); INSERT INTO product(pid,pname,price,category_id) VALUES(12,'香飘飘奶茶',1,'c005'); INSERT INTO product(pid,pname,price,category_id) VALUES(13,'果9',1,NULL);
4.3 QueryRunner核心类介绍
提供数据源
-
构造方法
QueryRunner(DataSource)
创建核心类,并提供数据源,内部自己维护Connection并自动关闭 -
普通方法
update(String sql , Object ... params)
执行DML语句query(String sql , ResultSetHandler , Object ... params)
执行DQL语句,并将查询结果封装到对象中。
提供连接
-
构造方法
QueryRunner()
创建核心类,没有提供数据源,在进行具体操作时,需要手动提供/关闭Connection -
普通方法
update(Connection conn , String sql , Object ... params)
使用提供的Connection,完成DML语
句
query(Connection conn , String sql , ResultSetHandler , Object ... params)
使用提供的Connection,执行DQL语句,并将查询结果封装到对象中。 -
静态方法
DbUtils.closeQuietly(conn);
关闭Connection资源,不抛异常
4.4QueryRunner实现添加、更新、删除操作
update(String sql, Object... params)
用来完成表数据的增加、删除、更新操作
添加
@Test
public void insert() throws SQLException{
//获取一个用来执行SQL语句的对象 QueryRunner
QueryRunner qr = new QueryRunner(C3P0Utils.getDataSource());
String sql = "INSERT INTO product(pid,pname,price,category_id) VALUES(?,?,?,?);";
Object[] params = {100,"百岁山", 5500, "c005"};
int line = qr.update(sql,params);// 用来完成表数据的增加、删除、更新操作
//结果集处理
System.out.println("line = " + line);
}
更新
@Test
public void update() throws SQLException{
//1 核心类
QueryRunner queryRunner = new QueryRunner(C3P0Utils.getDataSource());
//2 准备sql语句
String sql = "update product set pname=?,price=?,category_id=? where pid=?";
//3 准备实际参数
Object[] params = {"芒果99","998","c009",13};
//4 执行
int r = queryRunner.update(sql, params);
System.out.println(r);
}
删除
@Test
public void delete() throws SQLException{
QueryRunner queryRunner = new QueryRunner(C3P0Utils.getDataSource());
String sql = "delete from product where pid = ?";
Object[] params = {99};
int r = queryRunner.update(sql, params);
System.out.println(r);
}
4.5QueryRunner实现查询操作
query(String sql, ResultSetHandler rsh, Object... params)
用来完成表数据的查询操作
ResultSetHandler 结果集
BeanHandler
:将结果集中第一条记录封装到一个指定的javaBean中。BeanListHandler
:将结果集中每一条记录封装到指定的javaBean中,将这些javaBean在封装到List集合中ScalarHandler
:它是用于单数据。例如select count(*) from 表操作。ColumnListHandler
:将结果集中指定的列的字段值,封装到一个List集合中
JavaBean
JavaBean就是一个类,在开发中常用语封装数据。具有如下特性
-
需要实现接口:java.io.Serializable ,通常实现接口这步骤省略了,不会影响程序。
-
提供私有字段:private 类型 字段名;
-
提供getter/setter方法:
-
提供无参构造
public class Product { private String pid; private String pname; private Double price; private String category_id; //省略 getter和setter方法 }
BeanHandler
/*
* 查询数据表结果集处理其中一种方式:
* BeanHandler处理方式
* 将数据表的结果集第一行数据,封装成JavaBean类的对象
* 构造方法:
* BeanHandler(Class<T> type)
* 传递一个Class类型对象,将结果封装到哪个类的对象呢
* ZhangWu类的Class对象
*/
@Test
public void demo01() throws SQLException{
// 通过id查询详情,将查询结果封装到JavaBean product
//1核心类
QueryRunner queryRunner = new QueryRunner(C3P0Utils.getDataSource());
//2 sql语句
String sql = "select * from product where pid = ?";
//3 实际参数
Object[] params = {6};
//4 查询并封装
Product product = queryRunner.query(sql, new BeanHandler<Product>(Product.class), params);
System.out.println(product);
}
BeanListHandler
/*
* 查询数据表结果集处理其中一种方式:
* BeanListHandler处理方式
* 将数据表的每一行数据,封装成JavaBean类对象
* 多行数据了,多个JavaBean对象,存储List集合
*/
@Test
public void demo02() throws SQLException{
//查询所有,将每一条记录封装到一个JavaBean,然后将JavaBean添加到List中,最后返回List,
BeanListHandler
QueryRunner queryRunner = new QueryRunner(C3P0Utils.getDataSource());
String sql = "select * from product";
Object[] params = {};
List<Product> list = queryRunner.query(sql, new BeanListHandler<Product>(Product.class),
params);
for(Product product : list){
System.out.println(product);
}
}
ScalarHander
/*
* 查询数据表结果集处理其中一种方式:
* ScalarHandler处理方式
* 处理单值查询结果,执行的select语句后,结果集只有1个
*/
@Test
public void demo03() throws SQLException{
// ScalarHandler : 用于处理聚合函数执行结果(一行一列)
// * 查询总记录数
QueryRunner queryRunner = new QueryRunner(C3P0Utils.getDataSource());
String sql = "select count(*) from product";
Long obj = queryRunner.query(sql, new ScalarHandler<Long>());
//System.out.println(obj.getClass());
System.out.println(obj);
}
ColumnListHandler
/*
* 查询数据表结果集处理其中一种方式:
* ColumnListHandler处理方式
* 将查询数据表结果集中的某一列数据,存储到List集合
* 哪个列不清楚,数据类型也不清楚, List<Object>
* ColumnListHandler构造方法
* 空参数: 获取就是数据表的第一列
* int参数: 传递列的顺序编号
* String参数: 传递列名
*
* 创建对象,可以加入泛型,但是加入的数据类型,要和查询的列类型一致
*/
@Test
public void demo04() throws SQLException{
// ColumnListHandler : 查询指定一列数据
QueryRunner queryRunner = new QueryRunner(C3P0Utils.getDataSource());
String sql = "select * from product";
List<String> list = queryRunner.query(sql, new ColumnListHandler<String>("pname"));
System.out.println(list);
}
5.6 DBUtils工具小结
DBUtils工具
- 作用:简化JDBC的操作
DBUtils常用类与方法
-
QueryRunner 用来执行SQL语句对象
update(Connection conn, String sql, Object… params)
插入表记录、更新表记录、删除表记录query(Connection conn, String sql, ResultSetHandler handler, Object… params)
查询表记录
-
ResultSetHandler 处理结果集的对象
- BeanHandler:将结果集中第一条记录封装到一个指定的javaBean中。
- BeanListHandler:将结果集中每一条记录封装到指定的javaBean中,将这些javaBean在封装到List集合中
- ScalarHandler:它是用于单数据。例如select count(*) from 表操作。
- ColumnListHandler:将结果集中指定的列的字段值,封装到一个List集合中
第6章 事务操作
事务概述
- 事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败.
- 事务作用:保证在一个事务中多次SQL操作要么全都成功,要么全都失败.
6.1 mysql事务操作
sql语句 | 描述 |
---|---|
start transaction | 开启事务 |
commit | 提交事务 |
rollback | 回滚事务 |
- 准备数据
# 创建一个表:账户表.
create database webdb;
# 使用数据库
use webdb;
# 创建账号表
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
# 初始化数据
insert into account values (null,'jack',10000);
insert into account values (null,'rose',10000);
insert into account values (null,'tom',10000);
-
操作
-
MYSQL中可以有两种方式进行事务的管理:
- 自动提交:MySql默认自动提交。及执行一条sql语句提交一次事务。
- 手动提交:先开启,再提交
-
方式1:手动提交
start transaction; update account set money=money-1000 where name='jack'; update account set money=money+1000 where name='rose'; #提交 commit; #或者回滚 rollback;
-
方式2:自动提交,通过修改mysql全局变量“autocommit”进行控制
show variables like '%commit%'; # 设置自动提交的参数为OFF: set autocommit = 0; -- 0:OFF 1:ON
-
6.2 jdbc事务操作
Connection 对象的方法名 | 描述 |
---|---|
conn.setAutoCommit(false/true) | 开启/关闭事务 |
conn.commit() | 提交事务 |
conn.rollback() | 回滚事务 |
注意:在释放Connection 对象前conn.setAutoCommit(true)设置事务自动提交,还原状态
代码演示
//事务模板代码
public void demo01() throws SQLException{
// 获得连接
Connection conn = null;
try {
//#1 开始事务
conn.setAutoCommit(false);
//.... 加钱 ,减钱
//#2 提交事务
conn.commit();
} catch (Exception e) {
//#3 回滚事务
conn.rollback();
} finally{
conn.setAutoCommit(true);
// 释放资源
conn.close();
}
}
6.3 DBUtils事务操作
必须保证连接为同一个连接,所以如果在业务层获得连接,再将连接传递到持久层,代码具有侵入性。
解决方式:使用ThreadLocal保存Connection对象
Connection对象的方法名 | 描述 |
---|---|
conn.setAutoCommit(false) | 开启事务,设置事务不自动提交 |
conn.setAutoCommit(false) | 创建核心类,不设置数据源(手动管理连接) |
query(conn , sql , handler, params ) 或 update(conn, sql , params) | 手动传递连接, 执行SQL语句CRUD |
DbUtils.commitAndCloseQuietly(conn) | 提交并关闭连接,不抛异常 |
DbUtils.rollbackAndCloseQuietly(conn) | 回滚并关闭连接,不抛异常 |
注意:在释放Connection 对象前conn.setAutoCommit(true)设置事务自动提交,还原状态
代码演示
//事务模板代码
public void demo02() throws SQLException{
// 获得连接
Connection conn = null;
try {
//#1 开始事务
conn.setAutoCommit(false);
//.... 加钱 ,减钱
//#2 提交事务并关闭连接
DbUtils.ommitAndCloseQuietly(conn);
} catch (Exception e) {
//#3 回滚事务并关闭连接
DbUtils.rollbackAndCloseQuietly(conn);
e.printStackTrace();
}
}
6.4 案例:JDBC事务分层(dao、service)传递Connection
分析
分层思想
-
开发中,常使用分层思想
- 不同的层次结构分配不同的解决过程,各个层次间组成严密的封闭系统
-
不同层级结构彼此平等
-
分层的目的是:
- 解耦
- 可维护性
- 可扩展性
- 可重用性
-
不同层次,使用不同的包表示
- com.itheima 公司域名倒写
- com.itheima.dao dao层
- com.itheima.service service层
- com.itheima.domain javabean
- com.itheima.utils 工具
代码实现
- 工具类C3P0Utils
public class C3P0Utils {
//创建一个C3P0的连接池对象(使用c3p0-config.xml中default-config标签中对应的参数)
public static DataSource ds = new ComboPooledDataSource();
//从池中获得一个连接
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
}
- c3p0-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<!-- 使用默认的配置读取连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/webdb</property>
<property name="user">root</property>
<property name="password">root</property>
<!-- 连接池参数 -->
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">2000</property>
<property name="maxIdleTime">1000</property>
</default-config>
</c3p0-config>
- 入口程序(view)
package com.itheima.view;
import com.itheima.serivce.AccountService;
import java.util.Scanner;
/*
创建转账案例的web层
使用Scanner获取用户输入的数据(付款人姓名,收款人姓名,转账金额)
创建AccountSerivce对象
调用转账方法,接收转账结果
对结果进行判断,给用户展示结果
*/
public class Accountview {
public static void main(String[] args) {
//使用Scanner获取用户输入的数据(付款人姓名,收款人姓名,转账金额)
Scanner sc = new Scanner(System.in);
System.out.print("请输入付款人姓名:");
String fromName = sc.next();
System.out.print("请输入收款人姓名:");
String toName = sc.next();
System.out.print("请输入转账金额:");
double money = sc.nextDouble();
//创建AccountSerivce对象
AccountService serivce = new AccountService();
//调用转账方法,接收转账结果
boolean b = serivce.transferAccount(fromName, toName, money);
//对结果进行判断,给用户展示结果
if (b){
System.out.println("转账成功!");
}else{
System.out.println("转账失败!");
}
}
}
- service层
package com.itheima.serivce;
import com.itheima.dao.AccountDao;
import com.itheima.utils.C3P0Utils;
import org.apache.commons.dbutils.DbUtils;
import java.sql.Connection;
import java.sql.SQLException;
/*
事务管理方式:向下传递Connection。有侵入性。
转账案例的Service层:接收web的传递的数据,调用dao层的方法,接收结果;把结果返回给view层
定义一个转账方法:
参数接收web传递的数据(付款人姓名,收款人姓名,转账金额)
使用C3P0连接池获取Connection
开启事务
创建AccountDao对象
调用减钱和加钱方法,接收结果
对结果进行判断
都执行成功,提交事务
有异常,回滚事务
把结果返回给view(web)层
还原状态
释放资源
*/
public class AccountService {
//定义一个转账方法
public boolean transferAccount(String fromName,String toName,double momey){
//使用C3P0连接池获取Connection
Connection conn = C3P0Utils.getConnection();
//定义返回的结果
boolean flag = false;
try {
//DBUtils进行事务处理的原理,是在Service层获得连接,以保证事务处理过程中的Connection对象为同一个Connection。
//因为必须保证连接为同一个连接,所以在业务层获得连接,再将连接传递到持久层,代码具有侵入性。
//开启事务,设置事务不自动提交
conn.setAutoCommit(false);
//创建AccountDao对象,调用dao层
AccountDao dao = new AccountDao();
//调用持久层
int row1 = dao.fromAccount(conn, fromName, momey);
System.out.println(0/0);
int row2 = dao.toAccount(conn, toName, momey);
//对结果进行判断
if(row1>0 && row2>0){
flag = true;
//都执行成功,提交事务
conn.commit();
}
} catch (Exception e) {
e.printStackTrace();
//有异常,回滚事务
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
//还原状态
conn.setAutoCommit(true);
//释放资源
DbUtils.closeQuietly(conn);
}
//把结果返回给web层
return flag;
}
}
- dao层
- 注意:
一张表–>一个dao
- 注意:
package com.itheima.dao;
import org.apache.commons.dbutils.QueryRunner;
import java.sql.Connection;
import java.sql.SQLException;
/*
创建转账案例的Dao层:用于对account表进行增删改查
注意:
一张表-->一个dao
定义两个方法:
一个减钱,一个加钱
*/
public class AccountDao {
/*
定义减钱方法
参数:
Connection conn:两个方法使用同一个Connection,从而保证使用同一个事务
String fromName:付款人姓名
double money:转账金额
返回值:
int:影响数据库的有效行数
*/
public int fromAccount(Connection conn,String fromName,double money) throws SQLException {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//调用update方法执行sql语句,接收结果
int row = qr.update(conn,"update account set money=money-? where name=?;",money,fromName);
//返回结果
return row;
}
/*
定义加钱方法
参数:
Connection conn:两个方法使用同一个Connection,从而保证使用同一个事务
String toName:收款人姓名
double money:转账金额
返回值:
int:影响数据库的有效行数
*/
public int toAccount(Connection conn,String toName,double money) throws SQLException {
//创建QueryRunner对象
QueryRunner qr = new QueryRunner();
//调用update方法执行sql语句,接收结果
int row = qr.update(conn,"update account set money=money+? where name= ?;",money,toName);
//返回结果
return row;
}
}
第7章 ThreadLocal
7.1 分析
在“事务传递Connection参数案例”中,我们必须传递Connection对象,才可以完成整个事务操作。如果不传递参
数,是否可以完成?在JDK中给我们提供了一个工具类:ThreadLocal,此类可以在一个线程中共享数据。
java.lang.ThreadLocal
该类提供了线程局部 (thread-local) 变量,用于在当前线程中共享数据。
7.2 相关知识:ThreadLocal
java.lang.ThreadLocal 该类提供了线程局部(thread-local) 变量,用于在当前线程中共享数据。ThreadLocal工具类底层就是相当于一个Map,key存放的当前线程,value存放需要共享的数据。
举例
public class ThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> mainThread = new ThreadLocal<>();
mainThread.set("Hello Word");
System.out.println(mainThread.get());//Hello Word
new Thread(()->{
System.out.println(mainThread.get());//null
}).start();
}
}
结论:向ThreadLocal对象中添加的数据只能在当前线程下使用。
7.3 使用ThreadLocal优化程序
分析
代码实现
- 工具类
public class C3P0Utils {
//创建一个C3P0的连接池对象(使用c3p0-config.xml中default-config标签中对应的参数)
public static DataSource ds = new ComboPooledDataSource();
//给当前线程绑定 连接
private static ThreadLocal<Connection> local = new ThreadLocal<Connection>();
/**
* 获得一个连接
*/
public static Connection getConnection(){
try {
//#1从当前线程中, 获得已经绑定的连接
Connection conn = local.get();
if(conn == null){
//#2 第一次获得,绑定内容 – 从连接池获得
conn = ds.getConnection();
//#3 将连接存 ThreadLocal
local.set(conn);
}
return conn; //获得连接
} catch (Exception e) {
//将编译时异常 转换 运行时 , 以后开发中运行时异常使用比较多的。
throw new RuntimeException(e);
/*
类与类之间 进行数据交换时,可以使用return返回值。也可以使用自定义异常返回值,调用者try{}catch(e){ e.getMessage() 获得需要的数据}
此处可以编写自定义异常。
*/
//throw new MyConnectionException(e);
}
}
}
- service层
public class AccountService {
/**
* 事务管理方式:向下传递Connection。有侵入性。使用DBUtils
* 业务层事务管理转账的方法
* @param from
* @param to
* @param money
*/
public void transfer(String from, String to, double money) {
//调用dao层
AccountDao accountDao = new AccountDao();
//DBUtils进行事务处理的原理,是在Service层获得连接,以保证事务处理过程中的Connection对象为同一个Connection。
//因为必须保证连接为同一个连接,所以在业务层获得连接,再将连接传递到持久层,代码具有侵入性。
//DBUtils使用的方法
Connection conn = null;
try {
//获得连接
conn = C3P0Utils.getConnection();
//设置事务不自动提交
conn.setAutoCommit(false);
//调用持久层
accountDao.outMoney(from,money);
//如果有异常
//int a = 1 / 0 ;
accountDao.inMoney(to,money);
//提交事务,并安静的关闭连接
DbUtils.commitAndCloseQuietly(conn);
} catch (SQLException e) {
//有异常出现时,回滚事务,并安静的关闭连接
DbUtils.rollbackAndCloseQuietly(conn);
e.printStackTrace();
}
}
}
- dao层
public class AccountDao {
/**
* 付款方法
* @param from 付款人
* @param money 金额
*/
public void outMoney(String from, double money) {
QueryRunner qr = new QueryRunner();
try {
Connection conn = C3P0Utils.getConnection();
String sql = "update account set money = money - ? where name = ?";
qr.update(conn, sql, money,from);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 收款方法
* @param to 收款人
* @param money 金额
*/
public void inMoney(String to, double money) {
QueryRunner qr = new QueryRunner();
try {
Connection conn = C3P0Utils.getConnection();
String sql = "update account set money = money + ? where name = ?";
qr.update(conn, sql, money,to);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
第8章 事务总结
8.1 事务特性:ACID
- 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency)事务前后数据的完整性必须保持一致。
- 隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
- 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
8.2 并发访问问题
如果不考虑隔离性,事务存在3中并发访问问题。
- 脏读:一个事务读到了另一个事务未提交的数据.
- 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
- 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。
8.3 隔离级别:解决问题
- 数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
- read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。
a)存在:3个问题(脏读、不可重复读、虚读)。
b)解决:0个问题
-
read committed 读已提交,一个事务读到另一个事务已经提交的数据。
a)存在:2个问题(不可重复读、虚读)。b)解决:1个问题(脏读)
-
repeatable read:可重复读,在一个事务中读到的数据始终保持一致,无论另一个事务是否提交。
a)存在:1个问题(虚读)。
b)解决:2个问题(脏读、不可重复读)
-
serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。
a)存在:0个问题。
b)解决:3个问题(脏读、不可重复读、虚读)
- 安全和性能对比
- 安全性: serializable > repeatable read > read committed > read uncommitted
- 性能 : serializable < repeatable read < read committed < read uncommitted
- 常见数据库的默认隔离级别:
- MySql: repeatable read
- Oracle: read committed
8.4 演示
- 模拟脏读
- 模拟不可重复读
- 模拟虚读幻读
-
查询数据库的隔离级别
show variables like '%isolation%'; 或 select @@tx_isolation;
-
设置数据库的隔离级别
set session transactionisolation level 级别字符串
- 级别字符串:
readuncommitted
、read committed
、repeatable read
、serializable
- 例如:
set session transaction isolation level read uncommitted;
-
配置文件全局修改
[mysqld] # 可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE. transaction-isolation = REPEATABLE-READ
-
java中修改
Connection接口的方法
conn.setTransactionIsolation(int level); static int TRANSACTION_NONE 指示不支持事务的常量。 static int TRANSACTION_READ_UNCOMMITTED 一个常量表示可能会发生脏读,不可重复读和幻读。 static int TRANSACTION_READ_COMMITTED 一个常数表示防止脏读;可能会发生不可重复的读取和幻像读取。 static int TRANSACTION_REPEATABLE_READ 一个常量表示防止了脏读和不可重复读;可以发生幻影读取。 static int TRANSACTION_SERIALIZABLE 一个常数表示防止脏读不可重复读和幻影读。
-
读未提交:readuncommitted
- A窗口设置隔离级别
- AB同时开始事务
- A 查询
- B 更新,但不提交
- A 再查询?-- 查询到了未提交的数据
- B 回滚
- A 再查询?-- 查询到事务开始前数据
- A窗口设置隔离级别
-
读已提交:read committed
- A窗口设置隔离级别
- AB同时开启事务
- A查询
- B更新、但不提交
- A再查询?–数据不变,解决问题【脏读】
- B提交
- A再查询?–数据改变,存在问题【不可重复读】
- A窗口设置隔离级别
-
可重复读:repeatable read
- A窗口设置隔离级别
- AB 同时开启事务
- A查询
- B更新, 但不提交
- A再查询?–数据不变,解决问题【脏读】
- B提交
- A再查询?–数据不变,解决问题【不可重复读】
- A提交或回滚
- A再查询?–数据改变,另一个事务
- A窗口设置隔离级别
-
串行化:serializable
- A窗口设置隔离级别
- AB同时开启事务
- A查询
- B更新?–等待(如果A没有进一步操作,B将等待超时)
- A回滚
- B 窗口?–等待结束,可以进行操作
- A窗口设置隔离级别