一、JDBC是什么?
JDBC API是一个Java API,可以访问任何类型表列数据,特别是存储在关系数据库中的数据。JDBC代表Java数据库连接。
JDBC库中所包含的API通常与数据库使用于:
- 连接到数据库。
- 创建SQL或MySQL语句。
- 在数据库中执行SQL或MySQL查询。
- 查看和修改数据库中的数据记录。
- 完成对数据的增删改查
https://mvnrepository.com/artifact/mysql/mysql-connector-java/8.0.21
https://mvnrepository.com/artifact/mysql/mysql-connector-java/5.1.47
二、创建JDBC应用程序
0. 引入jar
在工程下创建一个文件lib,把mysql-connector-java-8.0.21.jar 放进去,并添加为类库
建立一个JDBC应用程序,本教程中以Java连接MySQL为一个示例,分六个步骤进行:
1. 导入包
在程序中包含数据库编程所需的JDBC类。大多数情况下,使用 import java.sql.*
就足够了,如下所示:
import java.sql.*;
2. 注册JDBC驱动程序
需要初始化驱动程序,这样就可以打开与数据库的通信。以下是代码片段实现这一目标:
Class.forName("com.mysql.jdbc.Driver");
3. 打开一个连接
使用DriverManager.getConnection()
方法来创建一个Connection
对象,它代表一个数据库的物理连接,如下所示:
static final String USER = "root";
static final String PASS = "pwd123456";
System.out.println("Connecting to database...");
conn = DriverManager.getConnection(DB_URL,USER,PASS);
**Connection **表示一个连接类型 它的实例就是 一个具体的 连接对象
**DriverManager **是一个驱动管理类 , 他的作用是创建 连接对象。
4. 执行一个查询
需要使用一个类型为Statement
或PreparedStatement
的对象,并提交一个SQL语句到数据库执行查询。如下:
stmt = conn.createStatement();
String sql;
sql = "SELECT id, first, last, age FROM Employees";
ResultSet rs = stmt.executeQuery(sql);
**Statement **表示一个语句,它的实例就是一个 具体的语句对象,语句对象可以执行 sql 语句。
**ResultSet **表示一个结果 ,它类似于一个集合,保存了从数据库中查询的数据。
如果要执行一个SQL语句:UPDATE
,INSERT
或DELETE
语句,那么需要下面的代码片段:
stmt = conn.createStatement();
String sql;
sql = "DELETE FROM Employees";
int rs = stmt.executeUpdate(sql);
**executeQuery() 执行查询语句 **
executeUpdate() 执行增删改语句
5. 从结果集中提取数据
这一步中演示如何从数据库中获取查询结果的数据。可以使用适当的ResultSet.getXXX()
方法来检索的数据结果如下:
//STEP 5: Extract data from result set
while(rs.next()){
//Retrieve by column name
int id = rs.getInt("id");
int age = rs.getInt("age");
String first = rs.getString("first");
String last = rs.getString("last");
//Display values
System.out.print("ID: " + id);
System.out.print(", Age: " + age);
System.out.print(", First: " + first);
System.out.println(", Last: " + last);
}
6. 清理环境资源
在使用JDBC与数据交互操作数据库中的数据后,应该明确地关闭所有的数据库资源以减少资源的浪费,对依赖于JVM的垃圾收集如下:
rs.close();
stmt.close();
conn.close();
7. 完整示例
package case1;
// 使用JDBC面向接口编程,使用的包都是java.sql
import java.sql.*;
/**
* JDBC 基本使用流程
*/
public class LearnJdbc {
// 1. 注册驱动
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");// MySQL8 jar
//Class.forName("com.mysql.jdbc.Driver"); // MySQL7 jar
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws SQLException {
// 2. 创建连接对象
String url = "jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection = DriverManager.getConnection(url, "root", "123456");
// 3. 执行SQL
String sql = "select sno,sname,info,photo,ranks,fen from tb_singer";
Statement statement = connection.createStatement(); //创建一个语句对象
ResultSet rs = statement.executeQuery(sql);
// 4. 处理结果(查询)
while ( rs.next() ){
//解析出每行数据
int sno = rs.getInt("sno");
String sname = rs.getString("sname");
String info = rs.getString("info");
int fen = rs.getInt("fen");
String photo = rs.getString("photo");
int ranks = rs.getInt("ranks");
//输出
System.out.println( sno+"\t"+ sname+"\t"+info+"\t"+fen+"\t"+photo+"\t"+ranks);
}
// 5. 资源清理
rs.close();
statement.close();
connection.close();
}
}
三、参数化SQL
3.1 SQL注入问题
SQL 注入指的存在SQL拼接的情况下,用户恶意传入 SQL片段, 这些片段和原有的SQL组成的新的SQL改变了SQL本身的含义。
import java.sql.*;
import java.util.Scanner;
/**
* SQL 注入问题
*/
public class LearnJdbc {
// 1. 注册驱动
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");// MySQL8 jar
//Class.forName("com.mysql.jdbc.Driver"); // MySQL7 jar
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws SQLException {
Scanner scanner = new Scanner(System.in);
System.out.println("输入账号");
String username = scanner.nextLine();
System.out.println("输入密码");
String password = scanner.nextLine();
// 2. 创建连接对象
String url = "jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection = DriverManager.getConnection(url, "root", "123456");
// 3. 执行SQL
String sql = "select * from tb_user where username= '"+username+"' and password = '"+password+"' ";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
// 4. 处理结果(查询)
if( resultSet.next() ){
System.out.println("登陆成功");
}else{
System.out.println("登陆失败");
}
// 5. 资源清理
resultSet.close();
statement.close();
connection.close();
}
}
当用户输入 非法的用户名和密码 可以登陆成功。
3.2 参数化SQL应用
与Statement 不同 参数化SQL 使用 PreparedStatement 语句对象执行SQL. 把SQL中需要填充值得位置 使用 ? 占位。然后使用相应的setXXX( ) 进行赋值。
// 3. 执行SQL
String sql = "select * from tb_user where username= ? and password= ? "; //准备SQL
PreparedStatement preparedStatement = connection.prepareStatement(sql); //创建语句对象 预编译SQL
preparedStatement.setString(1,username);//占位符赋值
preparedStatement.setString(2,password);//占位符赋值
ResultSet resultSet = preparedStatement.executeQuery();
setXXX( int index , XXX value ) 下标从1 开始 , 类型 XXX 需要根据 列类型选择。set api的选择需要集合数据库类型和java类型的对应关系。
SQL 类型 与 Java类型
char String
varchar String
int int
bigint long
float float
double double
decimal BigDecimal
date | datetime Date|String
3.3 实现数据CURD
3.3.1 查询示例
// 3. 执行SQL
String sql = "select * from tb_singer where ranks > ? ";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setInt(1,5);
ResultSet rs = psmt.executeQuery();
3.3.2 新增实例
// 3. 执行SQL
String sql = "insert into tb_singer(sname,info,fen,photo,ranks) values(?,?,?,?,?)";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setString(1,"小刚" );
psmt.setString(2,"忧郁情歌王子" );
psmt.setInt(3,40000 );
psmt.setString(4,"xxxx.png" );
psmt.setInt(5,67);
int row = psmt.executeUpdate();
3.3.3 修改示例
// 3. 执行SQL
String sql = "update tb_singer set sname=? , ranks = ? where sno = ? ";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setString(1,"王宝强");
psmt.setInt(2, 78 );
psmt.setInt(3,7);
int row = psmt.executeUpdate();
3.3.4 删除示例
// 3. 执行SQL
String sql = "delete from tb_singer where sno = ? ";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setInt(1,5);
int row = psmt.executeUpdate();
四、工具封装
5.1 准备配置文件
db.properties 配置文件
jdbc.url=jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=123456
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
5.2 加载并初始化
public class JdbcUtil {
/**
* 连接信息
*/
public static String URL = null;
public static String USERNAME = null;
public static String PASSWORD = null;
public static String DRIVER_CLASS = null;
/**
* 加载配置文件
* */
static {
Properties config = new Properties();
try {
config.load(JdbcUtil.class.getResourceAsStream("db.properties"));
//取数据赋值
URL = config.getProperty("jdbc.url");
USERNAME = config.getProperty("jdbc.username");
PASSWORD = config.getProperty("jdbc.password");
DRIVER_CLASS = config.getProperty("jdbc.driverClassName");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 注册驱动
*/
static {
try {
Class.forName(DRIVER_CLASS);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//其他方法
}
5.3 获取连接对象封装
/**
* 获得连接对象
* @return
*/
public static Connection getConn() {
Connection connection = null;
try {
//获得连接对象
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return connection;
}
5.4 资源释放封装
/**
释放资源
*/
public static void close(Connection conn, Statement smt , ResultSet rs){
if( rs != null){
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if( smt !=null ){
try {
smt.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if( conn!=null ){
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
5.5 通用增删改封装
public static int update( String sql , Object... params ) throws SQLException {
//获得连接对象
Connection connection = getConnection();
//创建语句对象
PreparedStatement psmt = connection.prepareStatement(sql);
//获得参数化SQL中的 ? 个数
int count = psmt.getParameterMetaData().getParameterCount();
//遍历参数个数 从数组中取值来赋值
for(int i=1; i<=count; i++){
psmt.setObject(i, params[i-1] );
}
//执行SQL语句
int row = psmt.executeUpdate();
//返回执行结果
return row;
}
5.6 通用查询封装
public static <T> T query(String sql ,
ResultSetHandler<T> hander,
Object... params ) throws SQLException {
//1 获得一个连接对象
Connection connection = getConnection();
//2 创建一个语句对象
PreparedStatement psmt = connection.prepareStatement(sql);
//3.设置问号的值
int count = psmt.getParameterMetaData().getParameterCount();
for(int i=1; i<=count; i++){
psmt.setObject(i,params[i-1]);
}
//4.执行SQL
ResultSet resultSet = psmt.executeQuery();
// 5. 遇到一个麻烦,我们不知道这个结果集到底是哪个表的结果 也就不知道把结果转成什么对对象集合?
// 回调(把转换结果的工作交给转换器做,resultSet交给转换器,向他要结果。)。
T mapper = hander.mapper(resultSet);
//6.关闭资源
close( resultSet,psmt,connection );
//返回。
return mapper;
}
5.6.1 使用回调机制
System.out.println("------------------");
List<Dept> list = JdbcUtil.query("select * from dept", new ResultSetHandler<List<Dept>>() {
@Override
public List<Dept> mapper(ResultSet rs) throws SQLException {
List<Dept> list = new ArrayList<>();
while (rs.next()){
Dept dept = new Dept();
dept.setDeptno( rs.getInt("deptno") );
dept.setDname( rs.getString("dname"));
dept.setLoc(rs.getString("loc"));
list.add(dept);
}
return list;
}
});
这种方式的问题是,没次调用都要编写 回调代码,来处理 ResultSet 到结果的转换。它过于麻烦。
5.6.2 查询结果处理器
为了解决每次调用query方法都要自己提供转换逻辑,可以定义一个转换接口,定义一个转换方法,然后提供一些现成的转换器实现类,调用的时候可以根据不同的需求来使用不同的转换器对象。
定义转换接口
/**
* 这是一个转换接口,提供一个转换方法
*/
public interface ResultSetHandler<T> {
//提供一个结果集,返回要给"某个对象"。
T mapper(ResultSet rs) throws SQLException;
}
BeanHandler
这种转换器,可以把单行查询结果转成 对象。
package com.qfedu.handler;
import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
/**
* 它是 ResultSetHandler 实现类
* 他目标是把多行查询结果转成 单个对象。
* 问题: 1. 不知道结果集合中有哪些列 ? -- 可以通过 元数据知道
* 2. 不直到封装成那种类型对象 ? -- 可以通过 类对象指定
*/
public class BeanHandler<T> implements ResultSetHandler<T> {
private Class<T> cls;
public BeanHandler(Class<T> cls) {
this.cls = cls;
}
@Override
public T mapper(ResultSet rs) throws SQLException {
//获得列的数量
ArrayList<String> columns = new ArrayList<>();
int columnCount = rs.getMetaData().getColumnCount();
for(int i=1; i<=columnCount; i++ ){
//根据下标获得列名
String column = rs.getMetaData().getColumnLabel(i);
//把每列名字存入集合
columns.add(column);
}
T obj = null;
//遍历结果
if (rs.next()){
//创建一个对象,来保持这一行的所有数据( 使用对象的属性来保存字段数据 )
try {
obj = cls.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//遍历这一行的全部列
for (int i = 0; i < columns.size(); i++) {
//这是一行的没一列,把这一列给到对象的属性
Object value = rs.getObject(columns.get(i));
try {
//加入这个列叫ename -> 属性ename 找同名属性
Field filed = cls.getDeclaredField(columns.get(i));
//设置访问权限
filed.setAccessible(true);
//为它赋值
filed.set(obj,value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//返回这个对象
return obj;
}
}
p
BeanListHandler
这中结果处理器,可以把查询的多行记录转成 对象集合。
package com.qfedu.handler;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* 它是 ResultSetHandler 实现类
* 他目标是把多行查询结果转成 对象的集合。
* 问题: 1. 不知道结果集合中有哪些列 ? -- 可以通过 元数据知道
* 2. 不直到封装成那种类型对象 ? -- 可以通过 类对象指定
*/
public class BeanListHandler<T> implements
ResultSetHandler< List<T> > {
// 定义要给于该转换器绑定的类对象
private Class<T> cls;
//创建转换器时传入一个类对象
public BeanListHandler(Class<T> cls) {
this.cls = cls;
}
//该类的核心转换方法
@Override
public List<T> mapper(ResultSet rs) throws SQLException {
//准备集合
List<T> list = new ArrayList<>();
//获得列的数量
ArrayList<String> columns = new ArrayList<>();
int columnCount = rs.getMetaData().getColumnCount();
for(int i=1; i<=columnCount; i++ ){
//根据下标获得列名
String column = rs.getMetaData().getColumnLabel(i);
//把每列名字存入集合
columns.add(column);
}
//遍历结果
while (rs.next()){
//创建一个对象,来保持这一行的所有数据( 使用对象的属性来保存字段数据 )
T obj = null;
try {
obj = cls.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//遍历这一行的全部列
for (int i = 0; i < columns.size(); i++) {
//这是一行的没一列,把这一列给到对象的属性
Object value = rs.getObject(columns.get(i));
try {
//加入这个列叫ename -> 属性ename 找同名属性
Field filed = cls.getDeclaredField(columns.get(i));
//设置访问权限
filed.setAccessible(true);
//为它赋值,赋值前需要判断一下value的类型 应为有可能 value类型和属性类型不兼容问题。
//比如: 数据库是BigDecimal 尔实体类是Double 那么赋值前需要把 BigDecimal转成Double
if( value!=null && ( value.getClass() == BigDecimal.class ) ){
value = new BigDecimal( value.toString() ).doubleValue();
}
filed.set(obj,value);
} catch (Exception e) {
e.printStackTrace();
}
}
//把对象添加集合
list.add(obj);
}
return list;
}
}
MapHandler 处理器
package util.handler;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class MapHandler implements ResultSetHandler< Map<String,Object>> {
@Override
public Map<String, Object> mapper(ResultSet resultSet) {
//声明一个待返回的对象
Map<String,Object> map = null;
try {
//实例化对象
map = new HashMap<>();
//获得元数据
ResultSetMetaData metaData = resultSet.getMetaData();
//获得列名
List<String> cloumns = new ArrayList<>();
int columnCount = metaData.getColumnCount();//获得总列数
for(int i=0; i< columnCount; i++){
cloumns.add(metaData.getColumnName(i+1) );//获得列名字
}
//有了列名就可以取值了
for (int i=0;i<cloumns.size();i++){
//获得数据
Object value = resultSet.getObject(cloumns.get(i));
//添加到Map
map.put(cloumns.get(i),value);
}
} catch (Exception sqlException) {
sqlException.printStackTrace();
}
return map;
}
}
思考,你可与发挥你的想想,来编写更多更好用的转换器。
完成示例
package com.qfedu.util;
import com.qfedu.entity.Emp;
import com.qfedu.handler.ResultSetHandler;
import java.sql.*;
import java.util.List;
import java.util.Properties;
/**
* 目标: 封装一个数据库工具类
* a) 快速获得连接对象
* b) 关闭资源
* c) 执行DML
* d)执行DQL
*/
public class JdbcUtil {
//定义一个静态属性来保存从文件中加载的配置信息,他是hashtable 的子类,本质还是一个map集合。
private static Properties config;
//静态代码块,用来加载配置文件,
//静态代码块,类加载执行一次,恰好用于来加载驱动,因为驱动也只需要加载一次。
static {
config = new Properties();
try {
//如果 Thread.currentThread().getContextClassLoader().getResourceAsStream("jdbc.properties") 这种方式必须把
//配置文件放置到src下
//如果使用 JdbcUtil.class.getResourceAsStream("jdbc.properties") 这种方式,必须把配置文件放置于JdbcUtil 同包。
config.load( JdbcUtil.class.getResourceAsStream("jdbc.properties"));
//加载驱动
Class.forName( config.getProperty("jdbc.driverClassName") );
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获得数据库连接
* @return 返回一个连接对象
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
// 这里创建连接需要的信息 从配置对象 config 中获取它是一个map 通过key来获取值。 不要把key写错了,否则拿到null
// 这里的key和 jdbc.properties 文件中的key保持一致。
Connection conn= DriverManager.getConnection(
config.getProperty("jdbc.url"),
config.getProperty("jdbc.user"),
config.getProperty("jdbc.password") );
return conn;
}
/**
* 关闭数据库相关资源
* @param rs 结果集,只有查询才有
* @param smt 语句对象
* @param conn 连接对象
* @throws SQLException
*/
public static void close(ResultSet rs, Statement smt, Connection conn ) throws SQLException {
//所有资源关闭前都需要验空 否则引发空指针异常
if( rs!=null ){
rs.close();
}
if(smt!=null){
smt.close();
}
if(conn!=null){
conn.close();
}
}
/**
* 通用更新数据的方法
* @param sql 执行的语句
* @param params 占位符的值是一个可变参数,不同的sql 参数数量可能不同。
* @return
* @throws SQLException
*/
public static int update( String sql , Object... params ) throws SQLException {
//获得连接对象
Connection connection = getConnection();
//创建语句对象
PreparedStatement psmt = connection.prepareStatement(sql);
//获得参数化SQL中的 ? 个数
int count = psmt.getParameterMetaData().getParameterCount();
//遍历参数个数 从数组中取值来赋值
for(int i=1; i<=count; i++){
psmt.setObject(i, params[i-1] );
}
//执行SQL语句
int row = psmt.executeUpdate();
//返回执行结果
return row;
}
// select * from emp where sal > ? and job=? ;
// select * from dept where deptno = ?;
// 查询结果如果处理问题? 10 列 3
public static <T> T query(String sql ,
ResultSetHandler<T> hander,
Object... params ) throws SQLException {
//1 获得一个连接对象
Connection connection = getConnection();
//2 创建一个语句对象
PreparedStatement psmt = connection.prepareStatement(sql);
//3.设置问号的值
int count = psmt.getParameterMetaData().getParameterCount();
for(int i=1; i<=count; i++){
psmt.setObject(i,params[i-1]);
}
//4.执行SQL
ResultSet resultSet = psmt.executeQuery();
// 5. 遇到一个麻烦,我们不知道这个结果集到底是哪个表的结果 也就不知道把结果转成什么对对象集合?
// 回调(把转换结果的工作交给转换器做,resultSet交给转换器,向他要结果。)。
T mapper = hander.mapper(resultSet);
//6.关闭资源
close( resultSet,psmt,connection );
//返回。
return mapper;
}
}
六、事务
6.1 事务概念
MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务
- 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
- 事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
- 事务用来管理 insert,update,delete 语句
6.2 事物特性
一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
- **原子性:**一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- **一致性:**在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)Oralce、可重复读(repeatable read)(MySQL )和串行化(Serializable)。
- **持久性:**事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
脏读: 读到别人没有提交的数据 【 > 大于读未提交 】
不可重复:同一个事务中两次读取数据不一致【 > 读提交 】 update 操作
幻读: 同一个事务中相同条件的两次查询结果行不同 【>=大于可重复读】 insert 操作
mysql 查看当前隔离级别
select @@tx_isolation ; -- mysql.5.x
select @@transaction_isolation ; -- mysql 8.x
mysql 修改隔离级别
set global transaction isolation level repeatable read ; --全局级别修改
set session transaction isolation level repeatable read; -- 回话级别修改
mysql 事务控制相关
start transaction; -- 开启事务
commit; -- 提交事务
rollback; --回滚事务
6.3 JDBC事物控制
6.3.1 编程中的事务
编程中的事务往往就是一个业务 。只有对数据进行 增 删 改 组合操作 ,才需要事务控制。也就是说一个事务包含至少2条DML SQL语句的组合, 这些SQL语句组合在一起就一个事务,但是默认情况下每一条DML SQL的执行, 默认就是一个事务单元,每条SQL并不知道自己会处于一个事务中,默认情况下执行完毕就自动提交。这样就存在一个问题,各个SQL 没有关联,就会存在部分成功,部分失败。
但是实际上我们认为的事务往往是一个业务 ,这个业务通常组合了多条DML语句, 必需要把这多条同生共死的SQL纳入到一个真正的事务中。
6.3.2 事务控制核心
关闭SQL 执行完毕 默认提交的行为 connection.setAutoCommit( false );
保证同一个事务中的多个SQL语句执行是使用的同一个连接对象,期间不可关闭。
- 执行期间 出现异常: 回滚 connection.rollback();
- 执行期间 没有异常: 提交 connection.commit();
6.3.3 事务控制思想
控制事务的核心思想就是 同一事务中的多条SQL 必须使用同一个连接对象,中途不可关闭,有异常(可以是自定义异常)回滚,无异常提交。
问题关键点1 在于如何控制使用 相同连接对象 ,将来单一的SQL被封装Dao 方法中,彼此不知道对方的存在,也不知道自己会处于事务之中。
问题关键点2 将来的程序必然是多线程的,也就是同一个业务,可能存在多个线程执行,所以必须保证不同线程中的连接对象是隔离的。
6.3.4 ThreadLocal 实现事务控制
ThreadLocal 称为线程本地变量,是一种容器(实际不负责存储数据,而是识别线程,把数据存入 线程的 ThreadLocalMap),只能存放一个值,作用是可以为不同线程建立变量副本,保证同一个线程再任何时间任何地点,都能获得到相同对象,且这个对象是线程隔离的,它所体现的思想是_以空间换时间,不用线程同步_。
核心方法
set( Object value ):void 添加数据
get( ): Object 获得数据
remove( ): 移除数据
原理
Thread 源码
每个线程对象,都存在一个 threadLocals 属性,它是一个类似Map的集合(并非HashMap),这个集合是ThreadLocal的一个内部类类型ThreadLocalMap.
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
也就是说 每个线程可以存储多个 ThreadLocal 变量。
ThreadLocal 源码
public class ThreadLocal<T> {
/**
获得当前线程的 ThreadLocalMap 集合属性 threadLocals
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
获得当前线程的 threadLocals 从中获得数据副本
loca.get()
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//获得不到返回默认值 这个方法没有重写的情况下返回null
}
/**
设置数据到当前线程的 threadLocals 中去
local.set()
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
移除当前线程 threadLocals 中的数据,防止内存泄漏
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
}
6.3.5 代码实现
package util;
import java.io.FileReader;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
/**
* JDBC 操作工具类
* 1. 连接对象获取
* 2. 资源释放
* 3. 执行DML通用方法
* 4. 执行DQL通用方法
*/
public class JdbcUtil {
//准备一个容器变量
private ThreadLocal<Connection> local = new ThreadLocal<>();
/**
* 获得连接对象
* @return
*/
public static Connection getConn() {
if(local.get()==null){
Connection connection = null;
try {
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
local.set(connection);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return local.get();
}
/** 增 删 改通用方法*/
public static int update( String sql , Object... params ) throws SQLException {
//1 连接件对象
Connection conn = getConn();
//3 执行SQL
PreparedStatement psmt = conn.prepareStatement(sql);
for (int i=0;i<params.length;i++){
psmt.setObject(i+1,params[i]);
}
int row = psmt.executeUpdate();
//关闭
JdbcUtil.close(null,psmt,null);//暂时不可关闭
return row;
}
/**
* 开始事务
*/
public static void beginTransaction(){
try {
JdbcUtil.getConn().setAutoCommit(false);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 提交事务
*/
public static void commit(){
try {
JdbcUtil.getConn().commit();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
* 回滚
*/
public static void rollback(){
try {
JdbcUtil.getConn().rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
/**
释放资源
*/
public static void close(Connection conn, Statement smt , ResultSet rs){
if( rs != null){
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if( smt !=null ){
try {
smt.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if( conn!=null ){
try {
//还原成默认提交
conn.setAutoCommit(true);
//关闭连接对象
conn.close();
//防止内存泄漏
local.remove();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
public static void buy(){
OrderDao orderDao = new OderDaoImpl();
StockDao stockDao = new StockDaoImpl();
try {
//开启事务
JdbcUtil.beginTransaction();
//2. 减库存
int c = 1/0; //模拟一个异常
int row2 = stockDao.reduce(900, 10);
//1. 下单
int row1 = orderDao.createOrder(900, 10);
//提交事务
JdbcUtil.commit();
} catch (Exception throwables) {
//出现异常回滚
JdbcUtil.rollback();
}finally {
//关闭连接
JdbcUtil.close( JdbcUtil.getConn() ,null, null);
}
}
七、连接池
7.1 什么是连接池
创建一个容器 缓存多个数据库连接对象, 以减少 创建 销毁连接次数,提升效率 ,因为频繁创建销毁连接对象会占用用较大内存开销。 常见的连接池技术比如C3P0,DBCP**,Durid(**阿里巴巴)。不同的连接池使用方法基本都一样。DataSource
7.2 常见的连接池
7.3 Druid连接池使用
7.3.1 引入jar
下载 https://mvnrepository.com/artifact/com.alibaba/druid/1.2.4 druid.jar文件
7.3.2 准备配置文件
druid.url=jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
druid.username=root
druid.password=123456
druid.driverClassName=com.mysql.cj.jdbc.Driver
druid.maxActive=8
druid.maxWait=5000
配置参考
7.3.3 整合Durid
package util;
import com.alibaba.druid.pool.DruidDataSource;
import javax.sql.DataSource;
import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* JDBC 操作工具类
* 1. 连接对象获取
* 2. 资源释放
* 3. 执行DML通用方法
* 4. 执行DQL通用方法
*/
public class JdbcUtil {
//声明一个连接池
private static DataSource dataSource = null;
//线程级别容器
private static ThreadLocal<Connection> local = new ThreadLocal<>();
/**
* 加载配置文件
* */
static {
Properties config = new Properties();
try {
config.load(JdbcUtil.class.getResourceAsStream("db.properties"));
//初始化连接池
DruidDataSource ds = new DruidDataSource();
//配置数据源(通过读取的配置信息)
ds.configFromPropety( config );
//赋值
dataSource = ds;
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获得连接对象
* @return
*/
public static Connection getConn() {
//判断当前线程中是否存在 连接对象
if( local.get() == null){
try {
//从连接池中获得连接对象
Connection conn = dataSource.getConnection();
//把连接对象 添加 到线程中
local.set(conn);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
//从当前线程中获取自己的连接对象返回。
return local.get();
}
}
为了方便阅读,省去其他方法。
四 ORM 思想
4.1 什么ORM 思想
对象关系映射(英语:Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。如今已有很多免费和付费的ORM产品,而有些程序员更倾向于创建自己的ORM工具。
核心点:
- 把一行数据 看做一个对象 (多行数据就是对象的集合)
- 一张表对应一个类 (实体类) 。
- 表中的列 就是类属性。
4.2 ORM实现
4.2.1 查询功能示例
// 查询排名大于 5的歌手
public static void testQuery() throws SQLException {
// 2. 创建连接对象
String url = "jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection = DriverManager.getConnection(url, "root", "123456");
// 3. 执行SQL
String sql = "select * from tb_singer where ranks > ? ";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setInt(1,5);
ResultSet rs = psmt.executeQuery();
// 4. ORM
List<Singer> list = new ArrayList<>();
while (rs.next()){
//解析出每行数据
int sno = rs.getInt("sno");
String sname = rs.getString("sname");
String info = rs.getString("info");
int fen = rs.getInt("fen");
String photo = rs.getString("photo");
int ranks = rs.getInt("ranks");
//把这一行全部数据看成一个对象的全部属性,可以创建一个对象
Singer singer = new Singer();
singer.setSno(sno);
singer.setSname(sname);
singer.setInfo(info);
singer.setFen(fen);
singer.setRanks(ranks);
singer.setPhoto(photo);
//添加到集合
list.add(singer);
}
//5. 关闭
rs.close();
psmt.close();
connection.close();
//获得集合
list.stream().forEach(System.out::println);
}
4.2.2 新增功能示例
// 新增一个新歌手
public static void testAdd() throws SQLException {
//创建对象
Singer singer = new Singer(null,"郑爽","hehe",20000000,"xxx.png",20);
// 2. 创建连接对象
String url = "jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection = DriverManager.getConnection(url, "root", "123456");
// 3. 执行SQL
String sql = "insert into tb_singer(sname,info,fen,photo,ranks) values(?,?,?,?,?)";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setString(1,singer.getSname() );
psmt.setString(2,singer.getInfo() );
psmt.setInt(3,singer.getFen() );
psmt.setString(4,singer.getPhoto() );
psmt.setInt(5,singer.getRanks() );
int row = psmt.executeUpdate();
// 4.处理结果
System.out.println(row);
// 5
psmt.close();
connection.close();
}
4.2.3 修改功能示例
// 把编号为6的歌手 名字改为张小杰 排名改为 20
public static void testUpdate() throws SQLException {
//新数据
Singer singer = new Singer(6,"张小杰",null,null,null,20);
// 2. 创建连接对象
String url = "jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection = DriverManager.getConnection(url, "root", "123456");
// 3. 执行SQL
String sql = "update tb_singer set sname=? , ranks = ? where sno = ? ";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setString(1,singer.getSname());
psmt.setInt(2, singer.getRanks() );
psmt.setInt(3,singer.getSno());
int row = psmt.executeUpdate();
//4 处理
System.out.println(row);
//5 关闭
psmt.close();
connection.close();
}
4.2.4 删除功能示例
// 删除 歌手编号为 5的信息
public static void testDelete () throws SQLException {
// 2. 创建连接对象
String url = "jdbc:mysql://localhost:3306/qq_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false";
Connection connection = DriverManager.getConnection(url, "root", "123456");
// 3. 执行SQL
String sql = "delete from tb_singer where sno = ? ";
PreparedStatement psmt = connection.prepareStatement(sql);
psmt.setInt(1,5);
int row = psmt.executeUpdate();
//4 处理结果
System.out.println(row);
//5 关闭
psmt.close();
connection.close();
}
4.3 DAO 模式
4.3.1 什么是Dao模式
DAO (Data Access objects 数据存取对象)是指位于业务逻辑和持久化数据之间实现对持久化数据的访问。通俗来讲,就是将数据库操作都封装起来。 对外提供相应的接口,从而实现以面向对象的方式来操作数据。
4.3.2 Dao模式结构
4.3.3 Dao模式编程实现
Dao 接口
package com.qfedu.dao;
import com.qfedu.entity.User;
import java.sql.SQLException;
import java.util.List;
/**
* 对user表操作的功能接口
*/
public interface UserDao {
//增
int insert( User user ) throws SQLException;
//删
int deleteById( int id ) throws SQLException;
//改
int update( User newUser ) throws SQLException;
//查
User findByName(String username) throws SQLException;
User findById(int id) throws SQLException;
List<User> findAll() throws SQLException;
}
Dao实现类
package com.qfedu.dao.impl;
import com.qfedu.dao.UserDao;
import com.qfedu.entity.User;
import com.qfedu.handler.BeanHandler;
import com.qfedu.handler.BeanListHandler;
import com.qfedu.util.JdbcUtil;
import java.sql.SQLException;
import java.util.List;
/**
* 对刚刚接口的具体实现
*/
public class UserDaoImpl implements UserDao {
@Override
public int insert(User user) throws SQLException {
String sql = "insert into users(username,password,tel,email,address,reg_date) values(?,?,?,?,?,now())";
return JdbcUtil.update(sql, user.getUsername(),user.getPassword(),user.getTel(),user.getEmail(),user.getAddress());
}
@Override
public int deleteById(int id) throws SQLException {
String sql = "delete from users where id = ?";
return JdbcUtil.update(sql,id);
}
@Override
public int update(User newUser) throws SQLException {
String sql = "update users set username=?, password=?, tel = ? , email= ?, address=? where id=? ";
return JdbcUtil.update(sql,newUser.getUsername(),
newUser.getPassword(),newUser.getTel(),newUser.getEmail(),newUser.getAddress(),newUser.getId());
}
@Override
public User findByName(String username) throws SQLException {
String sql = "select * from users where username= ?";
return JdbcUtil.query(sql, new BeanHandler<>(User.class),username);
}
@Override
public User findById(int id) throws SQLException {
String sql = "select * from users where id= ?";
return JdbcUtil.query(sql, new BeanHandler<>(User.class),id);
}
@Override
public List<User> findAll() throws SQLException {
String sql = "select * from users";
return JdbcUtil.query(sql,new BeanListHandler<>(User.class));
}
}
测试Dao
package com.qfedu.test;
import com.qfedu.dao.UserDao;
import com.qfedu.dao.impl.UserDaoImpl;
import com.qfedu.entity.User;
import java.sql.SQLException;
import java.util.List;
public class TestDao {
public static void main(String[] args) throws SQLException {
System.out.println("----------------");
//查询宣全部用户
UserDao userDao = new UserDaoImpl();
List<User> all = userDao.findAll();
all.forEach(System.out::println);
System.out.println("----------------");
// 根据id 查询用户
User user = userDao.findById(5);
System.out.println(user);
// 根据用户名 查询用户
System.out.println("-------------------");
User user2 = userDao.findByName("css");
System.out.println(user2);
// 根据 添加 用户
System.out.println("------------------------");
User uu = new User();
uu.setUsername("lsj");
uu.setPassword("qf12345");
uu.setEmail("lianghui@1000phone.com");
int row = userDao.insert(uu);
System.out.println(row);
System.out.println("----------------------------");
// 根据id 删除用户
int row2 = userDao.deleteById(8);
System.out.println(row2);
System.out.println("----------------------------------");
//查询并修改用户
User old = userDao.findById(5);
old.setEmail("@1000phone");
old.setAddress("日本");
int row = userDao.update(old);
System.out.println(row);
}
}
八、三层架构
8.1 三层架构概念
三层架构就是为了符合“高内聚,低耦合”思想,把各个功能模块划分为表示层(UI)、业务逻辑层(BLL)和数据访问层(DAL)三层架构,各层之间采用接口相互访问,并通过对象模型的实体类(Model)作为数据传递的载体,不同的对象模型的实体类一般对应于数据库的不同表,实体类的属性与数据库表的字段名一致。
三层架构区分层次的目的是为了 “高内聚,低耦合”。开发人员分工更明确,将精力更专注于应用系统核心业务逻辑的分析、设计和开发,加快项目的进度,提高了开发效率,有利于项目的更新和维护工作
8.2 示意图
8.3 门面模式(理解)
业务层组合调用完成逻辑,实现Dao方法的复用,可以这样理解,任何复杂的业务功能底层都是组合调用多个Dao方法共同完成,所以在这里就可能存在事物管理问题。当然也存在一个service方法只会调用一个Dao的一个方法,我们把这种现象称为透传。
8.4 编码实现
参看代码
九、ApacheCommonDbutil【了解】
9.1 Dbutil 介绍
该工具是 Apache 开源软件基金会旗下 Apache Commons 工具库中的一个对 jdbc 封装的小工具。
使用 DbUtil 的一些优点是:
- 资源泄漏的可能性不存在。正确的JDBC编码并不难,但它是耗时和乏味的。这通常会导致可能难以追踪的连接泄漏。
- 更清洁、更清晰的持久性代码。在数据库中持续数据所需的代码数量大幅减少。其余代码清楚地表达了您的意图,而不会被资源清理杂乱无章。
- 从结果集自动填充 JavaBean 属性。您不需要通过调用设置器方法手动将列值复制到Bean实例中。结果集的每一行都可以由一个完全填充的Bean实例表示
http://commons.apache.org/proper/commons-dbutils/download_dbutils.cgi
9.2 Dbutil 上手
使用Dbutil 非常简单,使用前通常需要做以下准备。
- 引入相关jar ,目前就3个 commons-dbutils-1.7.jar druid-1.2.4.jar mysql-connector-java-8.0.21.jar (DB三剑客)
- 准备一个工具类 可以方便获得 连接池或者**连接对象 **(我们使用自己封装的JdbcUtil.java)。
- 创建QueryRunner 实例 ,指定 连接池( 可选 )。
- 执行方法 DML 语句使用 update 方法 执行 DQL使用 query方法
方法解析
- **update( String sql , Object … params ):int **
接收一个SQL语句,以及可变参数值,返回实际影响行数, 没啥技术含量跟咱们封装的一致。
- **query( String sql , ResultSetHandler handler , Object … params ) : T **
接收一个SQL语句,以及可变参数值,以及一个结果处理器 handler,它的作用是 负责把 查询结果转换成 某种类型的数据,可以是数组 Map Bean 。
结果处理器
单结果处理器:查询结果只有一行
- **ArrayHandler **把一行结果转成数组Objet[]
- **MapHandler ** 把一行结果转成 Map集合( 列名为key ) ** **
- **BeanHandler ** 把一个行结果转成 Bean对象【依赖实体类】
多行结果处理器: 查询结果不止一行
- **ArrayListHandler **把多行结果转 二维数组 List<Object[]>
- **MapListHandler ** 把多行结果 转成 List< Map<String,Object> >
- **BeanListHandler **把多行结果 转成 List
单个值查询
- **ScalarHandler **查询单个值
9.2.1 执行DML语句
private static void testDML() throws SQLException {
//获得连接池
DataSource dataSource = JdbcUtil.getDataSource();
//1 创建一个SQL执行器
QueryRunner runner = new QueryRunner(dataSource);
//2. 编写SQL 语句
String sql = "insert into tb_singer(sname,fen,ranks) values(?,?,?)";
//3. 执行SQL
int row = runner.update(sql, "赵梦非", 11, 1000);
//4. 输出
System.out.println(row);
}
可以明显看到,这个工具对应DML的封装,跟咱们自己封装的一样。没有什么特点
9.2.2 执行DQL语句
/**测试查询*/
public static void testDQL() throws SQLException {
//1. 获得连接池
DataSource dataSource = JdbcUtil.getDataSource();
//2. QueryRunner
QueryRunner runner = new QueryRunner(dataSource);
//3. SQL
String sql0 = "select * from tb_singer where sno = ?";//单行查询SQL
String sql1 = "select * from tb_singer where sno > ?";// 多行查询SQL
String sql2 = "select count(*) as rs from tb_singer"; // 单值查询SQL
System.out.println("-----------------单行查询 转换器 3 个 ----------------");
//4. 执行(并转换结果为数组)
Object[] rowData1= runner.query(sql0, new ArrayHandler(),1);
System.out.println(Arrays.toString(rowData1));
//4. 执行(并转换结果为Map)
Map<String, Object> rowData2 = runner.query(sql0, new MapHandler(), 1);
System.out.println(rowData2);
// 4. 执行(并转换结果为Bean)
Singer rowBean = runner.query(sql0, new BeanHandler<>(Singer.class), 1);
System.out.println(rowBean);
System.out.println("-----------------多行查询 转换器 3 个----------------");
List<Object[]> data1 = runner.query(sql1, new ArrayListHandler(), 2);
data1.forEach( e->{
System.out.println(Arrays.toString(e));
});
List<Map<String, Object>> data2 = runner.query(sql1, new MapListHandler(), 2);
data2.forEach( e->{
System.out.println(e);
});
List<Singer> data3 = runner.query(sql1, new BeanListHandler<>(Singer.class), 2);
data3.forEach( e->{
System.out.println(e);
} );
System.out.println("-----------------单值查询 转换器 1 个----------------");
Long xx = runner.query(sql2, new ScalarHandler<Long>());
System.out.println(xx);
}
可以看出 该工具对应查询的封装,使用的是接口回调 ,并且提供了各种接口实现类,很方便把查询结果转换成不同的数据结构。这就是这个工具优秀的地方。它有点在于查询。
MYSQL 官方 示例数据库
https://dev.mysql.com/doc/index-other.html