JDBC
一、概述
1.1 什么是JDBC?
jdbc 是java 访问 数据库的一套api, sun公司统一java语言去连接操作数据库定义的一套标准。
1.2 JDBC体系
二、JDBC 的重要组件
JDBC 是一套编程接口(提供了一些接口 类 ....),这写类库就是jdbc组成的元素,其中比较重要的,且面向开发者,实现编程的类或接口,我们成为组件。
interface : 狭义上的接口,类java语法层面上的术语。
编程接口: 广义上的接口,指的规范和标准(类库)。
重要组件:
这些组件都在 java.sql 包下。
2.1 Connection接口
Connection: 接口,该类型的实例,表示一个数据库连接。连接是我们jdbc的基础,对数据库表或数据的操作都是建立在连接之上的。
2.2 DriverManager 驱动管理类
DriverManager: 驱动管理类,用于获得(创建)数据库连接对象的。
- Connection getConnection( url, user,pwd )
url: 统一资定位符,确定待连接数据的在网络中的位置 协议+IP+端口+数据库名?其他参数
jdbc:mysql://localhost:3306/db_name?useUnicode=true&characterEncoding=UTF-8&useSSL=false
2.3 Statement 语句接口
Statement : 语句接口, 该类型的实例,表示一个SQL语句对象。
- int executeUpdate( String sql )
DML增删改 通用的api 参数表示执行的sql语句 返回值表示受影响的行数。
- ResultSet executeQuery(String sql)
DQL 执行查询语句的方法, 参数表示执行的SQL语句 返回值表示查到的数据(查询结果),这个结果是一个集合(这个集合和我们学的集合框架没有半毛钱关系)。
- boolean execute( String sql)
DDL 语句执行的方法, 参数表示执行的sql 返回值表示成功和失败
2.4 ResultSet 结果集
ResultSet: 结果集接口,该类型的实例表示查询结果对象,这个对象保存着从数据库中查询到的数据。
- boolean next( ) 控制数据光标移动的api, 如果集合中有下一行记录,那么返回true ,同时光标下移。没有数据则发回false。
- XXX getXXX( String columnName ): 例如 String getString(String columnName ) 从结果集中获得指定列的值。那么这个XXX 究竟用什么呢?需要根据该列的数据类型来确定。比如 ename 在数据库中是 varchar 那么在java中就是String。
2.5 PreparedStatement 语句
PreparedStatement: 参数化SQL语句对象,它是Statement 的子接口,该实例表示一个参数化SQL,SQL中的数据使用 ?站位。
-
创建PreparedStatement 对象的方式:
PreparedStatement prepareStatement( String sql )
通过连接对象调用该方法, 参数表示需要参数化的SQL语句
-
占位符赋值
setXXX(int index,XXX value)
XXX 取决于该参数的数据类型, index表示改参数的下标 , value表示该参数的值
三、JDBC 基本使用
3.1 连接数据库
准备
1. 创建工程
2. 引入启动包
示例:
public class TestConnection {
public static void main(String[] args) throws Exception {
// 1 . 注册驱动(把mysql.jar包里的一个类加载内存)
Class.forName("com.mysql.jdbc.Driver"); // 错误解决
// 2 . 建立连接
// url: 定位你要连接的数据库 格式: 协议+IP+端口+数据库名
// 示例:jdbc:mysql://localhost:3306/empdb
// user: 用户名
// password: 密码
Connection conn =
DriverManager.getConnection("jdbc:mysql://localhost:3306/empdb?useSSL=false","root","123456");
System.out.println(conn);
}
}
提示: url 账号 密码 不能写错,任何错误的连接信息,都会连接失败。
3.2 执行DML语句
package com.qfedu.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class TestDML {
public static void main(String[] args) throws Exception {
//1 注册驱动
Class.forName("com.mysql.jdbc.Driver"); // alt+ enter
//2 创建连接
Connection conn = DriverManager.
getConnection("jdbc:mysql://localhost:3306/empdb?useSSL=false",
"root","123456");
//3 执行SQL
Statement smt = conn.createStatement();
// 插入
/* int row = smt.executeUpdate("insert into emp(empno,ename,job,sal)
values(999,'张伟','软件开发工程师',14000) ");*/
// 删除
/*int row = smt.executeUpdate("delete from emp where empno=999");*/
// 修改
int row = smt.executeUpdate(" update emp set sal=3000 where ename='王思聪' ");
System.out.println(row);
//4 关闭资源
smt.close();
conn.close();
}
}
int executeUpdate(String sql) : 是增删改通用的方法。sql就是要执行的语句 int返回值 就是影响的行数。
3.3 执行DQL 语句
package com.qfedu.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
// 测试数据查询
public class TestQuery {
public static void main(String[] args) throws Exception {
//注册驱动
Class.forName("com.mysql.jdbc.Driver");
//创建连接
Connection conn= DriverManager.
getConnection("jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false",
"root","123456");
//创建语句对象
Statement smt = conn.createStatement();
ResultSet rs = smt.executeQuery("select empno, ename,job,sal from emp where deptno=30 or sal >1000");//执行语句得到结果
//处理结果
while( rs.next() ){
//一次循环读取一行记录
int no= rs.getInt("empno");
String name = rs.getString("ename");
String job = rs.getString("job");
double sal = rs.getDouble("sal");
System.out.println(no+"\t"+name+"\t"+job+"\t"+sal);
}
//关闭
rs.close();
smt.close();
conn.close();
}
}
ResultSet executeQuery(String sql) DQL 执行查询语句的方法, 参数表示执行的SQL语句 返回值表示查到的数据(查询结果)
3.4 执行DDL语句
package com.qfedu.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class TestDDL {
public static void main(String[] args) throws Exception {
//注册驱动
Class.forName("com.mysql.jdbc.Driver");
//创建连接
Connection conn= DriverManager.
getConnection("jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false",
"root","123456");
//创建语句对象
Statement smt = conn.createStatement();
// 执行DDL(创建一张表)
boolean xx = smt.execute("create table users(\n" +
" id int primary key auto_increment,\n" +
" username varchar(30) not null,\n" +
" password varchar(30) not null \n" +
")");
System.out.println(xx);
//关闭资源
smt.close();
conn.close();
}
}
常见错误锦集
1、Caused by: java.net.ConnectException: Connection refused: connect : 服务没开
2、Exception in thread "main" java.lang.ClassNotFoundException: com.mysql.jdbc.Driver :找不到驱动
3Exception in thread "main" java.sql.SQLException: Access denied for user 'root '@'localhost' (using password: YES) 账号或密码错误
Exception in thread "main" com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown database 'empbd' 数据库名字不对
4 Exception in thread "main" com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'empdb.emsp' doesn't exist 表存在
5.Exception in thread "main" com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'form emp' at line 1 语法错误找near前后
6 Exception in thread "main" com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'empon' in 'field list' 列名错误
7 Exception in thread "main" java.sql.SQLException: Column 'empno' not found. 没查询却在取
8 Exception in thread "main" java.sql.SQLException: Operation not allowed after ResultSet closed 再关闭ResultSet后继续取数据。
3.5 登录案例
准备数据
create table users(
id int primary key auto_increment,
username varchar(30) not null,
password varchar(30) not null
);
理解登录的原理
登录功能本质是查询,把用户的账号密码信息做查询条件,看数据库中是否存在满足条件的记录。
代码示例:
package com.qfedu.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Scanner;
//登录案例
public class LoginDemo {
public static void main(String[] args) throws Exception {
// 1 接受用户输入
Scanner scanner = new Scanner(System.in);
//用户名输入 密码输入
System.out.println("输入账号密码");
String uname = scanner.next();
String pwd = scanner.next();
// 2 把用户的输入作为筛选条件查询数据库中是否存在记录。
Class.forName("com.mysql.jdbc.Driver");
//创建连接
String url = "jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection =
DriverManager.getConnection(url,"root","123456");
//执行SQL
Statement smt = connection.createStatement();
String sql = "select * from users where username='"+uname+"' and password='"+pwd+"' ";
ResultSet rs = smt.executeQuery(sql);
//处理结果
if (rs.next()){
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
//关闭资源
scanner.close();
rs.close();
smt.close();
connection.close();
}
}
四、参数化SQL
参数化SQL 也叫预编译SQL,把SQL语句与SQL中数据进行分离的的一种方式,这种方式跟安全,更高效。
- SQL 注入
所谓SQL注入,就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。
- 参数化SQL 的DML语句
package com.qfedu.prepare;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
//参数化SQL
public class TestPreparedStatementDML {
public static void main(String[] args) throws Exception {
// insert();
// delete();
// update();
}
//测试插入
public static void insert() throws Exception{
Class.forName("com.mysql.jdbc.Driver");
//创建连接
String url = "jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection =
DriverManager.getConnection(url,"root","123456");
//参数化SQL
PreparedStatement psmt = connection.prepareStatement("insert into users(username,password) values(?,?) ");
//替换占位符,小标从 1 开始数
psmt.setString(1,"java");
psmt.setString(2,"8899");
//执行SQL
int row = psmt.executeUpdate();
System.out.println(row);
//释放资源
psmt.close();
connection.close();
}
//测试删除
public static void delete() throws Exception{
Class.forName("com.mysql.jdbc.Driver");
//创建连接
String url = "jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection =
DriverManager.getConnection(url,"root","123456");
PreparedStatement psmt = connection.prepareStatement("delete from users where id= ? ");
psmt.setInt(1,2);
int row =psmt.executeUpdate();
System.out.println(row);
psmt.close();
connection.close();
}
//测试修改
public static void update() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
//创建连接
String url = "jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection =
DriverManager.getConnection(url,"root","123456");
//参数化SQL
PreparedStatement psmt = connection.prepareStatement("update users set password= ? where username= ? ");
//赋值
psmt.setString(1,"888888");
psmt.setString(2,"java");
//执行
int row = psmt.executeUpdate();
System.out.println(row);
//关闭资源
psmt.close();
connection.close();
}
}
- 参数化查询的DQL
//测试查询
public static void query() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
//创建连接
String url = "jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection =
DriverManager.getConnection(url,"root","123456");
//参数化SQL
PreparedStatement psmt = conn.prepareStatement("select ename,job,sal from emp where sal>?");
psmt.setDouble(1,2000);
ResultSet rs = psmt.executeQuery();
while (rs.next()){
System.out.println( rs.getString("ename") + rs.getDouble("sal") );
}
//关闭资源
rs.close();
psmt.close();
connection.close();
}
}
五、单元测试
单元测试,一般指的是对程序中某个功能进行的可用性的测试,它属于白盒测试。在java中的单元测试框架就是Junit。
1. 引入jar包
junit-4.12.jar
hamcrest-core-1.3.jar
2. 使用注解
@Test 单元测试注解,带该注解的方法可用独立运行,当一个main使用
@Before 在当前单元测试前执行。 做初始化操作
@After 在当前单元测试后执行。 做扫尾操作
package com.qfedu.junit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
//学习Junit的用法
public class HelloJunit {
//1 导包 ok
// 2 注解 @Test @Before @After
//t1 就是一个独立执行的入口
@Test
public void t1(){
System.out.println("hello");
}
//t2 就是一个独立执行的入口
@Test
public void t2(){
System.out.println("world");
}
@Test
public void t3(){
System.out.println("java");
}
@Before
public void aaa(){
System.out.println("begin....");
}
@After
public void bbb(){
System.out.println("end.....");
}
}
六、工具类封装
版本(1)
直接使用JDBC API编程出现大量冗余代码,可用适当封装暴露出常用的方法,来实现数据库操作。
package com.qfedu.util;
import java.sql.*;
//数据操作工具类
public class DButil {
//定义数据库连接信息
private static final String url="jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
private static final String username="root";
private static final String password="123456";
private static final String driver="com.mysql.jdbc.Driver";
//静态代码块(执行一次)
static{
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
System.out.println("加载驱动失败");
}
}
// 获得数据库连接对象
public static Connection getConnection(){
Connection connection = null;
try {
connection= DriverManager.getConnection(url,username,password);
} catch (SQLException e) {
e.printStackTrace();
System.out.println("创建连接对象失败");
}
return connection;
}
// 释放资源(哪些资源需要关闭,需要管的传入,没有的就传null)
public static void close(Connection conn, Statement smt, ResultSet rs){
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(smt!=null){
try {
smt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
版本(2)
1 导入 连接驱动
mysql-connector-java-5.1.48.jar
2 准备配置文件 db.properties 置于 src下。
jdbc.username=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.driver=com.mysql.jdbc.Driver
properties 文件的后缀一定不要写错,且不要使用中文
3 编码
package com.qfedu.jdbc;
import java.sql.*;
import java.util.Properties;
//工具类
public class Dbutil {
// 连接信息
private static String username=null;
private static String url=null;
private static String password=null;
private static String driver=null;
//使用连接信息(读取配置信息,什么时候去读呢?)
static {
try {
//读取配置信息
Properties pro = new Properties();// 准备map集合
//把 db.properties 中的数据导入到 pro 集合中
// 获得当前线程 Thread.currentThread()
// 利用线程或获得类加载器(加载类) getContextClassLoader()
// 类加载器就可以中类路径下加载资源 getResourceAsStream("db.properties")
pro.load( Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties") );
//赋值
driver= pro.getProperty("jdbc.driver");
username=pro.getProperty("jdbc.username");
password=pro.getProperty("jdbc.password");
url=pro.getProperty("jdbc.url");
//加载驱动
Class.forName(driver);
}catch( Exception e){
e.printStackTrace();
}
}
//获得连接的方法
public static Connection getConnection(){
//准备一个空连接对象
Connection conn =null;
try {
//为连接对象赋值
conn = DriverManager.getConnection(url,username,password);
} catch (SQLException e) {
e.printStackTrace();
}
//返回获得的连接对象,如果发生异常则返回默认值null
return conn;
}
//关闭资源的方法
public static void close(Connection conn){
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void close(Connection conn, Statement smt){
if(smt!=null){
try {
smt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void close(Connection conn, Statement smt, ResultSet rs){
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(smt!=null){
try {
smt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
七、ORM 思想
7.1概念
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。
7.2映射方式
类 对应 表
属性 对应 列
对象 对应 行
集合 对应 多行
与表对应了类叫 实体类
7.3开发示例
完整的案例参考代码
7.3.1 准备数据
-- 后宫佳丽3000表
create table tb_girl(
id int primary key,
gname varchar(30),
age int,
level varchar(30)
);
7.3.2 ORM 实体类
package com.qfedu.entity;
//实体类
public class Girl {
private int id;
private String gname;
private int age;
private String level;
public Girl() {
}
public Gril(int id, String gname, int age, String level) {
this.id = id;
this.gname = gname;
this.age = age;
this.level = level;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getGname() {
return gname;
}
public void setGname(String gname) {
this.gname = gname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
@Override
public String toString() {
return "Gril{" +
"id=" + id +
", gname='" + gname + '\'' +
", age=" + age +
", level='" + level + '\'' +
'}';
}
}
7.3.3 编写Dao类
Dao(Data Access Object)为数据访问类,在开发中,应该吧数据库操作交给专门访问数据库的对象来完成。而不是哪里需要访问数据库就写jdbc代码,这样利于各个功能模块的维护。
package com.qfedu.dao;
import com.qfedu.entity.Girl;
import com.qfedu.jdbc.Dbutil;
import java.sql.Connection;
import java.sql.PreparedStatement;
// 就是访问Gril表的 Dao
public class GirlDao {
//添加一个gril
public int add( Girl gril ) throws Exception{
// 思想 : 添加一个girl,以面向对象来讲,就是添加一个girl对象
// 添加一个girl对象 使用orm思想,就是把这个对象的全部属性,
// 作为一行记录插入到数据库中。
// girl的属性有多个,那么这里传参做好的方式就是,传入一个girl对象
// 通过这个girl对象就可以获得它全部的属性。
// 在这里完成对数据的插入
// 1 获得连接对象
Connection conn = Dbutil.getConnection();
// 2 创建命令对象
String sql = "insert into tb_girl(id,gname,age,level) values(?,?,?,?)";
PreparedStatement psmt = conn.prepareStatement(sql);
// 3 替换?
psmt.setInt( 1, gril.getId() );
psmt.setString(2,gril.getGname());
psmt.setInt(3,gril.getAge());
psmt.setString(4,gril.getLevel());
// 4 执行
int row =psmt.executeUpdate();
// 5 释放资源
Dbutil.close(conn,psmt);
// 6 放回行数
return row;
}
// 扩展其他工能(....)
}
7.3.4 使用Dao完成功能
@Test
public void t2() throws Exception{
//利用Dao 调用方法完成数据操作,对调用者来讲无须关注细节
GirlDao dao = new GirlDao();
//参数
Girl girl = new Girl(1,"杨小红",18,"贵妃");
//调用DAO功能
int row =dao.add(girl);
System.out.println(row);
}
利用Dao 调用方法完成数据操作,对调用者来讲无须关注细节。
数据库类型和JAVA类型对应
varchar String
char String
int int
bigint long
float float
double double
decimal double
date sql.Date
setXXX getXXX 类型根据上面的对应关系来
日期处理:
日期转换 util.Date=> sql.Date
public static java.sql.Date toSQLDate( java.util.Date date ){
java.sql.Date sqlDate = new java.sql.Date( date.getTime() );
return sqlDate;
}
日期转换 String => util.Date
public static java.util.Date toUtilDate( String str ) {
//格式化工具
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
//把日期字符串转 Date
return simpleDateFormat.parse(str);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
八、Service 模式
在我们ORM思想中我们引入了Dao(数据范文对象),就把数据操作交给了Dao对象完成。再次基础上我们再次设计一下,Dao 主要职责是数据读写,一个软件它本身是各种功能的的集合,这些功能我是不是可以理解为是这个软件对外提供的的服务(Service )。我们需要搞清楚 Service 与Dao 有什么关系呢? Service 业务功能的实现 要利用Dao 数据操作完成。在软件开发中,一般都会把 业务和数据访问分离开来。
抽取业务功能类,把业务需要使用到的数据读写 交给 Dao 来完成,因为业务类关注的是业务逻辑,不关注数据操作,这是软件设计规范中的,职责分离。
package com.qfedu.service;
import com.qfedu.dao.GirlDao;
import com.qfedu.entity.Girl;
// 用于抽取 Girl模块的业务功能
public class GirlService {
// 赐封 业务 => (修改)
public boolean cifeng( int id, String new_level ) throws Exception {
//这业务需要牵扯到数据操作。引入Dao
GirlDao girlDao = new GirlDao();
// 1. 根据id 先查询 要修改的人
Girl girl= girlDao.findGirlById(id);
// 2. 设置新的level。
girl.setLevel( new_level );
// 3. 执行更新
int row = girlDao.update(girl);
// 结果转换
if(row>0){
return true;
}else{
return false;
}
}
}
九、事务控制
9.1问题引入
业务:下订单
- 插入订单数据。 OrderService->OrderDao
- 修改库存。 StockService->StockDao
这业务使用到两Dao操作,需要确保都成功
Service
{
create Order(){
OrderService orderService= new OrderService ;
orderService.insert();
StockService stockDao = new StockService ();
StockService .update();
}
}
9.2 事务的四大特点(ACID)
actomicity(原子性)
表示一个事务内的所有操作是一个整体,要么全部成功,要么全部失败
consistency(一致性)
表示一个事务内有一个操作失败时,所有的更改过的数据都必须回滚到修改前状态
isolation(隔离性)
事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另事务修改它之后的 状态,事务不会查看中间状态的数据。
durability(持久性)
持久性事务完成之后,它对于系统的影响是永久的。
9.3 事务隔离级别:
Read Uncommitted(读取未提交内容) 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也 不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容) 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的,Oracle)。它满足了隔离的简单定义:一个事务只能看见 已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其 他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read可重读 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理 论上,这会导致另一个棘手的问题:幻读(Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行 时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和 Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable 可串行化 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。 简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。 这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。
9.4 JDBC 实现事务管理
9.3.1 事务基本api
1. jdbc中提供了事务管理的API(具体由java.sql.Connection 接口类型提供方法)
2. 开启事务: 关闭SQL执行后自动提交 setAutoCommit(false)
3. 提交事务: commit()
4. rollback()
这些方法由连接对象调用,连接对象是事务实现的关键,如果你想让多个SQL语句在同一个事务中,那么执行这些Sql的连接对象必须是同一个。不然就没办法管理事务。
9.3.2 ThreadLocal
ThreadLocal 称为线程本地变量,指的是可以利用ThreadLocal 为线程保存一份私有变量(就是一个变副本)。
ThreadLocal 内部维护了一个Map ,这个map把当前线程引用作为key, 数据作为value。这样每个线程都有一个独立的变量副本。线程间就没有影响。
9.3.3 改造工具
package com.qfedu.jdbc.util;
import java.sql.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Properties;
//工具类
public class Dbutil {
// 连接信息
private static String username=null;
private static String url=null;
private static String password=null;
private static String driver=null;
//声明一个ThreadLocal 用于保存连接对象(同一个线程共享)
private static ThreadLocal<Connection> localConn = new ThreadLocal<>();
//使用连接信息(读取配置信息,什么时候去读呢?)
static {
try {
//读取配置信息
Properties pro = new Properties();// 准备map集合
//把 db.properties 中的数据导入到 pro 集合中
// 获得当前线程 Thread.currentThread()
// 利用线程或获得类加载器(加载类) getContextClassLoader()
// 类加载器就可以中类路径下加载资源 getResourceAsStream("db.properties")
pro.load( Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties") );
//赋值
driver= pro.getProperty("jdbc.driver");
username=pro.getProperty("jdbc.username");
password=pro.getProperty("jdbc.password");
url=pro.getProperty("jdbc.url");
System.out.println(pro);
System.out.println(pro.toString());
//加载驱动
Class.forName(driver);
}catch( Exception e){
e.printStackTrace();
}
}
//获得连接的方法
public static Connection getConnection(){
//从当前线程(执行这这段代码的线程 )中获得连接对象
if( localConn.get() ==null ){ // 当前线程中还没有连接对象
try {
//创建连接对象
Connection conn = DriverManager.getConnection(url,username,password);
//添加到当前线程中去
localConn.set(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
//返回现有的
return localConn.get();
}
//关闭资源的方法
public static void close(Connection conn){
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void close(Connection conn, Statement smt){
if(smt!=null){
try {
smt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void close(Connection conn, Statement smt, ResultSet rs){
if(rs!=null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(smt!=null){
try {
smt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//日期转换 util.Date=> sql.Date
public static Date toSQLDate( java.util.Date date ){
Date sqlDate = new Date( date.getTime() );
return sqlDate;
}
//日期转换 String => util.Date
public static java.util.Date toUtilDate( String str ) {
//格式化工具
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
//把日期字符串转 Date
return simpleDateFormat.parse(str);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
//开始事务的方法
public static void begin(){
try {
getConnection().setAutoCommit(false); //设置手动事务
} catch (SQLException e) {
e.printStackTrace();
}
}
//提交事务
public static void commit(){
try {
getConnection().commit(); //提交
} catch (SQLException e) {
e.printStackTrace();
}finally {
Dbutil.close(getConnection());
localConn.remove(); // 移除连接对象
}
}
//回滚事务
public static void rollback(){
try {
getConnection().rollback(); //回滚
} catch (SQLException e) {
e.printStackTrace();
}finally {
Dbutil.close(getConnection());
localConn.remove(); //移除,和线程解除绑定
}
}
}
核心控制代码
package com.qfedu.jdbc.service;
import com.qfedu.jdbc.dao.OrderDao;
import com.qfedu.jdbc.entity.Order;
import com.qfedu.jdbc.entity.Stock;
import com.qfedu.jdbc.util.Dbutil;
//用户业务
public class UserService {
//下订单
public Order createOrder( Order order ) {
// 在这里用户Service 调用其他现成Service 协助完成功能。
OrderService orderService = new OrderService();
StockService stockService = new StockService();
Dbutil.begin();// 开启事务
try {
//1.插入订单数据
orderService.add(order);
//模拟一个其他代码错误
int c=1/0;
//2.修改库存( 从订单信息中拿 的商品sid 和要扣减的数量)
Stock newStock = new Stock(order.getSid(), order.getNum());
stockService.updateStock(newStock);
// 提交事务
Dbutil.commit();
}catch (Exception e){
System.out.println("出现错误,要回滚数据");
//回滚
Dbutil.rollback();
}
//返回刚刚创建的订单
return order;
}
}
public class OrderDao {
// 插入数据
public int insert(Order order) throws Exception {
//1 连接
Connection conn = Dbutil.getConnection();
System.out.println("order:conn"+ conn);
//2 语句
PreparedStatement psmt =
conn.prepareStatement("insert into sys_order(oid,num,create_date,sid) values(?,?,now(),?)");
//3 替换
psmt.setLong(1,order.getOid());
psmt.setInt(2,order.getNum());
psmt.setInt(3,order.getSid());
// 执行
int row =psmt.executeUpdate();
// 关闭资源
Dbutil.close(null,psmt);
return row;
}
}
public class StockDao {
//修改方法
public int update(Stock stock) throws Exception{
//1 连接
Connection conn = Dbutil.getConnection();
System.out.println("stock:conn"+ conn);
//2 语句
PreparedStatement psmt = conn.prepareStatement("update sys_stock set stock=stock-? where sid=? ");
//3 替换
psmt.setInt(1,stock.getStock());
psmt.setInt(2,stock.getSid());
// 执行
int row =psmt.executeUpdate();
// 关闭资源
Dbutil.close(null,psmt);
return row;
}
}
这里不要关闭连接对象,因为可能这个方法被包含在一个事务中。 Dbutil.close(null,psmt);
十、三层架构
是软件设计分层的一种设计思想,它把一个软件分为 视图(界面) 业务操作 数据操作。
- 视图层: 负责数据展现
- 业务逻辑层: 负责功能业务
- 持久层:数据操作
在业务层和持久层中引入了接口,接口的目的是 解耦(实现模块间的低耦合)。
十一、连接池 与 Dbutils
11.1 连接池
连接池是一种池化技术,这种技术是把数据库的连接对象缓存到一个容器(池)中,减少了重复创建连接对象
和关闭连接对象,造成的性能损耗。当需要使用连接对象时,从连接池中获得一个,使用完成以后归还的连接池,以重复利用该连接对象,如果连接池中没有可用的连接对象,那么将等待。
连接池技术 中使用的连接对象并不是 原始的java.sql.Connection ,一般为它的子类型,该类型重写close(),这个关闭并不是关闭这个连接对象,而是把这个连接对象归还给连接池。
11.2 实现连接池原理
package com.qfedu.jdbc;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
//自定义连接池
public class MyPool {
//准备一个集合
static LinkedList<PoolConnection> connections = new LinkedList<>();
// 初始化连接池( 默认有4个连接对象 )
static{
// 初始化几个连接对象
for (int i=0;i<4;i++){
try {
//原生Conn
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/empdb?useSSL=false","root","123456");
//包装后的Conn
PoolConnection connection = new PoolConnection(conn);
// 添加到连接池
connections.add(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
//装饰模式:在不改变现有类的基础上,扩展或者整强该类的功能。
//在连接池技术中,Connection 需要被装饰。
//增强close方法
//提供一个方法,重连接池中获得连接对象
public static Connection getConnection(){
Connection conn = connections.removeFirst(); // 从连表的头部获取一个连接对象
return conn;
}
public static void main(String[] args) {
for(int i=0;i<1000;i++) {
Connection con = MyPool.getConnection();
System.out.println(con+":"+i);
PoolConnection obj = (PoolConnection) con;
try {
obj.close(connections);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
package com.qfedu.jdbc;
import java.sql.*;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
// 装饰类
public class PoolConnection implements Connection {
// 一个被装饰的连接对象,所有脏活累活交个 conn
java.sql.Connection conn;
//构造器,用于构造包装对象,需要出入一个原生对象
public PoolConnection(Connection conn){
this.conn= conn;
}
@Override
public void close() throws SQLException {
//这类啥都不写
}
public void close(LinkedList list) throws SQLException {
//这类啥都不写( 归还到集合 )
//list.addLast(conn); // 错误,conn是一个原生连接对象
list.addLast(this);// 我们自己是一个连接对象(自己是一个装饰连接对象),
}
/******************************************************************/
@Override
public Statement createStatement() throws SQLException {
return conn.createStatement();
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return conn.prepareStatement(sql);
}
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return conn.prepareCall(sql);
}
........
}
11.3 使用Druid 连接池
Druid(德鲁伊) 是一个连接池技术,来自于阿里,目前比较流行,事实上还有其他连接池产品,比如c3p0
Dbcp .... 。
11.4 配置和使用
- 导入jar
druid-1.1.10.jar
- 配置文件
druid.username=root
druid.password=123456
druid.driverClassName=com.mysql.jdbc.Driver
druid.url=jdbc:mysql://localhost:3306/empdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
druid.initialSize=4
druid.maxActive=8
druid.maxWaitMillis=5000
配置文件中的key 是根据连接池底层来的,底层默认就是这样的
- 编代码
package com.qfedu.jdbc;
import com.alibaba.druid.pool.DruidDataSource;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
// 这是一个活的连接池和连接对象的工具类
// DataSource 数据源,数据来源(连接池)。
public class DataSourceUtil {
//声明一个连接池的引用
private static DataSource dataSource = null;
//获得连接池的方法
public synchronized static DataSource getDataSource(){
//连接池为空
if(dataSource==null){
//使用Druid 连接池
DruidDataSource druidDataSource = new DruidDataSource();
//配置连接池、
Properties pro = new Properties();
//加载配置文件
try {
pro.load( Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties"));
} catch (IOException e) {
e.printStackTrace();
}
druidDataSource.configFromPropety(pro);// 通过一个Properties对象完成配置
dataSource = druidDataSource;
}
return dataSource;
}
}
十二、Commons-Dbutils
12.1 介绍
官网:https://commons.apache.org/proper/commons-dbutils/
Commons-Dbutils 是一个通用的轻巧的JDBC工具类库,Apache 软件开源项目,设计目的是简化jdbc编程。
- 它不是一个ORM框架 (ORM 框架比如Mybatis Hibatenate)
- 它不是 Dao 框架,但是他可以作为构建Dao 框架的基础。
- 它仅仅是一个jdbc的工具库,让我使用jdbc更简单而已。
12.2 快速上手
12.2.1 准备jar
commons-dbutils-1.7.jar
12.2.2 编码
@Test
public void t1 () throws SQLException {
// 1. 核心类
QueryRunner qr = new QueryRunner( DataSourceUtil.getDataSource() ); // 查询跑=> 运行查询=> 运行SQL
// 2. 核心方法 execute() 执行DML语句
int row = qr.execute( "delete from emp where empno=?", 7782 );
System.out.println(row);
}
前提是先要搞到 连接的工具类, 这种方式是使用连接池的方式,方法执行完成后,自动归还连接对象。
12.2.3 连接对象方式
@Test
public void t2 () throws SQLException {
//1 . 核心类
QueryRunner qr = new QueryRunner();
//2. 单独提供连接对象
Connection conn = DataSourceUtil.getDataSource().getConnection();
// 3 核心方法
int row =qr.update(conn,"insert into emp(empno,ename,job) values(?,?,?)",123,"文军","法师");
//要不要关连接对象
// 4 关闭资源
DbUtils.closeQuietly(conn);
}
前提是先要搞到 连接的工具类,这是使用单个连接对象的方式,这种范式执行完成后,需要自己手动关闭连接对象,如果你是使用的连接池获得的对象,那么就是归还给连接池。抽取出连接连接对象的目的在于某些地方需要用到事务,更方便管理。
12.3 重要组件和API
-
QueryRunner : SQL执行类,SQL执行器类,用于执行SQL语句(增删改查)。
- QueryRunner( DataSource ds ) : 提供一个连接池,自己从连接池中获取
- QueryRunner( ) : 不需要提供连接池,执行SQL再提供一个连接对象。
api:
int update( String sql , Object... args ): 执行DML
int update( Connection conn ,String sql , Object... args ): 执行DML
T query( String sql , ResultSetHandler handler , Object ... args ): 执行DQL
T query ( Connection conn, , ResultSetHandler handler , Object ... args ): 执行DQL
int execute( String sql , Object... args ): 执行DDL
int execute( Connection conn ,String sql , Object... args ): 执行DDL
-
DButils : 工具类,提供些关闭资源,提交回滚事务的方法。
- close( )
- commitAndClose( )
- rollback()andClose( )
-
ResultSetHandler 结果转换接口
可以自己通过匿名内部类实现该接口从而实现把数据查询结果构造成我们想要的类型。实际上工具提供了
很多现成的转换器。
- BeanHandler : 把一行查询结果转换成一实体类对象。
- BeanListHandler: 把多行查询结果转换成 对象集合
@Override
public Car findById(Integer id) throws Exception {
//明确SQL
String sql = "select * from sys_cars where id=?";
Car car =runner.query(sql, new BeanHandler<>( Car.class ),id);
return car;
}
@Override
public List<Car> list() throws Exception {
String sql = "select * from sys_cars";
List<Car> list = runner.query(sql, new BeanListHandler<>(Car.class));
return list;
}
需要在转换器的构造器中提供相应的 字节码类型,底层通过反射实现自动转换。
-
- ArrayHandler : 一行记录转 数组 Objcet[]
- ArrayListHandler: 多行记录转 List< Object[] >
- MapHander : 一行记录转 Map<String,Object>
- MapListHandler: 多行记录转 List< Map<String,Object> >
/***********************************以下测试其他转换器******************************************/
// 单行记录转 数组
public Object[] findArray(Integer id) throws Exception{
String sql = "select * from sys_cars where id=?";
return runner.query(sql, new ArrayHandler(),id);
}
// 单行记录转 MAP
public Map<String,Object> findMap(Integer id) throws Exception{
String sql = "select * from sys_cars where id=?";
return runner.query(sql, new MapHandler(),id);
}
// 多行记录转 二维数组
public List<Object[]> find2Array() throws Exception{
String sql = "select * from sys_cars";
List<Object[]> arry = runner.query(sql, new ArrayListHandler());
return arry;
}
// 多行记录转 List< Map<String,Object> >\
public List<Map<String,Object>> findListMap() throws Exception{
String sql = "select * from sys_cars";
List<Map<String, Object>> listmap = runner.query(sql, new MapListHandler());
return listmap;
}
总结:
这么多转换器,用什么? 常用的是 BeanHandler BeanListHandler, 这两种需要实体类支持,且实体类的属性必须和数据库表中的列名一致。否则映射不上。
其他几种转换器可以摆脱对实体类的依赖。在没有实体类的情况下可以使用,更方便。