悉知JDBC
🏠个人主页:不会写代码的满满
🧑个人简介:大家好,我是满满,一个想要与大家共同进步的男人😉😉
目前状况🎉:开学即将大三,目标就是半年内找到一份实习工作👏👏
💕欢迎大家:这里是CSDN,我总结知识的地方,欢迎来到我的博客,我亲爱的大佬😘
正文开始 ----------
0. 学习目标
- 掌握JDBC的概念
- 掌握JDBC操作单表CRUD的方法
- 掌握JDBC操作数据库事务的方法
- 掌握数据库操作工具类的实现方法
- 了解一些市面上的流行的连接池
- 了解并熟悉掌握三层架构
第1章JDBC概述
首先看一下下面这张JDBC的思维导图理一下思路吧!!!

客户端操作MySQL数据库的方式
- 使用第三方客户端来访问MySQL:SQLyog、Navicat、SQLWave、MyDB Studio、EMS SQL Manager for MySQL
- 使用MySQL自带的命令行方式
- 通过Java来访问MySQL数据库,今天要学习的内容
什么是JDBC:Java DataBase Connectivity
(Java数据库连接) JDBC是Java访问数据库的标准规范
JDBC的作用:JDBC是用于执行SQL语句的Java API(Java语言通过JDBC可以操作数据库)
1.1 JDBC概述
JDBC:Java Database Connectivity,它是代表一组独立于任何数据库管理系统(DBMS)的API,声明在java.sql与javax.sql包中,是SUN(现在Oracle)提供的一组接口规范。由各个数据库厂商来提供实现类,这些实现类的集合构成了数据库驱动jar。
即JDBC技术包含两个部分:
(1)java.sql包和javax.sql包中的API
因为为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种DBMS软件,Java代码可以保持一致性。
(2)各个数据库厂商提供的jar
因为各个数据库厂商的DBMS软件各有不同,那么内部如何通过sql实现增、删、改、查等管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。
1.2.JDBC的由来
直接写代码操作数据库
直接写代码操作数据库存在的问题:- 不知道MySQL数据库的操作方式,解析方式
- 代码繁琐,写起来麻烦
- MySQL和Oracle等其他数据库的操作方式和解析方式不同,每个数据库都要写一套代码
- MySQL和Oracle等其他数据库相互切换麻烦
JDBC规范定义接口,具体的实现由各大数据库厂商来实现
JDBC是Java访问数据库的标准规范。真正怎么操作数据库还需要具体的实现类,也就是数据库驱动。每个数据库厂商根据自家数据库的通信格式编写好自己数据库的驱动。所以我们只需要会调用JDBC接口中的方法即可。数据库驱动由数据库厂商提供。
JDBC的好处:
- 我们只需要会调用JDBC接口中的方法即可,使用简单
- 使用同一套Java代码,进行少量的修改就可以访问其他JDBC支持的数据库
JDBC会用到的包:
- java.sql:JDBC访问数据库的基础包,在JavaSE中的包。如:java.sql.Connection
- javax.sql: JDBC访问数据库的扩展包
- 数据库的驱动,各大数据库厂商来实现。如:MySQL的驱动:com.mysql.jdbc.Driver
JDBC四个核心对象
这几个类都是在java.sql包中
- DriverManager: 用于注册驱动
- Connection: 表示与数据库创建的连接
- Statement: 执行SQL语句的对象
- ResultSet: 结果集或一张虚拟表
1.3 JDBC使用步骤
代码编写步骤:
JDBC访问数据库步骤
1:注册一个Driver驱动
三部曲:
(1)将DBMS数据库管理软件的驱动jar拷贝到项目的libs目录中
例如:mysql-connector-java-5.1.36-bin.jar
(2)把驱动jar添加到项目的build path中
(3)将驱动类加载到内存中
Class.forName(“com.mysql.jdbc.Driver”);
2:获取数据库连接(Connection)
Connection conn = DriverManager.getConnection(url,username,password);
mysql的url:jdbc:mysql://localhost:3306/数据库名?参数名=参数值
jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8
(如果JDBC程序与服务器端 的字符集不一致,会导致乱码,那么可以通过参数指定服务器端的字符集
3:创建SQL命令发送器Statement
创建Statement或PreparedStatement对象
4:通过Statement发送SQL命令并得到结果
5:处理结果(select语句)
6:关闭数据库资源ResultSet Statement Connection
1.4 相关的API:
相关API介绍
1、DriverManager:驱动管理类
使用Class.forName("com.mysql.jdbc.Driver");
加载驱动,这样驱动只会注册一次
public class Demo01 {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
// 后期可以将"com.mysql.jdbc.Driver"字符串写在文件中.
}
}
2、Connection:代表数据库连接
注意:使用
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
,存在两方面不足
- 硬编码,后期不易于程序扩展和维护
- 驱动被注册两次
3、Statement和PreparedStatement:用来执行sql
执行增、删、改:int executeUpate()
执行查询:ResultSet executeQuery()
4、如何遍历ResultSet ?
(1)boolean next():判断是否还有下一行
(2)getString(字段名或序号),getInt(字段名或序号),getObject(字段名或序号)
参数说明
String url
:连接数据库的URL,用于说明连接数据库的位置String user
:数据库的账号String password
:数据库的密码
连接数据库的URL地址格式:协议名:子协议://服务器名或IP地址:端口号/数据库名?参数=参数值
MySQL写法:jdbc:mysql://localhost:3306/dayxx 如果是本地服务器,端口号是默认的3306,则可以简写:
jdbc:mysql:///db1
注意事项
如果数据出现乱码需要加上参数: ?characterEncoding=utf8,表示让数据库以UTF-8编码来处理数据。
如: jdbc:mysql://localhost:3306/dayxx?characterEncoding=utf8
若出现以下内容,代表数据库服务没有启动

示例代码1:增、删、改
public class TestJDBC {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//1、注册驱动
//(1)方式一:Class.forName("驱动类的全名称")
Class.forName("com.mysql.jdbc.Driver");
// (2)创建驱动类的对象
// new com.mysql.jdbc.Driver();//硬编码
//(3)通过DriverManager注册驱动
// DriverManager.registerDriver(new com.mysql.jdbc.Driver());//硬编码
//2、获取连接,连接数据库
//TCP/IP协议编程,需要服务器的IP地址和端口号
//mysql的url格式:jdbc协议:子协议://主机名:端口号/要连接的数据库名
String url = "jdbc:mysql://localhost:3306/test";//其中test是数据库名
String user = "root";
String password = "123456";
Connection conn = DriverManager.getConnection(url, user, password);
//3、执行sql
//添加一个部门到数据库的t_department表中
//(1)编写sql
String sql = "insert into t_department values(null,'计算部2','计算钞票2')";
/*
* 回忆: TCP/IP程序时
* Socket代表连接
* socket.getOutputStream()来发送数据,
* socket.getInputStream()来接收数据
*
* 可以把Connection比喻成Socket
* 把Statement比喻成OutputStream
*/
//(2)获取Statement对象
Statement st = conn.createStatement();
//(3)执行sql
int len = st.executeUpdate(sql);
//(4)处理结果
System.out.println(len>0?"成功":"失败");
//4、关闭
st.close();
conn.close();
}
}
示例代码2:查询
public class TestSelect {
public static void main(String[] args) throws Exception{
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2、连接数据库
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
// 3、执行sql
String sql = "SELECT * FROM t_department";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);//ResultSet看成InputStream
while(rs.next()){//next()表示是否还有下一行
Object did = rs.getObject(1);//获取第n列的值
Object dname = rs.getObject(2);
Object desc = rs.getObject(3);
/*
int did = rs.getInt("did");//也可以根据列名称,并且可以按照数据类型获取
String dname = rs.getString("dname");
String desc = rs.getString("description");
*/
System.out.println(did +"\t" + dname + "\t"+ desc);
}
// 4、关闭
rs.close();
st.close();
conn.close();
}
}
第2章 使用PreparedStatement处理CRUD
2.1 通过PreparedStatement来解决Statement的问题
Statement的问题:通过PreparedStatement来代替
1. sql拼接
String sql = "insert into t_employee(ename,tel,gender,salary) values('" + ename + "','" + tel + "','" + gender + "'," + salary +")";
Statement st = conn.createStatement();
int len = st.executeUpdate(sql);
2 . sql注入
String sql = "SELECT * FROM t_employee where ename='" + ename + "'";
//如果我此时从键盘输入ename值的时候,输入:张三' or '1'= '1
//结果会把所有数据都查询出来
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
3. 处理blob等类型的数据
创建含有Blob字段类型的表 测试插入
String sql = "insert into user(username,photo) values('zs', 图片字节流)";
//此时photo是blob类型的数据时,无法在sql中直接拼接
PreparedStatement解决问题:
4. 避免sql拼接
String sql = "insert into t_employee(ename,tel,gender,salary) values(?,?,?,?)";
PreparedStatement pst = conn.prepareStatement(sql);//这里要传带?的sql,然后mysql端就会对这个sql进行预编译
//设置?的具体值
/*pst.setString(1, ename);
pst.setString(2, tel);
pst.setString(3, gender);
pst.setDouble(4, salary);*/
pst.setObject(1, ename);
pst.setObject(2, tel);
pst.setObject(3, gender);
pst.setObject(4, salary);
int len = pst.executeUpdate();//此处不能传sql
System.out.println(len);
5. 不会有sql注入
String sql = "SELECT * FROM t_employee where ename=?";
//即使输入'张三' or '1'= '1'也没问题
PreparedStatement pst = conn.prepareStatement(sql);
//中间加入设置?的值
pst.setObject(1, ename);
ResultSet rs = pst.executeQuery();
6. 处理blob类型的数据
String sql = "insert into user(username,photo) values(?,?)";
PreparedStatement pst = conn.prepareStatement(sql);
//设置?的值
pst.setObject(1, "zs");
FileInputStream fis = new FileInputStream("D:/QMDownload/img/美女/15.jpg");
pst.setBlob(2, fis);
int len = pst.executeUpdate();
System.out.println(len>0?"成功":"失败");
注意两个问题:
①my.ini关于上传的字节流文件有大小限制,可以在my.ini中配置变量
max_allowed_packet=16M
②每一种blob有各自大小限制:
tinyblob:255字节、blob:65k、mediumblob:16M、longblob:4G
2.2 获取自增长键值
/*
* 我们通过JDBC往数据库的表格中添加一条记录,其中有一个字段是自增的,那么在JDBC这边怎么在添加之后直接获取到这个自增的值
* PreparedStatement是Statement的子接口。
* Statement接口中有一些常量值:
* (1)Statement.RETURN_GENERATED_KEYS
*
* 要先添加后获取到自增的key值:
* (1)PreparedStatement pst = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
* (2)添加sql执行完成后,通过PreparedStatement的对象调用getGeneratedKeys()方法来获取自增长键值,遍历结果集
* ResultSet rs = pst.getGeneratedKeys();
*/
public class TestAutoIncrement {
public static void main(String[] args) throws Exception{
//1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2、获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
//3、执行sql
String sql = "insert into t_department values(null,?,?)";
/*
* 这里在创建PreparedStatement对象时,传入第二个参数的作用,就是告知服务器端
* 当执行完sql后,把自增的key值返回来。
*/
PreparedStatement pst = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
//设置?的值
pst.setObject(1, "测试部");
pst.setObject(2, "测试项目数据");
//执行sql
int len = pst.executeUpdate();//返回影响的记录数
if(len>0){
//从pst中获取到服务器端返回的键值
ResultSet rs = pst.getGeneratedKeys();
//因为这里的key值可能多个,因为insert语句可以同时添加多行,所以用ResultSet封装
//这里因为只添加一条,所以用if判断
if(rs.next()){
Object key = rs.getObject(1);
System.out.println("自增的key值did =" + key);
}
}
//4、关闭
pst.close();
conn.close();
}
}
2.3 批处理
需要在url的末尾添加此参数:rewriteBatchedStatements=true
/*
* 批处理:
* 批量处理sql
*
* 例如:
* (1)订单明细表的多条记录的添加
* (2)批量添加模拟数据
* ...
*
* 不用批处理,和用批处理有什么不同?
* 批处理的效率很多
*
* 如何进行批处理操作?
* (1)在url中要加一个参数
* rewriteBatchedStatements=true
* 那么我们的url就变成了 jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
* 这里的?,表示?后面是客户端给服务器端传的参数,多个参数直接使用&分割
* (2)调用方法不同
* pst.addBatch();
* int[] all = pst.executeBatch();
*
* 注意:如果批量添加时,insert使用values,不要使用value
*/
public class TestBatch {
public static void main(String[] args) throws Exception{
long start = System.currentTimeMillis();
//例如:在部门表t_department中添加1000条模拟数据
//1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2、获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true", "root", "123456");
//3、执行sql
String sql = "insert into t_department values(null,?,?)";
PreparedStatement pst = conn.prepareStatement(sql);
//设置?的值
for (int i = 1; i <=1000; i++) {
pst.setObject(1, "模拟部门"+i);
pst.setObject(2, "模拟部门的简介"+i);
pst.addBatch();//添加到批处理一组操作中,攒一块处理
/* if(i % 500 == 0){//有时候也攒一部分,执行一部分
//2.执行
pst.executeBatch();
//3.清空
pst.clearBatch();
}*/
}
pst.executeBatch();
//4、关闭
pst.close();
conn.close();
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));//耗时:821
}
}
第3章 JDBC实现对单表数据增、删、改、查
我们要对数据库进行增、删、改、查,需要使用Statement
对象来执行SQL语句。
4.1 JDBC实现对单表数据增、删、改
案例代码
public class Demo03 {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql:///mysqltest", "root", "root");
System.out.println(conn);
// 从连接中拿到一个Statement对象
Statement stmt = conn.createStatement();
// 1.插入记录
String sql = "INSERT INTO category (cname) VALUES ('手机');";
int i = stmt.executeUpdate(sql);
System.out.println("影响的行数:" + i);
// 2.修改记录
sql = "UPDATE category SET cname='汽车' WHERE cid=4;";
i = stmt.executeUpdate(sql);
System.out.println("影响的行数:" + i);
// 3.删除记录
sql = "DELETE FROM category WHERE cid=1;";
i = stmt.executeUpdate(sql);
System.out.println("影响的行数:" + i);
// 释放资源
stmt.close();
conn.close();
}
}
案例效果
4.2 JDBC实现对单表数据查询
使用JDBC查询数据库中的数据的步骤
- 注册驱动
- 获取连接
- 获取到Statement
- 使用Statement执行SQL
- ResultSet处理结果
- 关闭资源
案例代码
public class Demo04 {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql:///mysqltest", "root", "root");
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM category;";
ResultSet rs = stmt.executeQuery(sql);
// 内部有一个指针,只能取指针指向的那条记录
while (rs.next()) { // 指针移动一行,有数据才返回true
// 取出数据
int cid = rs.getInt("cid");
String cname = rs.getString("cname");
System.out.println(cid + " == " + cname);
}
// 关闭资源
rs.close();
stmt.close();
conn.close();
}
}
注意:
- 如果光标在第一行之前,使用rs.getXXX()获取列值,报错:Before start of result set
- 如果光标在最后一行之后,使用rs.getXXX()获取列值,报错:After end of result set
案例效果
总结:其实我们使用JDBC操作数据库的步骤都是固定的。不同的地方是在编写SQL语句
- 注册驱动
- 获取连接
- 获取到Statement
- 使用Statement执行SQL
- ResultSet处理结果
- 关闭资源
第4章 JDBC事务
之前我们是使用MySQL的命令来操作事务。接下来我们使用JDBC来操作银行转账的事务。
5.1 准备数据
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(10),
balance DOUBLE
);
-- 添加数据
INSERT INTO account (NAME, balance) VALUES ('张三', 1000), ('李四', 1000);
5.2 API介绍
Connection
接口中与事务有关的方法
void setAutoCommit(boolean autoCommit) throws SQLException; false:开启事务, ture:关闭事务
void commit() throws SQLException; 提交事务
void rollback() throws SQLException; 回滚事务
5.3 使用步骤
- 注册驱动
- 获取连接
- 获取到Statement
- 开启事务
- 使用PrepareStatement执行SQL
- 提交或回滚事务
- 处理结果集
- 关闭资源
5.4 案例代码
/*
* mysql默认每一个连接是自动提交事务的。
* 那么当我们在JDBC这段,如果有多条语句想要组成一个事务一起执行的话,那么在JDBC这边怎么设置手动提交事务呢?
* (1)在执行之前,设置手动提交事务
* Connection的对象.setAutoCommit(false)
* (2)成功:
* Connection的对象.commit();
* 失败:
* Connection的对象.rollback();
*
* 补充说明:
* 为了大家养成要的习惯,在关闭Connection的对象之前,把连接对象设置回自动提交
* (3)Connection的对象.setAutoCommit(true)
*
* 因为我们现在的连接是建立新的连接,那么如果没有还原为自动提交,没有影响。
* 但是我们后面实际开发中,每次获取的连接,不一定是新的连接,而是从连接池中获取的旧的连接,而且你关闭也不是真关闭,而是还给连接池,供别人接着用。以防别人拿到后,以为是自动提交的,而没有commit,最终数据没有成功。
*/
public class TestTransaction {
public static void main(String[] args) throws Exception{
/*
* 一般涉及到事务处理的话,那么业务逻辑都会比较复杂。
* 例如:购物车结算时:
* (1)在订单表中添加一条记录
* (2)在订单明细表中添加多条订单明细的记录(表示该订单买了什么东西)
* (3)修改商品表的销量和库存量
* ...
* 那么我们今天为了大家关注事务的操作,而不会因为复杂的业务逻辑的影响导致我们的理解,那么我们这里故意
* 用两条修改语句来模拟组成一个简单的事务。
* update t_department set description = 'xx' where did = 2;
* update t_department set description = 'yy' where did = 3;
*
* 我希望这两天语句要么一起成功,要么一起回滚
* 为了制造失败,我故意把第二条语句写错
* update t_department set description = 'yy' (少了where) did = 3;
*/
//1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2、获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
//设置手动提交事务
conn.setAutoCommit(false);
//3、执行sql
String sql1 = "update t_department set description = 'xx' where did = 2";
String sql2 = "update t_department set description = 'yy' did = 3";//这是错的
//使用prepareStatement的sql也可以不带?
PreparedStatement pst = null;
try {
pst = conn.prepareStatement(sql1);
int len = pst.executeUpdate();
System.out.println("第一条:" + (len>0?"成功":"失败"));
pst = conn.prepareStatement(sql2);
len = pst.executeUpdate();
System.out.println("第二条:" + (len>0?"成功":"失败"));
//都成功了,就提交事务
System.out.println("提交");
conn.commit();
} catch (Exception e) {
System.out.println("回滚");
//失败要回滚
conn.rollback();
}
//4、关闭
pst.close();
conn.setAutoCommit(true);//还原为自动提交
conn.close();
}
}
public class Demo05 {
public static void main(String[] args) {
Connection conn = null;
try {
// 拿到连接
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql:///dayxx", "root", "root");
// 开启事务
conn.setAutoCommit(false);
Statement pstmt = conn.createStatement();
// 张三减500
String sql = "UPDATE account SET balance = balance - 500 WHERE id=1;";
pstmt.executeUpdate(sql);
// 模拟异常
// int i = 10 / 0;
// 李四加500
sql = "UPDATE account SET balance = balance + 500 WHERE id=2;";
pstmt.executeUpdate(sql);
pstmt.close();
// 成功,提交事务
System.out.println("成功,提交事务");
conn.commit();
} catch (Exception e) {
// 失败,回滚事务
try {
System.out.println("出了异常,回滚事务");
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
5.5 案例效果
**说明:**JDBC操作MySQL默认是自动提交事务的,事务不安全;可以手动开启事务管理,进行事务提交和回滚。
第5章 数据库连接池
1、什么是数据库连池
连接对象的缓冲区。负责申请,分配管理,释放连接的操作。
2、为什么要使用数据库连接池
Connection对象在每次执行DML和DQL的过程中都要创建一次,DML和DQL执行完毕后,connection对象都会被销毁. connection对象是可以反复使用的,没有必要每次都创建新的.该对象的创建和销毁都是比较消耗系统资源的,如何实现connection对象的反复使用呢?使用连接池技术实现。
3.连接池的优势
- 预先准备一些链接对象,放入连接池中,当多个线程并发执行时,可以避免短时间内一次性大量创建链接对象,减少计算机单位时间内的运算压力,提高程序的响应速度
- 实现链接对象的反复使用,可以大大减少链接对象的创建次数,减少资源的消耗
4、市面上有很多现成的数据库连接池技术:
- JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口(通常被称为数据源),该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:
- DBCP 是Apache提供的数据库连接池,速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持
- C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以
- Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
- BoneCP 是一个开源组织提供的数据库连接池,速度快
- Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池
- HikariCP是由日本程序员开源的一个数据库连接池组件,代码非常轻量,并且速度非常的快。根据官方提供的数据,在i7,开启32个线程32个连接的情况下,进行随机数据库读写操作,HikariCP 的速度是现在常用的C3P0数据库连接池的数百倍。在SpringBoot2.0 中,官方也是推荐使用 HikariCP
5. 使用HikariCP作为连接池
HikariCP特点
- 字节码精简 :优化代码(HikariCP利用了一个第三方的Java字节码修改类库Javassist来生成委托实现动态代理,动态代理的实现在ProxyFactory类),直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码;
- 优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一;
- 自定义数组类型(FastStatementList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描,相对与ArrayList极大地提升了性能,而其中的区别是,ArrayList在每次执行get(Index)方法时,都需要对List的范围进行检查,而FastStatementList不需要,在能确保范围的合法性的情况下,可以省去范围检查的开销。
自定义集合类型(ConcurrentBag):支持快速插入和删除,特别是在同一线程既添加又删除项时,提高并发读写的效率;- 针对CPU的时间片算法进行优化:尽可能在一个时间片里面完成各种操作(具体机制比较模糊)。
- 针对连接中断的情况:比其他CP响应时间上有了极好的优化,响应时间为5S,会抛出SqlException异常,并且后续的getConnection()可以正常进行。
关于Connection的操作:另外在Java代码中,很多都是在使用完之后直接关闭连接,以前都是从头到尾遍历,来关闭对应的Connection,而HikariCP则是从尾部对Connection集合进行扫描,整体上来说,从尾部开始的性能更好一些。
HikariCP配置
maximum-pool-size 池中最大连接数(包括空闲和正在使用的连接)
minimum-idle 池中最小空闲连接数量。默认值10
pool-name 连接池的名字
auto-commit 是否自动提交池中返回的连接。默认值为true。
idle-timeout 空闲时间。仅在minimum-idle小于maximum-poop-size的时候才会起作用。默认值10分钟。
max-lifetime 连接池中连接的最大生命周期。当连接一致处于闲置状态时,数据库可能会主动断开连接。
connection-timeout 连接超时时间。默认值为30s,可以接收的最小超时时间为250ms。但是连接池请求也可以自定义超时时间。
第6章 JDBC获取连接与关闭连接工具类实现
通过上面案例需求我们会发现每次去执行SQL语句都需要注册驱动,获取连接,得到Statement,以及释放资源。发现很多重复的劳动,我们可以将重复的代码定义到某个类的方法中。直接调用方法,可以简化代码。
那么我们接下来定义一个JDBCUtil
类。把注册驱动,获取连接,得到Statement,以及释放资源的代码放到这个类的方法中。以后直接调用方法即可。
6.1 编写JDBC工具类步骤
- 将固定字符串定义为常量
- 在静态代码块中注册驱动(只注册一次)
- 提供一个获取连接的方法
static Connection getConneciton();
- 定义关闭资源的方法
close(Connection conn, Statement stmt, ResultSet rs)
- 重载关闭方法
close(Connection conn, Statement stmt)
6.2 案例代码
JDBCUtil.java
package com.manman.util;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* @author Gaoziman
* @version 1.0
* description:
* @date 2022/8/15 19:19
*/
public class JdbcUtils {
public static Connection conn = null;
public static PreparedStatement ps = null;
public static ResultSet rs = null;
private static String driverClass = "com.mysql.cj.jdbc.Driver";
private static String url = "jdbc:mysql:///jdbc?serverTimezone=UTC";
private static String username = "root";
private static String password = "root";
private static HikariDataSource dataSource;
public static Properties properties =null;
static {
try {
properties = new Properties();
properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties"));
} catch (Exception e) {
System.out.println("未加载到配置文件,使用默认配置");
properties.setProperty("driverClassName", driverClass);
properties.setProperty("jdbcUrl", url);
properties.setProperty("username", username);
properties.setProperty("password", password);
}
HikariConfig hikariConfig = new HikariConfig(properties);
dataSource = new HikariDataSource(hikariConfig);
}
/**
* 获取连接
@return0000
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 回收连接
* @param conn
*/
public static void evictConnection(Connection conn){
dataSource.evictConnection(conn);
}
/**
* 查询所有的方法
* @param sql
* @param c
* @return
* @param <T>
*/
public static <T> List<T> selectList(String sql,Class<T> c) {
List<T> tlist = new ArrayList<>();
try {
conn = dataSource.getConnection();
//3、获取预编译对象
ps = conn.prepareStatement(sql);
// 4、执行SQL
rs = ps.executeQuery();
// 5、处理结果集
List tList = new ArrayList<T>();
// 获取结果集元数据
ResultSetMetaData md = rs.getMetaData();
// 获得总列数
int columnCounts = md.getColumnCount();
while (rs.next()){
// 根据反射创建对象 将每一行的数据封装到一个对象中 这里在外层通过反射创建对象
T t = c.newInstance();
for (int i = 1; i <=columnCounts ; i++) {
// 获取结果集中每一行每一列中的数据
Object cellValue = rs.getObject(i);
// 结果集中的每一列的列名 == 实体类中的属性名 这里可以通过使用反射给属性名赋值
String columnName = md.getColumnName(i);
// 根据属性名来获取属性的代理对象
Field f = c.getDeclaredField(columnName);
f.setAccessible(true);
if (cellValue != null)
f.set(t, cellValue);
}
// 将这些字段封装到对象中
tList.add(t);
}
//返回集合
JdbcUtils.evictConnection(conn);
return tList;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/**
* 增删改操作
* @param sql
* @param params
* @return
* @throws Exception
*/
public static int executeUpdate(String sql , Object... params) throws Exception {
conn = JdbcUtils.getConnection();
// 3、获取预编译对象
ps = conn.prepareStatement(sql);
//设置参数
for (int i = 0; i <params.length ; i++) {
ps.setObject(i+1,params[i]);
}
// 4、执行SQL
int i = ps.executeUpdate();
JdbcUtils.evictConnection(conn);
return i;
}
}
第7章 三层架构的设计原则
三层架构(3-tier architecture) ,通常意义上的三层架构就是将整个业务应用划分为:表示层(User Interface layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data access layer)。区分层次的目的即为了 “高内聚低耦合” 的思想。
在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构。推荐的分层式结构一般分为三层,从上至下分别为:表示层、业务逻辑层(又或称为领域层)、数据访问层。
**1、表现层(UI):**通俗讲就是展现给用户的界面,即用户在使用一个系统的时候他的所见所得。
**2、业务逻辑层(BLL):**针对具体问题的操作,也可以说是对数据层的操作,对数据业务逻辑处理。
**3、数据访问层(DAL):**该层所做事务直接操作数据库,针对数据的增添、删除、修改、查找等。
表示层
位于最外层(最上层),离用户最近。用于显示数据和接收用户输入的数据,为用户提供一种交互式操作的界面。
业务逻辑层
**业务逻辑层(Business Logic Layer)**无疑是系统架构中体现核心价值的部分。它的关注点主要集中在业务规则的制定、业务流程的实现等与业务需求有关的系统设计,也即是说它是与系统所应对的领域(Domain)逻辑有关,很多时候,也将业务逻辑层称为领域层。
例如Martin Fowler在《Patterns of Enterprise Application Architecture》一书中,将整个架构分为三个主要的层:表示层、领域层和数据源层。作为领域驱动设计的先驱Eric Evans,对业务逻辑层作了更细致地划分,细分为应用层与领域层,通过分层进一步将领域逻辑与领域逻辑的解决方案分离。
业务逻辑层在体系架构中的位置很关键,它处于数据访问层与表示层中间,起到了数据交换中承上启下的作用。由于层是一种弱耦合结构,层与层之间的依赖是向下的,底层对于上层而言是“无知”的,改变上层的设计对于其调用的底层而言没有任何影响。
如果在分层设计时,遵循了面向接口设计的思想,那么这种向下的依赖也应该是一种弱依赖关系。因而在不改变接口定义的前提下,理想的分层式架构,应该是一个支持可抽取、可替换的“抽屉”式架构。正因为如此,业务逻辑层的设计对于一个支持可扩展的架构尤为关键,因为它扮演了两个不同的角色。
对于数据访问层而言,它是调用者;对于表示层而言,它却是被调用者。依赖与被依赖的关系都纠结在业务逻辑层上,如何实现依赖关系的解耦,则是除了实现业务逻辑之外留给设计师的任务。
数据层
数据访问层(Data Access Layer):有时候也称为是持久层,其功能主要是负责数据库的访问,可以访问数据库系统、二进制文件、文本文档或是XML文档。
简单的说法就是实现对数据表的Select,Insert,Update,Delete的操作。如果要加入ORM的元素,那么就会包括对象和数据表之间的mapping,以及对象实体的持久化。
优缺点
优点
1、开发人员可以只关注整个结构中的其中某一层;
2、可以很容易的用新的实现来替换原有层次的实现;
3、可以降低层与层之间的依赖;
4、有利于标准化;
5、利于各层逻辑的复用。
6、结构更加的明确
7、在后期维护的时候,极大地降低了维护成本和维护时间
缺点
1、降低了系统的性能。这是不言而喻的。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。
2、有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码。
3、增加了开发成本。
规则
三层结构的程序不是说把项目分成DAL, BLL, WebUI三个模块就叫三层了, 下面几个问题在你的项目里面:
UILayer里面只有少量(或者没有)SQL语句或者存储过程调用, 并且这些语句保证不会修改数据?
如果把UILayer拿掉, 你的项目还能在Interface/API的层次上提供所有功能吗?
你的DAL可以移植到其他类似环境的项目吗?
三个模块, 可以分别运行于不同的服务器吗?
如果不是所有答案都为YES, 那么你的项目还不能算是严格意义上的三层程序. 三层程序有一些需要约定遵守的规则:
最关键的, UI层只能作为一个外壳, 不能包含任何BizLogic的处理过程
设计时应该从BLL出发, 而不是UI出发. BLL层在API上应该实现所有BizLogic, 以面向对象的方式
不管数据层是一个简单的SqlHelper也好, 还是带有Mapping过的Classes也好, 应该在一定的抽象程度上做到系统无关
不管使用
COM+(
Enterprise Service), 还是Remoting,
还是WebService
之类的远程对象技术, 不管部署的时候是不是真的分别部署到不同的服务器上, 最起码在设计的时候要做这样的考虑, 更远的, 还得考虑多台服务器通过负载均衡作集群
所以考虑一个项目是不是应该应用三层/多层设计时, 先得考虑下是不是真的需要? 实际上大部分程序就开个WebApplication就足够了, 完全没必要作的这么复杂. 而多层结构, 是用于解决真正复杂的项目需求的。
第8章 以用户登录为例解析三层架构逻辑实现
要以三层架构来实现一个业务,我们要从几个方面入手,按先后顺序来区分:
1、分析需求
2、拆解业务逻辑
3、确定调用关系
4、定义接口
定义接口:
表示层:
采集用户输入(输入:String用户名,String密码,
输出:user对象(包含username和password属性))
显示登录信息(输入:login对象,输出:String login.loginStatus值) 显示错误信息(输入:login对象,输出:String login.errorInfo值)
1、思考:我们用scan对象来获取用户输入,为什么接口方法不定义成
(输入:scan对象或无输入对象,输出:user对象)?2、思考:显示登录信息的输出,为什么不用login对象,而是要输出login.loginStatus的值?
逻辑层:
校验用户密码,生成登录状态
(输入:user对象,
输出:login对象(包含loginStatus和errorInfo属性)) 判断是否允许登录
(输入:login对象,
输出:boolean)3、思考:校验用户密码方法的输入为什么不定义成接受拼好的查询SQL ?
数据访问层:
查询用户密码
(输入:user对象,
输出:String password) 如果没查到用户,返回null,否则返回数据库中 存的密码。) 判断是否允许登录
(输入:login对象,
输出:boolean)4、思考:查询用户密码,为什么不直接返回是否登录成功的true或false ?
思考解答:
1、考虑封装的通用性,scan是命令行实现,如果是web实现,则需要使用request对象,这样接口就做不到通用
2、输出也要考虑通用性,如果输出login对象,那么需要在表现层的其他方法或更上层中拆解login对象。如果是web页面带模板解析,还能拆解对象,但是命令行的展现终端是System.out,不具备拆解能力。
3、如果在逻辑层拼好sql,那么数据访问层只能使用数据库,不能适应文本文件存储底层的情况。
4、如果使用单点登录等框架,还可能需要调用远程方法来判断登录,这样可以预留实现空间。
三层架构功能要区分清晰,不要混淆。要尽量考虑可扩展性,哪怕牺牲复杂度。
第9章 数据访问层DAO实现
数据访问层概念
数据持久化目标:
可以是数据库,也可以是普通文件,或xml。
还可以是云端设备等,设计数据访问层的一个目的就是
可以广泛适应各种持久化设备,把设计复杂度屏蔽在层的内部,抽取出简单的接口供上层使用。
如果不采用持久层设计,则导致业务代码与数据访问代码紧密耦合,可读性差,不利于后期修改和维护,不利于代码复用。
采用数据访问层后,可以把数据访问代码封装在层内部,从而对上层调用屏蔽实现细节,提高系统适应性,提供代码复用率。
通过针对数据访问层接口编程,可以在同一接口下实现不同数据库的持久类,从而简化上层调用,提高系统可扩展性。
什么是DAO
1)、DAO全称是(Data Access Objects) ,数据库访问对象,主要的功能就是用于进行数据操作的,在程序的标准开发架构中属于数据访问层的操作。
2)、DAO层一般有接口和该接口的实现类! 接口用于规范实现类! 实现类一般用于用于操作数据库! 一般操作修改,添加,删除数据库操作的步骤很相似,就写了一个公共类DAO类 ,修改,添加,删除数据库操作时 直接调用公共类DAO类!
DAO设计模式可以减少代码量,增强程序的可移植性,提高代码的可读性。在整个DAO中实际上都是以接口为操作标准的,即:客户端依靠DAO实现的接口进行操作,而服务端要将接口进行具体的实现。DAO由以下几个部分组成。
1)、VO实体类:主要由属性、setter、getter方法组成,VO类中的属性与表中的字段相对应,每一个VO类的对象都表示表中的每一条记录,即包含属性和表中字段完全对应的类。
2)、DAO接口:主要定义操作的接口,定义一系列数据库的原子性操作,例如:增加、修改、删除、按ID查询等,即提供了用户所有的操作方法(就如老师给学生提供一些学习方法)。
3)、Impl(DAO实现类): DAO接口的真实实现类,完成具体的数据库操作,但是不负责数据库的打开和关闭,即实现DAO中所有的方法(就如老师给提供的方法看你如何去完成);
4)、DatabaseConnection数据库连接类: 专门负责数据库操作的类,即连接数据库并获取连接对象,执行SQL。
如何实现数据访问层
实现实体类
实体类的特征:
1.属性一般用private修饰(getter/setter方法——用public修饰);
2.最好实现java.io.Serializable接口支持序列化机制,可以将该对象转换成字节序列而保存在磁盘上或在网络上传输;
3.对实体类提供无参构造方法,根据业务需要提供相应的有参构造方法;
4.定义属性serialVersionUID,解决不同版本之间的序列化问题(可选)
(private static final long serialVersionUID=2070056025956126480L)
其次要实现接口类
接口类的定义要根据业务需求,在逻辑层的调用需求,都需要在接口层定义相应的接口。
只要是实现了此接口的实现类,都可以在逻辑层被正常调用,这样就实现了底层实现类更新扩展替换,而不影响到逻辑层代码的目的。
然后要实现接口的实现类
接口的实现类具体实现接口逻辑,根据底层存储的不同,可以有多种不同的实现类,每个实现类对应一种底层存储。最后要实现数据库操作工具类。避免数据库连接和关闭代码的重复使用,方便修改。