引言
why?
我们在开发Java程序时,数据都是存储在内存中,属于临时存储,当程序停止或重启时,内存中的数据就丢失了!我们为了解决数据的长期存储问题,有如下解决方案:
-
数据通过I/O流技术,存储在本地磁盘中,解决了持久化问题,但是没有结构和逻辑,不方便管理和维护。
-
通过关系型数据库,将数据按照特定的格式交由数据库管理系统维护关系型数据库是通过库和表分隔不同的数据,表中数据存储的方式是行和列,区分相同格式不同值的数据。
what?
-
JDBC:Java Database Connectivity,意为Java数据库连接。
-
JDBC是Java提供的一组独立于任何数据库管理系统的API。
-
Java提供接口规范,由各个数据库厂商提供接口的实现,厂商提供的实现类封装成jar文件,也就是我们俗称的数据库驱动jar包。
-
学习JDBC,充分体现了面向接口编程的好处,程序员只关心标准和规范,而无需关注实现过程。
JDBC简单执行过程
JDBC的核心组成
-
接口规范:
-
为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种D8MS软件,Java代码可以保持一致性。
-
接口存储在java.sql和javax.sql包下。
-
-
实现规范:
-
因为各个数据库厂商的DBMS软件各有不同,那么各自的内部如何通过SQL实现增、脚、改、查等操作管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。
-
厂商将实现内容和过程封装成jar文件,我们程序员只需要将jar文件引入到项目中集成即可,就可以开发调用实现过程操作数据库了。
-
搭建
JDBC搭建步骤
-
准备数据库
-
官网下载数据库连接驱动jar包:
mysql-connector-j-8.0.33.jar
-
创建java项目,在项目下创建lib文件夹,将下载的驱动jar包复制到文件夹里
-
选中lib文件夹右键->Add as Library,与项目集成
-
编写代码
代码实现
首先准备数据库的数据:
create database jdbc_tt;
use jdbc_tt;
create table t_emp(
emp_id int auto_increment comment '员工编号' primary key ,
emp_name varchar(100) not null comment '姓名',
emp_salary double(10,5) not null comment '薪资',
emp_age int not null comment '年龄'
) comment '员工表';
insert into t_emp(emp_name, emp_salary, emp_age)
values ('andy',777.77,32),
('TayTay',666.66,41),
('katy',111,23),
('Pink!',123,26),
('Lisa',123,28);
-
代码实现
public class JDBCQuick { public static void main(String[] args) throws Exception { //1.注册驱动 Class.forName("com.mysql.jdbc.Driver");//可省略 //2.获取连接对象 String url = "jdbc:mysql://localhost:3306/jdbc_tt"; String user = "root"; String password = "123456"; Connection connection = DriverManager.getConnection(url, user, password); //3.获取执行SQL语句的对象 Statement statement = connection.createStatement(); //4.编写SQL语句并执行,以及接收结果集 String sql = "Select * from t_emp"; ResultSet resultSet = statement.executeQuery(sql);//resultset:即包含行和列的结果的集合 //5.处理结果,遍历结果集 while (resultSet.next()) { int empId = resultSet.getInt("emp_id"); String empName = resultSet.getString("emp_name"); double empSalary = resultSet.getDouble("emp_salary"); int empAge = resultSet.getInt("emp_age"); System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge); } //6.资源释放 resultSet.close(); statement.close(); connection.close(); } }
核心API理解
注册驱动
Class.forName("com.mysql.jdbc.Driver");//可省略
-
在Java中,当使用JDBC(Java Database Connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得JDBC API能够识别并与特定的数据库进行交互。
-
从JDK6开始,不再需要显式地调用
Class.forName()
来加载JDBc驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序。
Cennection
-
Connection接口是JDBC AP的重要接口,用于建立与数据库的通信通道。换而言之,Connection对象不为空,则代表一次数据库连接。
-
在建立连接时,需要指定数据库UL、用户名、密码参数。
-
URL:
jdbc:mysql://localhost:3306/jdbc_tt
-
jdbc:
mysql://IP地址:端口号/数据库名称?参数键值对1&参数键值对2
-
-
Connection
接口还负责管理事务,Connection
接口提供了commit
和rollback
方法,用于提交事务和回滚事务。 -
可以创建
Statement
对象,用于执行SQL语句并与数据库进行交互。 -
在使用JDBC技术时,必须要先获取
Connection
对象,在使用完毕后,要释放资源,避免资源占用浪费及泄漏。
Statement
-
Statement
接口用于执行SQL语句并与数据库进行交互。它是JDBC API中的一个重要接口。通过Statement
对象,可以向数据库发送SQL语句并获取执行结果。 -
结果可以是一个或多个结果。
-
增删改:受影响行数单个结果。
-
查询:单行单列、多行多列、单行多列等结果。
-
-
但是Statement接口在执行SQL语句时,会产生SQL注入攻击问题:
- 当使用Statement执行动态构建的SQL查询时,往往需要将查询条件与SQL语句拼接在一起,直接将参数和SQL语句一并生成,让SQL的查询条件始终为true得到结果。
PrepareStatement
-
PreparedStatement
是Statement
接口的子接口,用于执行预编译的SQL查询,作用如下:-
预编译SQL语句:在创建
PreparedStatement
时,就会预编译SQL语句,也就是SQL语句已经固定。 -
防止SQL注入:PreparedStatement支持参数化查询,将数据作为参数传递到sQL语句中,采用
?
占位符的方式,将传入的参数用一对单引号包裹起来,无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。 -
性能提升:PreparedStatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。
-
-
后续的学习我们都是基于PreparedStatementi进行实现,更安全、效率更高!
改良Statement为PrepareStatement:
package com.sql.test;
import java.sql.*;
import java.util.Scanner;
public class JDBCPrepare {
public static void main(String[] args) throws Exception{
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");//可省略
//2.获取连接对象
String url = "jdbc:mysql://localhost:3306/jdbc_tt";
String user = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, user, password);
//3.获取预编译SQL语句的对象
PreparedStatement preparedStatement = connection.prepareStatement("Select * from t_emp where emp_name = ?");
System.out.println("请输入员工姓名:");
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
//4.为?占位符赋值,并执行SQL语句并执行,以及接收结果集
preparedStatement.setString(1,name);
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果,遍历结果集
while (resultSet.next()) {
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
//6.资源释放
resultSet.close();
preparedStatement.close();
connection.close();
}
}
ResultSet
-
ResultSet
是JDBC API中的一个接口,用于表示从数据库中执行查询语句所返回的结果集。它提供了一种用于遍历和访问查询结果的方式。 -
遍历结果:ResultSet可以使用
next()
方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为booleanl类型,true代表有下一行结果,false则代表没有。 -
获取单列结果:可以通过
getXxx
的方法获取单列的数据,该方法为重载方法,支持索引和列名进行获取。
基于PrepareStatement实现CRUD
首先导入Junit工具包:
查询数据(Retrieve)
查询单行单列的数据
public class JDBCOperration {
@Test
public void tesyQuerySingleCol() throws Exception{
//1.注册驱动
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
//3.预编译
PreparedStatement preparedStatement = connection.prepareStatement("Select count(*) '员工数量' from t_emp");
//4.执行SQL
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果
while(resultSet.next()){
int count = resultSet.getInt(1);//通过列获取
//int count = resultSet.getInt("员工数量");//通过标签获取
System.out.println(count);
}
//6.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
执行结果
5
注意:即使已知数据只有一行,省略resultSet.next()
,因为它的作用是检查是否有下一行。即使只有一行,也要有一个if来判断调用。
查询单行多列的数据
public class JDBCOperration {
//查询单行多列的数据
@Test
public void tesyQuerySingleRow() throws Exception{
//1.注册驱动
//2.获取链接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
//3.预编译SQL
PreparedStatement preparedStatement = connection.prepareStatement("select * from t_emp where emp_id = ?");
//4.为占位符赋值并获取结果集
preparedStatement.setInt(1,4);
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
int emp_id = resultSet.getInt("emp_id");
String emp_name = resultSet.getString("emp_name");
double emp_salary = resultSet.getDouble("emp_salary");
int emp_age = resultSet.getInt("emp_age");
System.out.println(emp_id+"\t"+emp_name+"\t"+emp_salary+"\t"+emp_age);
}
//5.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
查询多行多列的数据
public class JDBCOperration {
@Test
public void testyQueryMoreRow() throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
PreparedStatement preparedStatement = connection.prepareStatement("select * from t_emp where emp_age > ?");
preparedStatement.setInt(1,25);
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
int emp_id = resultSet.getInt("emp_id");
String emp_name = resultSet.getString("emp_name");
double emp_salary = resultSet.getDouble("emp_salary");
int emp_age = resultSet.getInt("emp_age");
System.out.println(emp_id+"\t"+emp_name+"\t"+emp_salary+"\t"+emp_age);
}
resultSet.close();
preparedStatement.close();
connection.close();
}
}
更改数据(Update)
插入数据
与查询不同的是:插入数据返回的不是结果集resultSet,而是一个int型数据,表示受影响数据的个数。具体代码如下:
public class JDBCOperration {
@Test
public void testInsert() throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
PreparedStatement preparedStatement = connection.prepareStatement("insert into t_emp(emp_name,emp_salary,emp_age) values(?,?,?)");
preparedStatement.setString(1,"Rock");
preparedStatement.setDouble(2,789.23);
preparedStatement.setInt(3,56);
int result = preparedStatement.executeUpdate();
if(result>0){
System.out.println("成功!");
}else {
System.out.println("失败!");
}
preparedStatement.close();
connection.close();
}
}
修改数据
@Test
public void testUpdate() throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
PreparedStatement preparedStatement = connection.prepareStatement("update t_emp set emp_salary = ? where emp_id = ?");
preparedStatement.setDouble(1,988.56);
preparedStatement.setInt(2,5);
int res = preparedStatement.executeUpdate();
if(res>0){
System.out.println("修改成功!");
}else{
System.out.println("修改失败!");
}
preparedStatement.close();
connection.close();
}
删除数据
@Test
public void testDelete() throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
PreparedStatement preparedStatement = connection.prepareStatement("delete from t_emp where emp_id = ?;");
preparedStatement.setInt(1,3);
int res = preparedStatement.executeUpdate();
if(res>0){
System.out.println("删除成功!");
}else{
System.out.println("删除失败!");
}
preparedStatement.close();
connection.close();
}
常见问题
资源的管理
在使用JDBC的相关资源时,比如Connection、PreparedStatement、ResultSet使用完毕后,要及时关闭这些资源以释放数据库服务器资源和避免内存泄世漏是很重要的。
SQL语句问题
java.sql.SQLSyntaxErrorException
:SQL语句错误异常,一般有几种可能:
-
SQL语句有错误,检查SQL语句!建议SQL语句在SQL工具中测试后再复制到 Java程序中!
-
连接数据库的URL中,数据库名称编写错误,也会报该异常!
SQL语句未设置参数问题
用户名或密码错误
通信异常
JDBC扩展
实体类和ORM
-
在使用JDBC操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了Java中变成了一个一个的变量,不利于维护和管理。而我们Jva是面向对象的,一个表对应的是一个类,一行数据就对应的是Java中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,
这个载体就是实体类! -
ORM(Object Relational Mapping)思想,对象到关系数据库的映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性!
-
当下JDBC中这种过程我们称其为手动ORM。后续我们也会学习ORM框架,比如MyBatis、JPA等。
规范
-
一般把实体类放在一个单独的子包pojo中,提高可读性。
-
类名一般就是数据库表中t_后面的全写
-
列名即为属性名,命名规范要满足“驼峰”
-
测试代码一般放在pojo的上级文件夹中
搭建实体类
package com.sql.advanced.pojo;
//类名就是数据库表t_后面的单词全写
public class Employee {
private Integer empId;
private String empName;
private Double salary;
private Integer empAge;
//构造方法
public Employee(){
}
public Employee(Integer empId, String empName, Double salary, Integer empAge){
this.empId = empId;
this.empName = empName;
this.salary = salary;
this.empAge = empAge;
}
public Integer getEmpId() {
return empId;
}
public void setEmpId(Integer empId) {
this.empId = empId;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public Integer getEmpAge() {
return empAge;
}
public void setEmpAge(Integer empAge) {
this.empAge = empAge;
}
@Override
public String toString() {
return "Employee{" +
"empId=" + empId +
", empName='" + empName + '\'' +
", salary=" + salary +
", empAge=" + empAge +
'}';
}
}
将查询到的一条数据封装到一个对象里
@Test
public void testORM() throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
PreparedStatement preparedStatement = connection.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp where emp_id = ?");
preparedStatement.setInt(1,1);
ResultSet resultSet = preparedStatement.executeQuery();
Employee employee = null;//为了方便使用,在取值之前先创建一个变量
//以下即为ORM的体现:
if(resultSet.next()){
employee = new Employee();
//为对象的属性赋值
employee.setEmpId(resultSet.getInt("emp_id"));
employee.setEmpName(resultSet.getNString("emp_name"));
employee.setSalary(resultSet.getDouble("emp_salary"));
employee.setEmpAge(resultSet.getInt("emp_age"));
}
System.out.println(employee);
resultSet.close();
preparedStatement.close();
connection.close();
}
将多个数据封装进多个对象中
@Test
public void testORMlist() throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc_tt", "root", "123456");
PreparedStatement preparedStatement = connection.prepareStatement("select * from t_emp");
ResultSet resultSet = preparedStatement.executeQuery();
Employee employee = null;
List<Employee> employeelist = new ArrayList<>();
while(resultSet.next()){
employee = new Employee();
//为当前对象属性赋值
employee.setEmpId(resultSet.getInt("emp_id"));
employee.setEmpName(resultSet.getNString("emp_name"));
employee.setSalary(resultSet.getDouble("emp_salary"));
employee.setEmpAge(resultSet.getInt("emp_age"));
//将当前对象存入集合
employeelist.add(employee);
}
//输出结果
for(Employee emp:employeelist){
System.out.println(emp);
}
resultSet.close();
preparedStatement.close();
connection.close();
}
主键回显
- 在数据中,执行新增操作时,主键列为自动增长,可以在表中直观的看到。但是在Java程序中,我们执行完新增后,只能得到受影响行数,无法得知当前新增数据的主键值。在Java程序中获取数据库中插入新数据后的主键值,并赋值给Java对象,此操作为主键回显。
@Test
public void testturnPk() throws Exception{
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:/jdbc_tt", "root", "123456");
//预编译sql语句,告知preparement返回主键值
String sql = "insert into t_emp(emp_name,emp_salary,emp_age) values(?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
//创建对象,将对象属性赋值,并填充在占位符上
Employee employee = new Employee(null,"Jack",123.45,29);
preparedStatement.setString(1,employee.getEmpName());
preparedStatement.setDouble(2,employee.getSalary());
preparedStatement.setInt(3,employee.getEmpAge());
//执行语句
int res = preparedStatement.executeUpdate();
//处理结果
if(res > 0){
System.out.println("插入成功!");
//获取当前新增数据的主键,回显到Java中employee对象的属性上
//返回的主键值是一个单行单列的数据
ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
if(generatedKeys.next()){
int empId = generatedKeys.getInt(1);
employee.setEmpId(empId);
}
System.out.println(employee);
}else{
System.out.println("插入失败!");
}
//释放资源
preparedStatement.close();
connection.close();
}
总结:要想拿到主键,需要在预编译sql语句时添加一个参数:RETURN_GENERATED_KEYS
,来告知我需要拿到主键值。而后在确认语句执行成功后,用方法preparedStatement.getGeneratedKeys()
来接收获取到的主键值(接收到的是一个单行单列的结果集),最后读取该结果集即可得到主键值。
优化:在判断中我们使用到了reaultset结果集,导致在后面不可直接释放,所以将结果集在先前就定义好。但这样也可能在语句没有执行成功时该值为null,导致出现空指针异常,所以在释放前需判断是否为空。以下为优化后的代码:
@Test
public void testturnPk() throws Exception{
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:/jdbc_tt", "root", "123456");
//预编译sql语句,告知preparement返回主键值
String sql = "insert into t_emp(emp_name,emp_salary,emp_age) values(?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
//创建对象,将对象属性赋值,并填充在占位符上
Employee employee = new Employee(null,"Jack",123.45,29);
preparedStatement.setString(1,employee.getEmpName());
preparedStatement.setDouble(2,employee.getSalary());
preparedStatement.setInt(3,employee.getEmpAge());
//执行语句
int res = preparedStatement.executeUpdate();
ResultSet generatedKeys = null;
//处理结果
if(res > 0){
System.out.println("插入成功!");
//获取当前新增数据的主键,回显到Java中employee对象的属性上
//返回的主键值是一个单行单列的数据
generatedKeys = preparedStatement.getGeneratedKeys();
if(generatedKeys.next()){
int empId = generatedKeys.getInt(1);
employee.setEmpId(empId);
}
System.out.println(employee);
}else{
System.out.println("插入失败!");
}
//释放资源
if(generatedKeys != null){
generatedKeys.close();
}
preparedStatement.close();
connection.close();
}
批量操作
-
插入多条数据时,一条一条发送给数据库执行,效率低下!
-
通过批量操作,可以提升多次操作效率!
-
实现方法:
-
必须在连接数据库的URL后面追加
?rewriteBatchStatement=true
表示允许批量操作。 -
新增SQL必须用
values
,且语句最后不可加分号。 -
调用
addBatch()
,即将SQL语句进行批量添加操作 -
统一执行批量操作
executeBatch()
-
@Test
public void testBatch() throws Exception{
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbc_tt?rewriteBatchStatements = true", "root", "123456");
/*
注意:
*/
String sql = "insert into t_emp(emp_name,emp_salary,emp_age) values (?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
long start = System.currentTimeMillis();
//插入多条数据
for (int i = 0;i<10;i++){
preparedStatement.setString(1,"marry"+i);
preparedStatement.setDouble(2,100.0+i);
preparedStatement.setInt(3,20+i);
preparedStatement.addBatch();
}
//执行操作
preparedStatement.executeBatch();
long end = System.currentTimeMillis();
System.out.println("消耗时间:"+(end-start));
preparedStatement.close();
connection.close();
}
连接池
现有问题
每次操作数据库都要获取新连接,使用完毕后就close释放,频繁的创建和销毁逍成资源浪费。连接的数量无法把控对服务器来说压力巨大。
连接池
连接池就是数据库连接对象的缓冲区,通过配置,由连接池负责创建连接、管理连接、释放连接等操作。
预先创建数据库连接放入连接池,用户在请求时,通过池直接获取连接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率。
-
当池中无连接可用,且未达到上限时,连接池会新建连接。
-
池中连接达到上限,用户请求会等待,可以设置超时时间。
常见连接池
JDBC的数据库连接池使用javax.sql.DataSource接口进行规范,所有的第三方连接池都实现此接口,自行添具体实现!也就是说,所有连接池获取连接的和回收连接方法都一样,不同的只有性能和扩展功能!
-
DBCP是Apache提供的数据库连接池,速度相对C3Po较快,但自身存在一些BUG。
-
C3P0是一个开源组织提供的一个数据库连接池。速度相对较慢,稳定性还可以。
-
Proxool是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能。稳定性较c3p0差一点。
-
Druid是阿里提供的数据库连接池。是集DBCP、C3P0、Proxool优点于一身的数据库连接池,性能、扩展性、易用性都更好。功能丰富。
-
Hikari(shi ga li)取自日语,是光的意思,是SpringBoot2,x之后内置的一款连接池。基于BoneCP(已经放弃维护,推荐该连接池)做了不少的改进和优化,口号是快速、简单、可靠。
Druid连接池的使用
使用步骤:
-
引入jar包:
druid-1.2.9.jar
-
编码
硬编码实现(了解)
- 创建DruidDataSource连接池对象
- 设置连接池配置信息
- 通过连接池获取连接对象
- 回收连接
@Test
public void testHardCodeDruid() throws Exception{
//1. 创建DruidDataSource连接池对象
DruidDataSource ds = new DruidDataSource();
//2. 设置连接池配置信息:
// 2.1 必须配置的数据:驱动类、连接字符串、用户名、密码
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://127.0.0.1:3306/jdbc_tt");
ds.setUsername("root");
ds.setPassword("123456");
//2.2 非必须配置:
ds.setInitialSize(10);//初始化数量
ds.setMaxActive(20);//设置最大的数量
//3.通过连接池获取连接对象
DruidPooledConnection connection = ds.getConnection();
//基于connection进行CRUD
//回收连接
connection.close();
}
软编码实现(推荐)
可以发现,在硬编码中,我们将Druid连接池配置与Java代码编写在一起(即耦合性较大),后期更新维护起来困难。
解决办法:在项目目录下创建resources
文件夹,标识该文件夹为资源目录,创建db.properties
配置文件,将连接信息定义在该文件中。— 软编码
-
创建properties集合,用于存储外部配置文件key和value的值
-
通过类加载获取外部配置文件,获取输入流,加载到properties集合里
-
基于properties集合构建连接池
DruidDataResource
-
通过连接池获取连接对象
配置文件
注意拼写不能错
driverClassName = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/jdbc_tt
username = root
password = 123456
initialSize = 10
maxActive = 20
Java文件
@Test
public void testResourcesDruid() throws Exception{
//1. 创建Properties集合,用于存储外部配置文件key和value的值
Properties properties = new Properties();
//2. 通过类加载获取外部配置文件,获取输入流,加载到properties集合里
InputStream resourceAsStream = DruidTest.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(resourceAsStream);
//3. 基于properties集合构建DruidDataResource
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);//DruidDataResource实现了DataSource接口
//4. 通过连接池获取连接对象
Connection connection = dataSource.getConnection();
System.out.println(connection);
//5. 开发CRUD
//6. 回收连接
connection.close();
}
HikariCP连接池的使用
使用步骤
-
导入jar包
HikariCP-5.1.0.jar
和slf4j-api-2.0.9.jar
-
编码
硬编码实现(了解)
-
创建HikariDataResource连接池对象
-
设置连接池的配置信息【必需/非必需】
-
通过连接池获取连接对象
-
CRUD
-
回收连接
@Test
public void testHardHikari() throws Exception{
*/
//1.创建HikariDataResource连接池对象
HikariDataSource ds = new HikariDataSource();
//2.设置连接池的配置信息【必需/非必需】
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbc_tt");
ds.setUsername("root");
ds.setPassword("123456");
ds.setMinimumIdle(10);
ds.setMaximumPoolSize(20);
//3.通过连接池获取连接对象
Connection connection = ds.getConnection();
System.out.println(connection);
//4.CRUD
//5.回收连接
connection.close();
}
软编码实现(推荐)
-
创建
Properties
集合,用于存储外部配置文件键值对的值 -
读取外部文件,获取输入流,加载到
properties
中 -
基于
properties
创建Hikari连接池的配置对象HikariConfig
-
基于配置对象
HikariConfig
创建连接池HikariDataResource
-
建立连接
-
CRUD
-
回收连接
配置文件
driverClassName = com.mysql.cj.jdbc.Driver
jdbcUrl = jdbc:mysql://localhost:3306/jdbc_tt
username = root
password = 123456
minimumIdle = 10
maximumPoolSize = 20
Java文件
@Test
public void testResourcesHikari() throws Exception{
//1.创建Properties集合,用于存储外部配置文件key和value的值
Properties properties = new Properties();
//2.读取外部文件,获取输入流,加载到properties中
InputStream resourceAsStream = HikariTest.class.getClassLoader().getResourceAsStream("hikari.properties");
properties.load(resourceAsStream);
//3.创建HikariConfig连接池的配置对象,将Properties集合传进去
HikariConfig hikariconfig = new HikariConfig(properties);
//4.基于HikariConfig连接池的配置对象,构建HikariDataResource连接池
HikariDataSource hikariDataSource = new HikariDataSource(hikariconfig);
//建立连接
Connection connection = hikariDataSource.getConnection();
System.out.println(connection);
//CRUD
//回收连接
connection.close();
}
JDBC优化及工具类封装
JDBC工具类封装(v1.0)
现有问题
我们在使用JDBC的过程中,发现部分代码存在冗余的问题:
创建连接池
获取连接
连接的回收
注意:工具类仅对外提供共性的功能代码,所以均为静态的。
-
维护一个连接池对象
-
对外提供在连接池中获取连接的方法
-
对外提供回收链接的方法。
JDBCUtil.java
package com.sql.senior.util;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* JDBC工具类(v1.0):
* 1. 维护一个连接池对象
* 2. 对外提供在连接池中获取连接的方法
* 3. 对外提供回收连接的方法
*
* 注意:工具类仅对外提供共性的功能代码,所以均为静态方法
*/
public class JDBCUtil {
//1. 维护一个连接池对象
//创建连接池引用,因为要提供给当前项目的全局使用,所以创建为静态的
private static DataSource dataSource;
//在项目启动时,即创建连接池对象,赋值给dataSource
static{
try {
Properties properties = new Properties();
InputStream resourceAsStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(resourceAsStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//2. 对外提供在连接池中获取连接的方法
public static Connection getConnection(){
try {
return dataSource.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
//3. 对外提供回收连接的方法
public static void release(Connection conn) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
JDBCUtilTest.java
package com.sql.senior;
import com.sql.senior.util.JDBCUtil;
import org.junit.Test;
import java.sql.Connection;
public class JDBCUtilTest {
@Test
public void testGetConnection(){
Connection connection = JDBCUtil.getConnection();
System.out.println(connection);
JDBCUtil.release(connection);
}
}
ThreadLocal
JDK1.2的版本中就提供java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。通常用来在在多线程中管理共享数据库连接、Session等。
ThreadLocal用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个
ThreadLocalMap<ThreadLocal,Object>,其key就是一个ThreadLocal,而Object即为该线程的共享变量。
而这个map是通过ThreadLocal的set和get方法操作的。对于同一个static ThreadLocal,不同线程只能从中get,set,removel自己的变量,而不会影响其他线程的变量。
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离。
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
1、ThreadLocal对象.get:获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal对象.set:设置ThreadLocal中当前线程共享变量的值。
3、ThreadLocal对象.remove:移除ThreadLocal中当前线程共享变量的值。
工具类封装v2.0
将从连接池中获取连接的操作改为从ThreadLocal中获取连接。
-
维护一个连接池对象,维护一个线程绑定变量的ThreadLocal对象。
-
对外提供在threadlocal中获取的连接(如果是第一次获取,threadlocal为空,则在连接池获取一个连接存到ThreadLocal中;否则就直接从ThreadLocal中获取)
-
对外提供回收链接的方法,回收过程中,将要回收的连接从ThreadLocal中移除。
注意:使用ThreadLocal就是为了一个线程在多次数据库操作中,使用的是同一个连接!
JDBCUtilV2.java
package com.sql.senior.util;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.*;
import java.io.*;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
public class JDBCUtilV2 {
//1. 维护一个连接池对象
//创建连接池引用,因为要提供给当前项目的全局使用,所以创建为静态的
private static DataSource dataSource;
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
//在项目启动时,即创建连接池对象,赋值给dataSource
static{
try {
Properties properties = new Properties();
InputStream resourceAsStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(resourceAsStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//2. 对外提供在连接池中获取连接的方法
public static Connection getConnection(){
try {
//在ThreadLocal中获取Connection
Connection connection = threadLocal.get();
//如果ThreadLocal里没有存储connection,也就是第一次获取链接
if(connection == null){
//在连接池中获取一个连接
connection = dataSource.getConnection();
//存进threadLocal中
threadLocal.set(connection);
}
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
//3. 对外提供回收连接的方法
public static void release() {
try {
Connection connection = threadLocal.get();
if(connection != null){
threadLocal.remove();
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
功能测试比较
JDBCUtilV1
@Test
public void testJDBCV2(){
//V1功能测试
Connection connection1 = JDBCUtil.getConnection();
Connection connection2 = JDBCUtil.getConnection();
Connection connection3 = JDBCUtil.getConnection();
System.out.println(connection1);
System.out.println(connection2);
System.out.println(connection3);
}
获取到的三个不同的连接。
JDBCUtilV2
@Test
public void testJDBCV2(){
Connection connection1 = JDBCUtilV2.getConnection();
Connection connection2 = JDBCUtilV2.getConnection();
Connection connection3 = JDBCUtilV2.getConnection();
System.out.println(connection1);
System.out.println(connection2);
System.out.println(connection3);
}
三次获取的连接是一样的。
DAO封装及BaseDAO工具类
DAO概念
DAO:Data Access Object,数据访问对象
Java是面向对象语言,数据在Java中通常以对象的形式存在。一张表对应一个实体类,一张表的操作对应一个DAO对象!
在Java操作数据库时,我们会将对同一张表的增删改查操作统一维护起来,维护的这个类就是DAO层。
DAO层只关注对数据库的操作,供业务层Service调用,将职责划分清楚!
BaseDAO概念
前面JDBCUtil实现了1.注册驱动2.获取链接6.释放连接几个步骤封装起来,而BaseDao解决的就是将中间部分3.预编译SQL语句4.为占位符赋值,执行SQL语句5.处理结果封装起来。
(详细的查看BaseDao代码部分)
操作方法
- 要想操作一张表,我们需要创建该表的类
Employee
: - 创建各种操作的接口
EmployeeDao
,里面存放各种操作的抽象类。- 之所以选择接口,是因为在业务编写时,对每一个不同的数据库的CURD操作是不一样的,设置接口便于后期的操作。
- 创建实现类
EmployeeDaoImpl
,用于实现操作。 - 创建
BaseDao
用于处理通用的操作,避免重复代码的冗余。 - 写测试方法test。
项目结构
Employee
package com.sql.senior.pojo;
//类名就是数据库表t_后面的单词全写
public class Employee {
private Integer empId;
private String empName;
private Double salary;
private Integer empAge;
//构造方法
public Employee(){
}
public Employee(Integer empId, String empName, Double salary, Integer empAge){
this.empId = empId;
this.empName = empName;
this.salary = salary;
this.empAge = empAge;
}
public Integer getEmpId() {
return empId;
}
public void setEmpId(Integer empId) {
this.empId = empId;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public Integer getEmpAge() {
return empAge;
}
public void setEmpAge(Integer empAge) {
this.empAge = empAge;
}
@Override
public String toString() {
return "Employee{" +
"empId=" + empId +
", empName='" + empName + '\'' +
", salary=" + salary +
", empAge=" + empAge +
'}';
}
}
EmployeeDao
package com.sql.senior.dao;
import java.util.List;
import com.sql.senior.pojo.Employee;
/**
* 该接口对应表的CURD操作
*/
public interface EmployeeDao {
/**
* 查询所有的操作
* @return 返回表中所有的数据
*/
List<Employee> selectAll();
/**
* 根据empId查询单个员工的数据操作
* @param id empId 主键对
* @return 返回一个员工对象(一行数据
*/
Employee selectById(Integer id);
/**
* 新增一条员工数据的操作
* @param employee ORM思想中的一个员工对象
* @return 返回受影响的行数
*/
int insert(Employee employee);
/**
* 修改一条员工数据的操作
* @param employee ORM思想中的一个员工对象
* @return 返回受影响的行数
*/
int update(Employee employee);
/**
* 根据empId删除一条员工数据
* @param empid empId 主键对
* @return 返回受影响的行数
*/
int delete(Integer empid);
}
BaseDao
/**
* 将共性的数据库的操作代码封装在BaseDao中
*/
public class BaseDao {
/**
* 通用的增删改的方法
* @param sql 调用者要执行的SQL语句
* @param params SQL语句中的占位符要赋值的参数
* @return 受影响的行数
*/
public int excuteUpdate(String sql,Object... params) throws SQLException {
//1.通过JDBCUtilV2获取数据库链接
Connection connection = JDBCUtilV2.getConnection();
//2.预编译sql语句
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//3.占位符赋值
if(params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
//占位符从1开始,参数数组是从0开始
preparedStatement.setObject(i+1,params[i]);
}
}
//4.执行
int row = preparedStatement.executeUpdate();
//5.释放资源
preparedStatement.close();
JDBCUtilV2.release();
//6.返回结果
return row;
}
/**
* 通用的查询:多行多列、单行多列、单行单列
* 多行多列:List<Employee>
* 单行多列:Employee
* 单行单列:封装的是一个结果:Double、Integer...
* 封装过程:
* 1. 返回的类型:泛型,类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDao即可
* 2. 返回的结果:通用,List,可以存储多个结果,也可以只存储一个结果get(0)
* 3. 结果的封装:反射,要求调用者告知BaseDao要封装对象的类对象。
*/
public <T> List<T> excuteQuery(Class<T> clazz,String sql,Object... pareams) throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
Connection connection = JDBCUtilV2.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
if(pareams != null && pareams.length > 0) {
for (int i = 0; i < pareams.length; i++) {
preparedStatement.setObject(i+1,pareams[i]);
}
}
//执行sql,获取结果集
ResultSet resultSet = preparedStatement.executeQuery();
//获取结果集的元数据对象:包含了列的数量、每个列的名称
ResultSetMetaData metaData = resultSet.getMetaData();
List<T> list = new ArrayList<>();
int columnCount = metaData.getColumnCount();
//处理结果
while(resultSet.next()){
//循环一次,通过反射创建一个对象
T t = clazz.newInstance();
//循环遍历当前列,循环几次,看有多少列
for (int i = 1; i <= columnCount; i++) {
//通过下标获取列的值
Object value = resultSet.getObject(i);
//获取到的列的value值就是t这个对象中的某一属性
//获取当前列的名字 = 对象的属性名
String filedName = metaData.getColumnLabel(i);
//通过这个列的名字获取对象的属性
Field declaredField = clazz.getDeclaredField(filedName);
//获取权限
declaredField.setAccessible(true);
declaredField.set(t,value);
}
list.add(t);
}
preparedStatement.close();
resultSet.close();
JDBCUtilV2.release();
return list;
}
/**
* 通用查询,在上面查询中获取第一个结果,简化了获取单行单列的获取,单行多列的获取。
*/
public <T> T excuteQueryBean(Class<T> clazz,String sql,Object... params) throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
List<T> list = excuteQuery(clazz, sql, params);
if(list == null || list.size() == 0){
return null;
}
return list.get(0);
}
}
EmployeeImpl
public class EmployeeDaoImpl extends BaseDao implements EmployeeDao {
@Override
public List<Employee> selectAll() {
//由于在实现类BaseDao中我们创建对象是通过获取matadata的列名创建实例,而我们创建的对象属性名为驼峰的,与数据库中的命名不同,这时就要考虑起别名
try {
String sql = "select emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge from t_emp";
return excuteQuery(Employee.class,sql,null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Employee selectById(Integer id) {
try {
String sql = "select emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge from t_emp where emp_id = ?";
return excuteQueryBean(Employee.class,sql,id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int insert(Employee employee) {
try {
String sql = "insert into t_emp (emp_name,emp_salary,emp_age) values(?,?,?);";
return excuteUpdate(sql,employee.getEmpName(),employee.getSalary(),employee.getEmpAge());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public int update(Employee employee) {
try {
String sql = "update t_emp set emp_salary = ? where emp_id = ?;";
return excuteUpdate(sql,employee.getSalary(),employee.getEmpId());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public int delete(Integer empid) {
try {
String sql = "delete from t_emp where emp_id = ?";
return excuteUpdate(sql,empid);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
测试方法
//测试查询所有
@Test
public void test3(){
//创建Dao实现类对象
EmployeeDao employeeDao = new EmployeeDaoImpl();
//2.调用方法
List<Employee> employees = employeeDao.selectAll();
//3.处理
for(Employee employee : employees){
System.out.println("employee:"+employee);
}
}
//测试通过id查询单条数据
@Test
public void test4(){
EmployeeDao employeeDao = new EmployeeDaoImpl();
Employee employee = employeeDao.selectById(5);
System.out.println("employee = "+employee);
}
//测试插入数据
@Test
public void test5(){
EmployeeDao employeeDao = new EmployeeDaoImpl();
Employee employee = new Employee(null, "严杰", 8999.84, 22);
int insert = employeeDao.insert(employee);
System.out.println("insert = "+insert);
}
//测试修改数据
@Test
public void test6(){
EmployeeDao employeeDao = new EmployeeDaoImpl();
Employee employee = new Employee(28, "严杰", 9000.84, 22);
int update = employeeDao.update(employee);
System.out.println("update = "+update);
}
//测试删除数据
@Test
public void test7(){
EmployeeDao employeeDao = new EmployeeDaoImpl();
int delete = employeeDao.delete(29);
System.out.println("delete = "+delete);
}
总结
**Employee类:**用于创建一个个的ORM思想下的对象。
**EmployeeDao接口:**存放表对应的CRUD操作的抽象方法,这是为了适应不同数据库下操作语句的不同而创建的接口类。
**BaseDao类:**在执行语句过程中有一些重复性的操作:获取链接、获取预编译对象、为占位符赋值、执行语句、释放资源。该类的作用就是将这些重复冗余的操作进行封装。(其中获取连接、释放资源由JDBCUtil进行封装)
**EmployeeDao类:**这里就用于编写非重复性的代码,执行其业务语句,并进行最后一层封装:只对外暴露简单的方法的调用。
JDBC中事务实现
事务的四大特性
-
原子性(Atomicity): 事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
-
一致性(Consistency): 事务完成时,必须使所有的数据都保持一致状态。
-
隔离性(Isolation): 数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
-
持久性(Durability): 事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
方式一:将默认提交方式设置为手动提交
select @@autocommit;-- 查看事务提交方式
set @@autocommit;-- 设置为手动
commit;-- 提交事务
rollback;-- 回滚事务
方式二:开启事务
start transaction 或 begin;-- 开启事务
commit;-- 提交事务
rollback;-- 回滚事务
查看当前连接,事务提交的方式
(ON、1、true自动提交;OFF、0、false关闭自动提交,需要手动提交)
show variables like 'autocommit'
set autocommit = false;
我们通常情况下要想实现事务,都会将事务的提交方式改为手动提交,但要注意,完成这系列业务后,要将手动提交改为自动提交。
所以基于这个原因,我们就要对工具类
JDBCUtil
做一个优化:在操作完毕后,将连接归还给连接池之前,要将事务的手动提交改回自动提交。
JDBCUtil
//3. 对外提供回收连接的方法
public static void release() {
try {
Connection connection = threadLocal.get();
if(connection != null){
threadLocal.remove();
//将事务提交方式改为自动提交
connection.setAutoCommit(true);
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
同样的,在别的代码下所有释放连接的操作都要进行优化:
如果没有开启事务autocommit=true,则该怎么释放资源就怎么释放;
但如果开启了事务autocommit=false,则需要我们手动提交,不可让其在执行过程中直接释放资源。
BaseDao
将释放资源的部分添加一个判断语句:
if(connection.getAutoCommit()){//如果没有开启事务(即自动提交)则释放
JDBCUtilV2.release();//如果我们开启了事务,应该手动提交,所以并不能让他直接释放
}
JDBC事务代码实现
-
准备数据库表:
create table t_bank( id int primary key auto_increment comment '账号主键', ACCOUNT varchar(20) NOT NULL UNIQUE comment '账号', money int unsigned comment '金额,不能为负值'); insert into t_bank(ACCOUNT, money) values ('张三',1000),('李四',1000);
-
DAO接口代码:
public interface BankDao { //加钱的操作 public int addMoney(Integer id,Integer money); //减钱的操作 public int subMoney(Integer id,Integer money); }
-
DAO实现类代码:
public class BankDaoImpl extends BaseDao implements BankDao { @Override public int addMoney(Integer id, Integer money) { try { String sql = "update t_bank set money = money + ? where id = ?"; return excuteUpdate(sql,money,id); } catch (SQLException e) { throw new RuntimeException(e); } } @Override public int subMoney(Integer id, Integer money) { try { String sql = "update t_bank set money = money - ? where id = ?"; return excuteUpdate(sql,money,id); } catch (SQLException e) { throw new RuntimeException(e); } } }
-
测试代码:
@Test public void test8(){ BankDao bankDao = new BankDaoImpl(); Connection connection = null; try { //1.获取连接,将连接的事务改为手动提交 connection = JDBCUtilV2.getConnection(); connection.setAutoCommit(false);//开启事务 //2.减钱 bankDao.subMoney(1,100); //3.加钱 bankDao.addMoney(2,100); //4.前置的多次DAO操作如果没有异常。则提交事务 connection.commit(); } catch (Exception e) { try { //前置的操作出现异常了,进行回滚操作 connection.rollback(); } catch (Exception ex) { throw new RuntimeException(ex); } throw new RuntimeException(e); }finally { JDBCUtilV2.release(); } }
注意:
当开启事务后,切记一定要根据代码执行结果来决定是否提交或回滚!否则数据库看到的不一定是正确的数据!