目录
3.7.基于preparedStatement方式演示增删改查
一.前言
1.课程需要哪些软件、jar包、前置技术?
2.为什么学习JDBC技术?
二.全新的JDBC技术概述
2.1.jdbc概念总结
1.jdbc是(Java Database Connectivity)单词的缩写,翻译为java连接数据库
2.jdbc是java程序连接数据库的技术统称
3.jdbc由java语言的规范(接口)和各个数据库厂商的实现驱动(jar)组成
4.jdbc是一种典型的面向接口编程
5.jdbc的优势:
a.只需要学习jdbc规范接口的方法,即可操作所有的数据库软件
b.项目中期切换数据库软件,只需要更换对应的数据库驱动jar包,不需要更改代码
2.2.jdbc核心api和使用路线
jdbc技术组成
1.jdk下jdbc规范接口, 存储在java.sql和javax.sql包中的api
为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种DBMS软件,Java代码可以保持一致性。
2.各个数据库厂商提供的驱动jar包
因为各个数据库厂商的DBMS软件各有不同,那么内部如何通过sql实现增、删、改、查等管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。
3.jar包是什么?
java程序打成的一种压缩包格式,你可以将这些jar包引入你的项目中,然后你可以使用这个java程序中类和方法以及属性了!
2.3.mysql软件介绍
1.mysql软件知名版本迭代时间
2.mysql 8.x版本数据库性能提升介绍
三.全新JDBC核心API
3.1.引入mysql-jdbc驱动jar
1.驱动jar的版本选择
2.java工程导入依赖
第一步:创建一个普通的java工程(注意:不是maven工程)
idea怎样创建一个java项目?_二十一世紀難民的博客-优快云博客
第二步:准备jar包
jdbc的源码包下载地址:
MySQL :: Download Connector/J/
jdbc的jar包下载地址:
https://mvnrepository.com/artifact/mysql/mysql-connector-java
第三步:将jdbc的jar包导入当前项目
1.1.创建lib目录,并将jdbc的jar包复制到lib目录下,之后按照如下操作:
1.2.确认
1.3.添加成功
3.2.jdbc基本使用步骤(6步)
1.注册驱动
2.获取连接
3.创建statement对象
4.1.准备SQL语句
4.2.发送SQL语句并获取返回结果
5.进行结果集解析
6.关闭资源
3.3.基于statement的案例展示:
package jdbc.statement;
import com.mysql.jdbc.Driver;
import java.sql.*;
public class one {
public static void main(String[] args) throws SQLException {
/*1.注册驱动
依赖选择:
如果驱动版本8+,就导入com.mysql.cj.jdbc.Driver这个包
如果驱动版本5+,就导入com.mysql.jdbc.Driver这个包
*/
DriverManager.registerDriver(new Driver());
/*2.获取连接
java程序要和数据库建立连接
java程序,连接数据库时是调用的某个方法,该方法需要填入连接数据库的基本信息
数据库ip地址:127.0.0.1
数据库端口号:3306
账户:root
密码:123456
连接数据库的名称:crm
*/
//java.sql 接口 = 实现类
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
//3.创建statement对象
Statement statement = connection.createStatement();
//4.1.准备SQL语句
String sql = "select * from user";
//4.2.发送SQL语句并获取返回结果
ResultSet resultSet = statement.executeQuery(sql);
//5.进行结果集解析
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String name = resultSet.getString("name");
System.out.println("id:"+id+"\t用户名:"+username+"\t昵称:"+name);
}
//6.关闭资源
resultSet.close();
statement.close();
connection.close();
}
}
测试结果:
【附】数据库字段信息:
3.4.JDBC核心API的参数详解:
代码案例展示:
package jdbc.statement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
import java.util.Scanner;
/**
* TODO:
* 输入账号和密码,进入数据查询信息(t_user),反馈登录成功还是登录失败
*
* TODO:
* 1.键盘输入事件,收集账号和密码信息
* 2.注册驱动
* 3.获取连接
* 4.创建statement
* 5.发送查询SQL语句,并获取返回结果
* 6.结果判断,显示登录成功还是失败
* 7.关闭资源
*/
public class two {
public static void main(String[] args) throws Exception {
//1.键盘输入事件,收集账号和密码信息
Scanner scanner = new Scanner(System.in);
System.out.println("请输入账户名:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
//2.注册驱动
/**
* 方案一弊端详解:
* DriverManager.registerDriver(new Driver())----这句代码会注册两次驱动:
* DriverManager.registerDriver()----这是静态方法,会注册一次驱动;
* Driver.static{ DriverManager.registerDriver() }----又注册一次驱动;
* 开启两次驱动会带来JVM性能的消耗,需要想办法解决----即只注册一次驱动,只触发一次静态代码块
* 触发静态代码块:
* 类加载机制:即类加载大的时刻,会触发静态代码块!
* 类的三个时机:1.加载【class文件被加载,成为JVM的class对象】
* 2.连接【验证(检查文件类型)->准备(静态变量默认值)->解析(触发静态代码块)】
* 3.初始化(静态属性赋真实值)
* 如何触发类的加载?
* 1.new 关键字
* 2.调用静态方法
* 3.调用静态属性
* 4.接口
* 5.反射
* 6.子类触发父类
* 7.程序的入口main
*
*
*/
//方案一:淘汰
//DriverManager.registerDriver(new Driver());
//方案二:反射
//当前驱动版本为5.0.8,写法如下:
Class.forName("com.mysql.jdbc.Driver");
//如果选择的驱动版本8+,那么就该写为:
//Class.forName("com.mysql.jdbc.cj.Driver");
//3.获取连接(3种方法)
/**
* 核心参数:
* 1.数据库软件所在的主机的ip地址:127.0.0.1或localhost
* 2.数据库软件所在的主机的端口号:3306
* 3.连接的具体数据库库名:crm
* 4.连接的账号:root
* 5.连接的密码:123456
* 6.可选信息:无
*
* 参数:String url 数据库软件所在的信息,连接具体的数据库,以及其他可选信息!
* 语法:jdbc:数据库管理软件名称[mysql或oracle]://ip地址或主机名:port端口号/数据库名?key=value
* & key=value & key=value 可选信息!
* 具体案例:
* 案例一:jdbc:mysql://127.0.0.1:3306/crm
* 案例二:jdbc:mysql://localhost:3306/crm
* 案例三:jdbc:oracle://localhost:3306/crm
* 如果数据库在本机且端口号为3306,那么可以简写:
* jdbc:mysql://127.0.0.1:3306/crm 简写为 jdbc:mysql:///crm
*/
//获取连接方法一:
Connection connection1 =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
//获取连接方法二:
/**
* Properties info:存储账号和密码
* Properties类似于Map,只不过key和value都是字符串形式
*/
Properties info = new Properties();
info.put("user","root");
info.put("password","123456");
Connection connection2 = DriverManager.getConnection("jdbc:mysql://localhost:3306/crm",info);
//获取连接方法三:
/**
* String url --> jdbc:数据库软件名://ip:port/具体数据库库名?key=value & key=value 可选信息
* 案例展示:
* jdbc:mysql://127.0.0.1:3306/crm?user=root&password=123456
* url的路径属性可选信息:
* user=账号&password=密码
* 8.0.25版本以后,自动识别时区,不用添加【serverTimezone=Asia/Shanghai】
* 8.0版本以后,默认使用utf-8格式,不用添加【useUnicode=true&characterEncoding=utf8&useSSL=true】
*/
Connection connection3 =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm?user=root&password=123456" +
"&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true");
Statement statement = connection3.createStatement();
//4.发送SQL语句(1.编写Sql语句,2.发送Sql语句)
String sql = "select * from t_user where username ='"+username+"' and password ='"+password+"';";
/**
* SQL分类:DDL(容器创建、修改、删除)、DML(插入、删除、修改)、DQL(查询)、DCL(权限控制)、TPL(事务控制语言)
* statement.executeUpdate(sql)与statement.executeQuery(sql)的区别:
* statement.executeUpdate(sql)--适用于非DQL语句,返回值为受影响的行数
* statement.executeQuery(sql)--适用于DQL语句,返回值为结果集,结果集中封装了所有符合查询条件的对象
*/
//发送sql语句
//int i = statement.executeUpdate(sql);
//发送sql语句
ResultSet resultSet = statement.executeQuery(sql);
//6.结果判断,显示登录成功还是失败,只要有数据存在,就证明密码和账号正确
if(resultSet.next()){
System.out.println("登录成功");
}else {
System.out.println("登录失败");
}
//7.关闭资源
resultSet.close();
statement.close();
connection3.close();
connection2.close();
connection1.close();
}
}
数据库字段与数据展示:
测试结果:
3.5.基于statement方式的问题:
1.案例演示:
在3.4的案例中输入一个表中不存在的账户:
sql语句变成了这样:
表中不存在的账户也能登录,这就是statement的sql注入问题。
2.statement方式存在的问题:
1.SQL语句需要字符串拼接,比较麻烦
2.只能拼接字符串类型,其他的数据库类型无法处理
3.可能发生注入攻击----动态值充当了SQL语句结构,影响了原有的查询结果。
3.如何解决statement方式的问题?
用preparedStatement方式优化。
3.6.基于preparedStatement方式优化
用preparedStatement方式优化statement方式存在的问题。
代码案例:
package jdbc.preparedStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Scanner;
/**
* TODO:
* 1.使用preparedStatement演示防止注入攻击
* 2.演示preparedStatement的使用流程
*/
public class one {
public static void main(String[] args) throws Exception {
//1.键盘输入事件,收集账号和密码信息
Scanner scanner = new Scanner(System.in);
System.out.println("请输入账户名:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
//2.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//3.获取连接
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm?user=root&password=123456" +
"&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true");
/**
* TODO:Statement方式与prepareStatement方式比较
* Statement方式
* 1.创建statement
* 2.拼接SQL语句
* 3.发送SQL语句,并且获取返回结果
* prepareStatement方式
* 1.编写SQL语句结果,SQL语句中不包含动态值部分,动态值部分用占位符"?"代替
* 2.创建prepareStatement,并且传入动态值
* 3.给占位符"?"赋值
* 4.发送SQL语句即可,并获取返回结果
*
*/
//4.编写SQL语句并用prepareStatement发送SQL
//4.1编写SQL语句结果
String sql = "select * from t_user where username = ? and password = ?";
//4.2.创建预编译prepareStatement并设置SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//4.3.给占位符"?"赋值
/**
* TODO:参数的意义
* 参数1:parameterIndex---->给SQL语句中的第几个占位符赋值,下标值是从1开始的
* 参数2:object---->给SQL语句中的第几个占位符具体赋什么值
*/
preparedStatement.setObject(1,username);
preparedStatement.setObject(2,password);
//4.4.发送SQL语句即可,并获取返回结果
/**
* preparedStatement与statement的区别在于,前者不用再传入sql语句参数了
* preparedStatement.executeQuery();
* statement.executeQuery(sql);
*/
ResultSet resultSet = preparedStatement.executeQuery();
//5.结果判断,显示登录成功还是失败,只要有数据存在,就证明密码和账号正确
if(resultSet.next()){
System.out.println("登录成功");
}else {
System.out.println("登录失败");
}
//7.关闭资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
测试结果:
5
3.7.基于preparedStatement方式演示增删改查
增删改
package jdbc.preparedStatement;
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class PScrud {
@Test
public void PSinsert() throws Exception {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "INSERT INTO t_user (id,username,`password`) VALUES (?,?,?)";
//4.创建preparedStatement,并传入SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.给占位符赋值
preparedStatement.setObject(1,5);
preparedStatement.setObject(2,"长江");
preparedStatement.setObject(3,123456);
//6.发送SQL语句,i代表受影响的行数
int i = preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("添加成功");
}else {
System.out.println("添加失败");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
@Test
public void PSdelete() throws Exception{
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "DELETE FROM t_user WHERE id = ?";
//4.创建preparedStatement,并传入SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.给占位符赋值
preparedStatement.setObject(1,5);
//6.发送SQL语句,i代表受影响的行数
int i = preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("删除成功");
}else {
System.out.println("删除失败");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
@Test
public void PSupdate() throws Exception{
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "UPDATE t_user SET username = ? WHERE id = ? ";
//4.创建preparedStatement,并传入SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.给占位符赋值
preparedStatement.setObject(1,"山海经");
preparedStatement.setObject(2,3);
//6.发送SQL语句,i代表受影响的行数
int i = preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("修改成功");
}else {
System.out.println("修改失败");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
}
查询案例:
package jdbc.preparedStatement;
import org.junit.Test;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class PScrud {
//查询数据并将查询到的数据先存到Map中,再将Map添加到集合中
@Test
public void PSselect() throws Exception {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "select * from t_user";
//4.创建preparedStatement,并传入SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.发送SQL并获取查询到的数据结果集
ResultSet resultSet = preparedStatement.executeQuery();
//获取返回的结果集中列的信息对象
ResultSetMetaData metaData = resultSet.getMetaData();
//得到返回结果集有多少列----即数据库中一行完整的数据拥有的字段个数
int columnCount = metaData.getColumnCount();
ArrayList<Map> list = new ArrayList<>();
while (resultSet.next()){
HashMap map = new HashMap();
//自动遍历结果集的列数,注意要从1开始且小于等于总列数
for (int i = 1;i <= columnCount;i++){
//获取指定列下角标的值!
Object value = resultSet.getObject(i);
//获取指定列下角标的列的名称
String columnLabel = metaData.getColumnLabel(i);
map.put(columnLabel,value);
}
//一行完整数据的所有内容均被存到了map中
//将map存储到集合中即可
list.add(map);
}
System.out.println("list="+list);
//7.关闭资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
测试结果:
3.8.preparedStatement使用方法总结
1.使用步骤总结
//1.注册驱动
//2.获取连接
//3.编写SQL语句
//4.创建preparedStatement对象并传入SQL语句结构
//5.占位符赋值
//6.发送查询SQL语句,并获取返回结果
//7.解析返回的结果集
//8.关闭资源
2.使用API总结
//1.注册驱动
方案1:调用静态方法,但是会注册2次驱动,降低系统性能
DriverManager.registerDriver(new Driver());
方案2:反射触发(推荐)
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection =DriverManager.getConnection();
3个参数:public static Connection getConnection(String url,String user, String password)
2个参数:public static Connection getConnection(String url,java.util.Properties info)
1个参数:public static Connection getConnection(String url)
//3.编写SQL语句
//4.创建preparedStatement对象并传入SQL语句结构
静态:
Statement statement = connection.createStatement();
预编译:
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.发送查询SQL语句,并获取返回结果
int i = preparedStatement.executeUpdate();//处理非DQL语句,i代表受影响的行数
ResultSet resultSet = preparedStatement.executeQuery();//处理DQL语句
//6.解析返回的结果集
while (resultSet.next()){}
//7.关闭资源
close();
四.全新的JDBC扩展提升
4.1.自增主键Id的回显实现
作用:在多表关联插入数据的业务场景下,一般主表的主键都是自动生成的,所以在插入数据之前无法知道这条数据的主键值,但是从表需要在插入数据之前就绑定主表的主键,这时可以使用主键回显技术。
案例展示:
package jdbc.preparedStatement;
import org.junit.Test;
import java.sql.*;
public class PSInsertReturnId {
/**
* TODO:向t_user表中插入一条数据,并返回自增主键Id的值
*
* TODO:使用总结:
* 1.创建preparedStatement对象的时候,告知preparedStatement对象携带回数据库自增的主键Id
* PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
* 2.获取PreparedStatement对象装载主键值的结果集对象,一行一列,获取对应的数据即可
* ResultSet resultSet = preparedStatement.getGeneratedKeys();
* @throws Exception
*/
@Test
public void PSselect() throws Exception {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "INSERT INTO t_user (username,`password`) VALUES (?,?)";
//4.创建preparedStatement,并传入SQL语句结果,同时要求preparedStatement对象将本次插入的数据对应的Id的值带回来
PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
//5.给占位符赋值
preparedStatement.setObject(1,"长江黄河");
preparedStatement.setObject(2,123456);
//6.发送SQL语句并返回结果,i代表受影响的行数
int i = preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("添加成功");
//可以获取回显的主键
//获取preparedStatement对象装载的结果集对象,结果集对象的结构为一行一列
ResultSet resultSet = preparedStatement.getGeneratedKeys();
resultSet.next();//移动指针到有效数据位置
int id = resultSet.getInt(1);
System.out.println("本次插入的数据在表中对应的Id值为:"+id);
}else {
System.out.println("添加失败");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
}
测试结果:
4.2.批量数据插入性能提升
代码案例:
@Test
public void PSInsertAll() throws Exception {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection(
"jdbc:mysql:///crm?rewriteBatchedStatements=true","root","123456");
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "INSERT INTO t_user (username,`password`) VALUES (?,?)";
//4.创建preparedStatement,并传入SQL语句结果,同时要求preparedStatement对象将本次插入的数据对应的Id的值带回来
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.给占位符赋值
for (int j = 0;j < 20;j++){
preparedStatement.setObject(1,"长江黄河"+j);
preparedStatement.setObject(2,123+j);
/**
* 暂时先不执行,将值追加到values后面,此时sql为:
* INSERT INTO t_user (username,`password`) VALUES (?,?),(?,?),(?,?),(?,?),(?,?)......
*/
preparedStatement.addBatch();
}
//6.发送SQL语句,执行批量(插入)操作并返回结果,ints代表受影响的行数
int[] ints = preparedStatement.executeBatch();
//7.输出结果
if(ints.length>0){
System.out.println("添加成功,本次添加的数据为:"+ints.length+"条");
}else {
System.out.println("添加失败");
}
//8.关闭资源
preparedStatement.close();
connection.close();
}
测试结果:
4.3.jdbc中数据库事务的实现
案例展示:
必须保证数据库的存储引擎为InnoDB,因为InnoDB是支持事务的。
一个事务的最基本要求,必须是处在同一个Connection(连接对象)。
package jdbc.preparedStatement;
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
class transactionDao{
public void addMoney(Double money,String username,Connection connection) throws Exception {
String sql = "UPDATE t_user set money = money + ? where username = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,username);
//int a = 2/0;
preparedStatement.executeUpdate();
preparedStatement.close();
System.out.println("加钱成功");
}
public void reduceMoney(Double money,String username,Connection connection) throws Exception {
String sql = "UPDATE t_user set money = money - ? where username = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,username);
preparedStatement.executeUpdate();
preparedStatement.close();
System.out.println("扣钱成功");
//int a = 2/0;
}
}
public class transactionTest {
@Test
public void zhuanzhangTest() throws Exception {
Double money = new Double(50);
zhuanzhang("ls","zs",money);
}
public void zhuanzhang(String addAccount,String reduceAccount,Double money) throws Exception {
transactionDao dao = new transactionDao();
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection=DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/crm","root","123456");
try{
connection.setAutoCommit(false);
dao.addMoney(money,addAccount,connection);
System.out.println("====================");
dao.reduceMoney(money,reduceAccount,connection);
connection.commit();
System.out.println("转账成功");
}catch (Exception e){
System.out.println("转账失败");
e.printStackTrace();
//回滚数据
try {
connection.rollback();
System.out.println("事务回滚成功");
} catch (SQLException e1) {
System.out.println("事务回滚失败");
e1.printStackTrace();
}
}finally {
connection.close();
}
}
}
测试结果:
五.数据库连接池技术----Druid
5.1.连接性能消耗问题分析
连接的生命周期:创建连接、使用连接、销毁连接。如果创建连接花费时间和销毁连接的时间远大于使用连接的时间,这个程序的效率就极低。改进方案:数据库连接池技术----Druid。
5.2.数据库连接池的作用
总结缺点:
(1)不使用数据库连接池,每次都通过DriverManager获取新连接,用完直接抛弃断开,
连接的利用率太低,太浪费。
(2)对于数据库服务器来说,压力太大了。我们数据库服务器和Java程序对连接数也无法控制
,很容易导致数据库服务器崩溃。
我们就希望能管理连接。
- 我们可以建立一个连接池,这个池中可以容纳一定数量的连接对象,一开始,
我们可以先替用户先创建好一些连接对象,等用户要拿连接对象时,就直接从池中拿,
不用新建了,这样也可以节省时间。然后用户用完后,放回去,别人可以接着用。
- 可以提高连接的使用率。当池中的现有的连接都用完了,那么连接池可以向服务器申
请新的连接放到池中。
- 直到池中的连接达到“最大连接数”,就不能在申请新的连接了,如果没有拿到连接的用户只能等待。
5.3.市面常见的连接池产品对比
JDBC 的数据库连接池使用 javax.sql.DataSource接口进行规范,所有的第三方连接池
都实现此接口,自行添加具体实现!也就是说,所有连接池获取连接的和回收
连接方法都一样,不同的只有性能和扩展功能!
- DBCP 是Apache提供的数据库连接池,速度相对c3p0较快,但因自身存在BUG
- C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以
- Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,
稳定性较c3p0差一点
- Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身
的数据库连接池,妥妥国货之光!!!!
参考链接:
5.4.Druid的使用
1.导入Druid的jar包。
2.Druid的2种使用方式
2.1.硬编码方式
代码展示:
/**
* 创建druid连接池对象,使用硬编码进行核心参数设置!
* 必须参数: 账号
* 密码
* url
* driverClass
* 非必须参数:
* 初始化个数
* 最大数量等等 不推荐设置
* TODO:直接使用代码设置连接池的连接参数方式
* * 1.创建一个druid连接池对象
* * 2.设置连接池参数
* * 参数分为必须参数和非必循参数
* * 3.获取连接[这是通用方法,所有连接池都一样]
* * 4.回收连接[这是通用方法,所有连接池都一样]
*/
@Test
public void druidHard() throws SQLException {
//连接池对象
DruidDataSource dataSource = new DruidDataSource();
//设置四个必须参数:1.注册驱动;2.url地址;3.user;4.密码
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/crm");
dataSource.setUsername("root");
dataSource.setPassword("123456");
//设置非必须参数
dataSource.setInitialSize(5);//初始化连接数
dataSource.setMaxActive(10);//最大的数量
//获取连接
Connection connection = dataSource.getConnection();
// 数据库CRUD
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "INSERT INTO t_user (username,`password`,money) VALUES (?,?,?)";
//4.创建preparedStatement,并传入SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.给占位符赋值
preparedStatement.setObject(1,"长江");
preparedStatement.setObject(2,123456);
preparedStatement.setObject(3,500.0);
//6.发送SQL语句,i代表受影响的行数
int i = preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("添加成功");
}else {
System.out.println("添加失败");
}
//连接池提供的连接,此时close()方法是回收连接的意思
connection.close();
}
测试结果:
2.2.软编码方式
代码案例
1.配置文件内容:
2.代码
package jdbc.druid;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.junit.Test;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
public class DruidDemo {
/**
* 不直接在java代码编写配置文件!
* 利用工厂模式,传入配置文件对象,创建连接池!
*
* @throws Exception
*/
@Test
public void druidSoft() throws Exception {
//1.读取外部配置文件Properties
Properties properties = new Properties();
//2.src下的文件可以用类加载器的方法加载,要填写配置文件的相对于src目录的相对路径
InputStream ips = DruidDemo.class.getClassLoader().getResourceAsStream("jdbc/druid/druid.properties");
properties.load(ips);
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
Connection connection = dataSource.getConnection();
// 数据库CRUD
//3.编写SQL语句结果,动态值用"?"号代替
String sql = "INSERT INTO t_user (username,`password`,money) VALUES (?,?,?)";
//4.创建preparedStatement,并传入SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.给占位符赋值
preparedStatement.setObject(1,"长江与黄河");
preparedStatement.setObject(2,123456);
preparedStatement.setObject(3,1000.0);
//6.发送SQL语句,i代表受影响的行数
int i = preparedStatement.executeUpdate();
//7.输出结果
if(i>0){
System.out.println("添加成功");
}else {
System.out.println("添加失败");
}
preparedStatement.close();
connection.close();
}
}
项目架构:
测试结果:
六.JDBC优化
6.1.jdbc工具类封装1.0
无法保证事务的管理的方式:
package jdbc.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* jdbc连接池工具类中包含一个连接池对象,并且对外提供获取连接和回收连接的方法
*
* 建议:
* 工具类的方法,携程静态方法,外部方便调用
*
* 实现:
* 属性:连接池对象(实例化一次)
* 方法:
* 1.对外提供连接的方法
* 2.回收外部传入连接方法
*/
public class jdbcUtils {
//准备连接池对象
private static DataSource ds;
//静态代码块
static{
try {
//初始化连接池对象
Properties properties = new Properties();
InputStream ips =
jdbcUtils.class.getClassLoader().getResourceAsStream("jdbc/druid/druid.properties");
properties.load(ips);
ds = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
//这么写,不能保证同一个线程,两次getConnection()得到的是同一个Connection对象
//如果不能保证是同一个连接对象,就无法保证事务的管理
return ds.getConnection();
}
public static void free(Connection conn) throws SQLException {
conn.close();//还给连接池
}
}
1
6.2.jdbc工具类封装2.0
能保证事务的管理的方式:
这个工具类的作用就是用来给所有的SQL操作提供“连接”,和释放连接。 这里使用ThreadLocal的目的是为了让同一个线程,在多个地方getConnection得到的是同一个连接。 这里使用DataSource的目的是为了(1)限制服务器的连接的上限(2)连接的重用性等
工具类:
package jdbc.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* jdbc连接池工具类中包含一个连接池对象,并且对外提供获取连接和回收连接的方法
*
* 建议:
* 工具类的方法,携程静态方法,外部方便调用
*
* 实现:
* 属性:连接池对象(实例化一次)
* 方法:
* 1.对外提供连接的方法
* 2.回收外部传入连接方法
*
*TODO:
* 利用线程本地变量,存储连接对对象(Connection对象)信息!确保一个线程中的多个方法可以获取同一个Connection对象
* 优势:
* 事务操作的时候,service和dao属于同一个线程中,不用再传递Connection对象了
* 各个方法间可以调用getConnection自动获取的是同一个连接池
*/
public class jdbcUtils {
//准备连接池对象
private static DataSource dataSource = null;
//创建线程本地变量:
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
//静态代码块
static{
try {
//初始化连接池对象
Properties properties = new Properties();
InputStream ips =
jdbcUtils.class.getClassLoader().getResourceAsStream("jdbc/druid/druid.properties");
properties.load(ips);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 这是工具类对外提供的获取连接的方法
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
//校验线程本地变量中是否存在Connection
Connection connection = tl.get();
//第一次线程本地变量中是没有Connection对象的
if(connection == null){
connection = dataSource.getConnection();
System.out.println(connection);
//将connection存入线程本地变量
tl.set(connection);
}
return connection;
}
/**
* 这是工具类对外提供的回收连接的方法
* @throws SQLException
*/
public static void free() throws SQLException {
Connection connection = tl.get();
if(connection != null){
//清空线程本地数据变量
tl.remove();
//事务状态回顾
/**
* connection.setAutoCommit(false)代表将自动提交事务改为手动提交事务----即开启手动提交事务
* 此处写connection.setAutoCommit(true)的目的在于:
* 如果当前线程A获取的本次连接在执行SQL语句时开启了手动提交事务,如不将状态值回归为true,
* 那么这个连接归还给连接池后,下一个拿到这个连接的线程B,拿到的会是一个需要手动提交事务
* 的连接,但是线程B中的业务有可能并不需要手动提交事务,因此要将状态值回归为true
*/
connection.setAutoCommit(true);
//将连接回收到连接池
connection.close();
}
}
}
配置文件
# properties文件里面的内容是以"key=value"这种形式的键值对格式存在
# druid连接池需要的配置参数,key固定命名
driverClassName=com.mysql.jdbc.Driver
username=root
password=123456
url=jdbc:mysql://127.0.0.1:3306/crm
initialSize=5
用转账案例测试jdbc连接池工具类代码
package jdbc.utils;
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
class transactionDao{
/**
* 给账户加钱的方法
* @param money
* @param username
* @throws Exception
*/
public void addMoney(Double money,String username) throws Exception {
Connection connection = jdbcUtils.getConnection();
String sql = "UPDATE t_user set money = money + ? where username = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,username);
//int a = 2/0;
preparedStatement.executeUpdate();
preparedStatement.close();
System.out.println("加钱成功");
System.out.println(connection);
}
/**
* 给账户扣钱的方法
*
* @param money
* @param username
* @throws Exception
*/
public void reduceMoney(Double money,String username) throws Exception {
Connection connection = jdbcUtils.getConnection();
String sql = "UPDATE t_user set money = money - ? where username = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,money);
preparedStatement.setObject(2,username);
preparedStatement.executeUpdate();
preparedStatement.close();
System.out.println("扣钱成功");
//int a = 2/0;
}
}
public class transactionTest {
@Test
public void zhuanzhangTest() throws Exception {
Double money = new Double(50);
zhuanzhang("ls","zs",money);
}
/**
* TODO:一个事务的最基本要求,必须是处在同一个Connection(连接对象)
*
* TODO:事务总结:
* 1.添加事务的地方是在业务方法中
* 2.在try代码块中开启事务和提交事务,在catch代码块中回滚事务
* 3.将connection传入dao层即可!dao层只负责使用,不要做connection.close()处理
*
* @param addAccount
* @param reduceAccount
* @param money
* @throws Exception
*/
public void zhuanzhang(String addAccount,String reduceAccount,Double money) throws Exception {
transactionDao dao = new transactionDao();
Connection connection = jdbcUtils.getConnection();
try{
//将事务从自动提交改为手动提交----即开启手动提交事务
//一个事务的最基本要求,必须是处在同一个Connection(连接对象)
connection.setAutoCommit(false);
//执行数据库动作
dao.addMoney(money,addAccount);
System.out.println("====================");
dao.reduceMoney(money,reduceAccount);
//事务提交
connection.commit();
System.out.println("转账成功");
}catch (Exception e){
System.out.println("转账失败");
e.printStackTrace();
try {
//事务回滚
connection.rollback();
System.out.println("事务回滚成功");
} catch (SQLException e1) {
System.out.println("事务回滚失败");
e1.printStackTrace();
}
}finally {
jdbcUtils.free();
}
}
}
5测试
6.3.Dao层封装
package jdbc.utils;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* TODO:封装dao层数据重复代码,封装2个方法
* 1.简化非DQL语句的方法
* 2.简化DQL语句的方法
*/
public abstract class BaseDao {
/**
* 简化非DQL语句的方法
*
* @param sql 带占位符的SQL语句
* @param parms 占位符赋值
* @return 执行SQL语句影响的行数
* @throws SQLException
*/
public int executUpdate(String sql, Object... parms) throws SQLException {
//获取连接
Connection connection = jdbcUtils.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值
//可变参数可以当作数组使用
for (int i = 1; i <= parms.length; i++) {
preparedStatement.setObject(i, parms[i - 1]);
}
//发送SQL语句----DML类型
int rows = preparedStatement.executeUpdate();
preparedStatement.close();
//是否回收连接,需要考虑当前连接对象有没有开启手动提交事务
if (connection.getAutoCommit()) {
//没有开启手动提交事务,正常回收连接
jdbcUtils.free();
//如果开启了手动提交事务,那么这里不做处理,在业务层去处理事务
}
return rows;
}
/**
* 简化DQL语句的方法
*
* @param clazz:要接收值的实体类集合的模板对象
* @param sql:sql查询语句,要求列名或者别名等于实体类的属性名
* @param parms:占位符的值
* @param <T>:声明结果的类型!
* @return :查询的实体类集合
* @throws SQLException
* @throws IllegalAccessException
* @throws InstantiationException
* @throws NoSuchFieldException
*
* <T>在不确定类型的情况下,可以声明一个泛型
* 1.确定泛型:User.class T = User;
* 2.要使用反射技术赋值
* public <T> List<T> executeQuery(Class<T> clazz, String sql, Object... parms)
*/
public <T> List<T> executeQuery(Class<T> clazz, String sql, Object... parms) throws SQLException, IllegalAccessException, InstantiationException, NoSuchFieldException {
Connection connection = jdbcUtils.getConnection();
//创建preparedStatement,并传入SQL语句结果
PreparedStatement preparedStatement = connection.prepareStatement(sql);
if (parms != null && parms.length != 0) {
//占位符赋值
//可变参数可以当作数组使用
for (int i = 1; i <= parms.length; i++) {
preparedStatement.setObject(i, parms[i - 1]);
}
}
//发送SQL并获取查询到的数据结果集
ResultSet resultSet = preparedStatement.executeQuery();
//获取返回的结果集中列的信息对象
ResultSetMetaData metaData = resultSet.getMetaData();
//得到返回结果集有多少列----即数据库中一行完整的数据拥有的字段个数
int columnCount = metaData.getColumnCount();
List<T> list = new ArrayList<>();
while (resultSet.next()) {
//调用类的无参构造函数实例化对象
T t = clazz.newInstance();
for (int i = 1; i <= columnCount; i++) {
//获取指定列下角标的值!
Object value = resultSet.getObject(i);
//获取指定列下角标的列的名称
String columnLabel = metaData.getColumnLabel(i);
//反射,给对象的属性值赋值
Field field = clazz.getDeclaredField(columnLabel);
//属性设置为true,就可以用反射打破private的修饰限制
field.setAccessible(true);
/**
* 赋值:
* 参数一:要赋值的对象,如果属性为静态的,可以为null
* 参数二:具体的属性值
*/
field.set(t, value);
}
list.add(t);
}
//关闭资源
resultSet.close();
preparedStatement.close();
//连接中没有事务,可以关闭连接
if(connection.getAutoCommit()){
jdbcUtils.free();
}
return list;
}
}
工具类优化前后比较
【补充】请问List<Map>与List<T>有什么区别?
List<Map>:
1.Map中的key和value是自定义的,不用预先设计好
2.没有数据校验机制
3.不支持反射操作
List<T>:形如List<User>
1.User中的数据类型和变量名要预先设计好,且必须和数据库中的字段一一对应
2.有数据校验机制
3.支持反射操作
总结: