文章目录
第一章:JDBC在整个开发中的地位
1.1 数据的持久化
- 持久化:把数据保存到可掉电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以”固化“,二持久化的实现过程大多通过各种关系数据库来完成
- 持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中
1.2 java中的数据存储技术
- 在java中,数据库存储技术可分为如下几类
- JDBC直接访问数据库
- JDO(Java data object)技术
- 第三方O/R工具,如hibernate,mybatis等。
- 值得注意的是,JDBC是java访问数据库的基石,JDP、Hibernate、MyBatis等只是更好的封装了JDBC。
1.3 JDBC介绍
- JDBC是一个独立于特定数据库管理系统、通用的的SQL数据库存取和操作的公共接口(一组API),定义了用来访问数据库的标准java类库,(java.sql,javax.sql)使用这些类库可以以一种标准的方法,方便的访问数据库资源。
- JDBC为访问不同的数据库题哦你过来一种统一的途径,为开发者屏蔽了一些细节问题
- JDBC的目标是使Java程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,这样就是的程序员无需对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程。
- 如果没有JDBC,程序员将会需要直接访问各种数据库,但由于不同数据库对于数据的增删改查操作规范的设计有所不同,因此,直接访问的方法虽然可以执行,但程序的复用性太差,可移植性太低,实际开发过程中并不推荐。
简单理解为:JDBC是SUN公司提供的一套API,使用这套API可以实现对具体数据库的操作(获取链接、关闭连接、DML、DDL、DCL)
1.4 JDBC的体系架构
由上图可知JDBC接口的两个层次:
- 面向应用的API:Java API,抽象接口,供应用程序员开发使用(连接数据库,执行SQL语句,获得结果)
- 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用
设计两个层次的好处
-
从开发程序员角度:不需要关注具体的数据库细节。JDBC是sun公司提供的一套用于数据库操作的接口,java程序员只需要面向这套接口编程即可——面向接口编程思想。
-
从数据库厂商角度:只需要提供标准的具体实现。不同的数据库厂商,需要针对这套接口提供不同实现,不同实现的组合,即为不同数据库的驱动,这也就是面向接口编程。
说明:
- 数据库的驱动——数据库厂商针对于JDBC这套接口,提供的具体实现类的结合
1.5 JDBC程序编写步骤
由于jdk中以及包含了java.sql
的相关组件,重要的是导入第三方的jar包。这里我们使用的是Oracle公司提供的驱动
Step1:导入java.sql包,获取java中关于数据库操作的有关类
Step2:获取不同厂商提供的驱动(Driver接口)的实现类driver对象
Step3:调用driver对象的连接方法connection(需要填写url地址,这里构建一个property对象来保存填写的信息)
第二章:数据库的连接
2.1 连接要素一:Driver接口实现类
2.1.1 Driver接口
Driver接口是java.sql
提供的接口接口,其内部定义了获得数据库连接的方法
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
// ...
}
采用mysql的Driver接口实现驱动
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
上述代码是mysql中Driver接口实现类的源码,mysql共色的接口实现驱动内部实际是一个静态代码块,内部通过DriverManager
对该驱动进行了注册,注册完毕后即可调用对应driver类对象的方法进行进一步信息填写与获取链接。
2.1.2 加载与注册JDBC驱动
可以看到,实际上mysql提供的driver类已经完成了有关接口的登记,因此直接就可以进入填写资料部分
2.2 连接要素二:统一资源定位符URL
由于数据库一般是某个网络下的一个软件,获取其的方法一般包含URL,
String url = "jdbc:mysql://121.196.148.142:3310/test";
规则如下:
// url:http://localhost:8080/gmall/keyboard.jpg
// jdbc:mysql:协议
// localhost:ip地址
// 3306:默认mysql的端口号
// test:test数据库
一般会用properties
来存粗有关信息,这是因为其一般是健值对存取数据,十分方便。
2.3 用户名和密码
利用Properties
来存储用户名和密码
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "gjs132008");
一般而言,我们会讲用户密码,url接口等登陆信息封装在properties文件中,并在连接时再获取有关信息并进行加载
# 获取四个配置信息,且=之间不能有空格
user=root
password=gjs132008
url=jdbc:mysql://121.196.148.142:3310/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
driverClass=com.mysql.jdbc.Driver
读取过程如下
Step1:调用当前类的类加载器对象,获取对应的properties
对象作为输入流构建对象is
InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("src/jdbc1/jdbcUtils/jdbc.properties");
Step2:开辟一个Properties
对象获取并存取上述的重要信息
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
2.4 获取连接的完整流程
获取一个连接的完整流程总共分为3步
Step1:读取配置文件中的基本信息
Step2:加载驱动
Step3:获取连接
源代码如下;
@Test
public void testConnection5() throws Exception {
//1.读取配置文件中的基本信息
InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("src/jdbc1/jdbcUtils/jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
// 2.加载驱动
Class.forName(driverClass);
// 3.获取连接
Connection connection = DriverManager.getConnection(url, user, password);
}
2.5 关闭数据库连接
和文件中的关闭文件类似,获取了连接后如果结束使用也应当将其获取的连接关闭,关闭源码如下
conn.close();
2.6 构建获取链接与关闭连接的工具类
由于获取数据库连接与关闭数据库连接都是会反复使用的,因此可以将构建连接与关闭连接封装进工具类的成员方法,工具类如下:
工具类的构建是基于如下考虑:每次执行数据库操作时都需要获取数据库连接,执行完操作后需要关闭数据库连接,这些操作如果和其他数据库操作写在一起就会很冗余,因此考虑将其写成一个工具类的静态方法,供需要时调用。工具类中应当包含以下两部分
-
获取数据库连接
/** * 获取连接 * @return 一个数据库的连接 * @throws Exception */ public static Connection getConnection() throws Exception { // 1.获取连接 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties"); Properties pros = new Properties(); pros.load(is); String user = pros.getProperty("user"); String password = pros.getProperty("password"); String url = pros.getProperty("url"); String driverClass = pros.getProperty("driverClass"); // 2.加载驱动 Class.forName(driverClass); // 3.获取连接 Connection connection = DriverManager.getConnection(url, user, password); return connection; }
-
关闭数据库连接
- 关闭没有结果集的数据库连接:
** * 关闭库资源操作 */ public static void closeResource(Connection conn, Statement ps) { try { // 避免空指针(对象没有创建的时候就关闭) if (conn != null) conn.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } try { // 避免空指针(对象没有创建的时候就关闭) if (ps != null) ps.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } }
- 关闭有结果集的数据库连接
public static void closeResource(Connection conn, Statement ps, ResultSet resultSet) { try { // 避免空指针(对象没有创建的时候就关闭) if (conn != null) conn.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } try { // 避免空指针(对象没有创建的时候就关闭) if (ps != null) ps.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } try { // 避免空指针(对象没有创建的时候就关闭) if (resultSet != null) resultSet.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } }
第三章 sql语句的实现
3.1 Statement 实现sql语句查询
3.1.1 出现的问题与原因
- 拼接字符串——麻烦
- SQL注入
- Statement无法操作Blob类型数据
- 实现批量插入时效率较低
3.2 PreparedStatement实现
是Statement的字接口
特点:预编译SQL语句
3.2.1 增删改操作
以修改数据为例子
public int update(String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 1. 获取数据库连接
conn = JDBCUtils.getConnection();
// 2. 预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
// 3. 填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]); //小心参数声明错误,第一个是sql的需要+1,第二个是java中的
}
//方式一: 4. 执行
// ps.execute();
//方式二: 5. 对行数有诉求的
return ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭
JDBCUtils.closeResource(conn, ps);
}
return 0
}
增删改的方式一:ps.execute()
:
- 如果执行的是查询操作,有返回结果,则此方法返回
true
; - 如果是增删改操作,没有返回结果,则此时返回
false
增删改方式二:ps.executeUpdata()
:
- 返回一个数,该数值为修改所涉及的条目的个数。例如修改了4条数据,就返回4。
因此可以利用下面的代码段进行添加的信息回执
int insertCount = update(sql,arg0,arg1...){
if(insertCount>0){
System.out.println("添加成功!");
}
}
3.2.2 查询操作
增删改操作在执行完语句后,没有结果集合,只会返回一些数字性的信息。例如,影响数据xxx行。而如果是查询操作,则会返回查询结果,并且查询结果会因为数据库查询内容不同而不同。在JDBC中,将执行查询操作返回的内容放在结果集resultSet
中,把结果集合的特性信息放在结果集元数据中ResultSetMetaData
,结果集元数据可以通过resultSet.getMetaData()
方法获取。
结果集:
在prepareStatement
中,使用ps.executeQuery()
语句可以获得查询结果
resultSet = ps.executeQuery();
结果集元数据:
通常结果集元数据也会一并取出
ResultSetMetaData rsmd = resultSet.getMetaData();
当结果集和元数据取出来后,就可以根据结果的特性遍历输出想要的数据了
通用的工具类:当执行数据库的增删改操作时,关闭数据库时只需要传入conn
和statement
语句。而如果是执行查询操作,还需要传入resultSet
。
-
首先需要了解的是
Statement
和PreparedStatement
都是sun公司关于JDBC接口中定义的一套规范,不是第三方的;其次,后者是前者的子接口。 -
Statement
相当于信使,用于将java中的sql语句再数据库中执行 -
我们在开发中都不会去使用
Statement
,都会使用后者来实现对数据库的增删改查操作。这是因为Statement
有两个弊端- 问题一:存在拼串操作:繁琐
- 问题二:存在SQL注入问题
正因为上述两个问题,我们才需要使用
PreparedStatement
来替换Statement
-
此外,使用
PreparedStatement
能够实现对blob
文件的操作,这在Statement
中是做不到的。因为前者在预编译的时候会存在占位符?
,对于blob
类型的文件可以使用流的方式进行操作,而Statement
由于已经写好了sql语句,所以无法实现该过程 -
使用
PreparedStatement
能够更加高效的实现批量插入数据。因为PreparedStatement
中存在预编译过程- DBserver会对预编译语句提供性能优化,因为预编译语句有可能被重复调用(如下方代码所示),所以语句在编译后的执行结果应当被缓存下来(也就是下面代码的
ps=conn.prepareStatement(sql)
),那么下次调用的时候只要是相同的预编译语句就不需要再次编译,只要将参数直接传于编译过的语句中执行即可,因此得到了优化后的PreparedStatement语句。
String sql = "insert into goods(name)value(?)"; ps = conn.prepareStatement(sql); for (int i = 0; i < 20000; i++) { ps.setObject(1,"name_"+i); ps.execute(); }
- 而在
Statement
语句中,即使是相同的操作,但因为数据内容不一样,所以整个语句本身不能匹配,没有缓存语句的意义,事实是没有数据库会读普通语句编译后的执行代码缓存。这样每执行一次都要对传输的数据编译一次,也就是如下代码
Statement st = new conn.createStatement(); for(int i=0; i<=20000; i++){ String sql = "insert into goods(name)value('name_"+ i +"')"; st.execute(sql); }
- DBserver会对预编译语句提供性能优化,因为预编译语句有可能被重复调用(如下方代码所示),所以语句在编译后的执行结果应当被缓存下来(也就是下面代码的
3.3 知识小结
关于 javaBean 成员变量与数据库中数据项名不一致的问题
在数据库相关的编程中有一个很重要的思想是ORM思想,即要求一个表对应pojo中的一个类,表中的每一项对应该类的每一个属性。但数据库中的表以及表项的命名规则和java类中成员变量的规则并不相同。而在DAO中,需要根据数据库中表项名称,配合反射机制构建JavaBean对象。
因此这里就需要完成从表项名到类的成员变量名的映射,这就要求我们在使用sql语句时,给获取的结果构建别名。例如下图数据库表:
该表表名与类中的两个属性并不一致
public class Order {
private int orderId;
private String orderName;
}
因此在调用sql语句的时候,需要对其起别名代码如下:
select order_id orderId, order_name orderName from `order` where order_id = ?
这样就可以获取结果集中的别名
String columnLabel = rsmd.getColumnLabel(i + 1);
JDBC API 涉及到的两种思想
-
两种思想
-
面向接口编程思想:在写任何类和方法的时候都不会引入第三方API
- 我们最终使用的是sun公司实现的JDBC接口的实现类对象的重写的方法,看到的都是接口调方法,是mysql接口实现类的执行
-
ORM思想(object relational mapping)
- 一个数据表对应一个java类
- 表中的一条记录对应java类的一个对象
- 表中的一个字段对应java类中的一个属性
sql是需要结合列名和表的属性名来写,注意要起别名
-
-
两种技术
- JDBC结果集的元数据
ResultMetaData
- 获取列数:
getColumnCount()
- 获取列的别名:
getColumnLabel()
- 获取列数:
- 通过反射,创建指定类的对象,获取指定的属性并赋值
- JDBC结果集的元数据
最终小结
以上内容总结来说就涉及3个操作
- 数据库的连接与相关资源关闭
- 增删改操作(通用增删改)
- 查询操作(单一查询与序列化查询)——返回实例对象
3.4 常见问题
在idea中往数据库中插入数据时,如果中文会出现?
答:这是因为在编码数据的时候没有进行配置,于是需要在properties文件中配置如下信息:
useUnicode=true&characterEncoding=utf-8&useSSL=false
前面一部分时设置统一编码,中间是设置utf-8格式,最后是不进行SSL验证,这是在数据库连接中除开那三个要素之外的其他要素。
为什么在前几章节中都要用try…catch而不用throws?
- 这是因为希望只要程序在某个地方出现了故障就立马到达catch的部分,而不是继续进行。
第四章 操作Blob类型的字段
预留问题:关于sql中文件太大无法上传的问题——需要修改mysql的配置文件
4.1 MySQL BLOB类型数据
在mysql中,blob是一个二进制大型对象,是一个可以存储大量数据的容器,其能容纳不同大小的数据。这也是数据库的现实需要,因为数据库除了存储数据信息外,还需要存取一些用户的诸如图片等非表格型信息。BLOB类型的作用就在于此。
插入BLOB类型的数据必须使用PreparedStatement
,这是因为BLOB类型的数据无法使用字符串拼接去写(因为文件信息无法写),只能通过设置参数值完成。
MySQL中有四种BLOB类型的数据(四种的类型的区别仅在于大小的不同)
类型 | 大小(单位:字节) |
---|---|
TinyBlob | 最大 255k |
Blob | 最大 65K |
MediumBlob | 最大 16M |
LongBlob | 最大 4G |
4.2 向数据库中插入大数据类型
以向数据库中添加某个用户信息为例,对应的SQL语句如下
insert into user_info(id,name,birthday,photo)values(?,?,?,?)
其中前三个属性是数值型数据,第四个为文件,因此在调用PreparedStatement
并设置参数时,第四个参数位置应当赋予一个文件输入流,代码步骤如下
ps = conn.prepareStatement(sql);
ps.setObject(1, "2");
ps.setObject(2, "张三");
ps.setObject(3, "1998-01-11");
// 先通过路径获取文件,然后将文件构造成输入流
FileInputStream is = new FileInputStream(new File("src/jdbc4/blob/张三.jpg"));
ps.setBlob(4, is);
由此便实现了了图片数据的插入
说明:在向数据库中实现插入的时候,可能会出现文件太大插入失败的问题需要修改my.ini
文件的配置信息,并重启数据库
4.3 从数据库中读取大数据类型
和写入文件类似,通过调用连接的preparedStatement
方法执行sql语句即可实现大型数据的查询,sql语句如下:
select id, name, birthday, photo from user_info where id = ?
其中,文件类型的数据需要以流的方式构建一个File
类对象累存储至本地,具体需要调用接口类Blob实现类对象的如下方法,即可获取输入流:
java.io.InputStream getBinaryStream () throws SQLException;
具体步骤见如下代码:
// 1.获取该Blob
Blob photo = rs.getBlob("photo");
// 2.将该Blob文件写成输入流格式
is = photo.getBinaryStream();
// 3.在本地构建一个文件
fos = new FileOutputStream("com/jdbc4/blob/034162.jpg");
// 4.将内存中得到的输入流 写入 本地文件中
byte buffer[] = new Byte[1024];
int len;
while((len=is.read(buffer)!=-1){
fos.write(buffer,0,len);
}
4.4 ⭐️ Statement 与 PreparedStatement 的对比(结合了前五章的内容)
第五章 批处理表单数据
注意,使用update
,delete
等方法时,本身就有批量操作的效果,因此本章节所讲的内容主要是批量插入。
5.1 批处理方式一:使用Statement批处理
Connection conn = JDBCUtils.getConnection();
Statement st = new conn.createStatement();
for(int i=0; i<=20000; i++){
String sql = "insert into goods(name)value('name_"+ i +"')";
st.execute(sql);
}
5.2 批处理方式二:使用PreparedStatement批处理
@Test
public void testInsert1() {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCUtils.getConnection();
String sql = "insert into goods(name)value(?)";
ps = conn.prepareStatement(sql);
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
ps.execute();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, ps);
}
5.3 批处理方式三:使用batch批处理
说明:mysql默认是关闭批处理的的,我们需要配置一个参数,让mysql开启批处理支持:
?rewriteBatchedStatements=ture
// 批量插入方式三,使用batch
@Test
public void testInsert2() {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCUtils.getConnection();
String sql = "insert into goods(name)value(?)";
ps = conn.prepareStatement(sql);
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
// 1. 攒sql
ps.addBatch();
if (i % 500 == 0) {
// 2. 执行攒了一部分的sql
ps.executeBatch();
// 3. 删除以及执行的batch
ps.clearBatch();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, ps);
}
5.4 批处理方式四:不允许自动提交数据
// 批量插入方式四,设置不允许自动提交数据来对执行速度做优化
@Test
public void testInsert3() {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCUtils.getConnection();
// 设置 不允许自动提交,需要等到所有操作都结束后再提交
conn.setAutoCommit(false);
String sql = "insert into goods(name)value(?)";
ps = conn.prepareStatement(sql);
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
// 1. 攒sql
ps.addBatch();
if (i % 500 == 0) {
// 2. 执行攒了一部分的sql
ps.executeBatch();
// 3. 删除以及执行的batch
ps.clearBatch();
}
}
// 最后再统一提交
conn.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, ps);
}
第六章 数据库事务
6.1 数据库事物介绍
-
事务:一组**逻辑操作单元,使数据从一种状态变换到另一种状态**。
-
一组操作单元:一行或多行DML操作。例如转账:两个DML操作就是一组操作单元
update user_table set balance = balance - 100 where user='AA' update user_table set balance = balance + 100 where user='BB'
-
-
事务处理(事务操作原则)****:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),要么这些修改就永久地保存下来;要么数据库管理系统将放弃所做的所有修改**,整个事物**回滚(rollback)**到最初状态。
- ⚠️:数据一旦提交,就无法回滚
- 那些操作会导致数据的自动提交?
- DDL操作一旦执行,都会自动提交;
set autocommit = false
对操作失效; - DML默认情况下,一旦执行,就会自动提交;但我们可以通过
set autocommit = false
的方式取消DML操作的自动提交; - 默认在关闭连接的时候也会自动提交
- DDL操作一旦执行,都会自动提交;
-
为确保数据库中数据的一致性,数据的操作应当是离散的成组的逻辑单元:当它们全部完成时,数据的一致性得以保持,而当这个单元中的一部分操作失败,整个事务应当视为错误,所有从起点以后的操作应当全部退回到开始状态。
// 未考虑数据库事务的转账操作
{
@Test
public void testUpdate(){
String sql1 ="update user_table set balance = balance - 100 where user= ?";
update(sql1,"AA");
String sql2 ="update user_table set balance = balance + 100 where user= ?";
update(sql2,"BB");
System.out.println("转账成功");
}
public int update(String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 1. 获取数据库连接
conn = JDBCUtils.getConnection();
// 2. 预编译sql语句,返回PreparedStatement的实例
ps = conn.prepareStatement(sql);
// 3. 填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
// 4. 执行
return ps.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 关闭
JDBCUtils.closeResource(conn, ps);
}
return 0;
}
}
- 当使用上述代码进行转账是,一旦第6~7行间出现中断,就会造成数据库中数据的不一致。同时由于
update
函数是执行一条语句就关闭一个连接,根据上文,会出现提交操作,因此此时回滚也无法回到最初状态,因此需要对上述update
代码部分进行修改。
6.2 JDBC事务处理
基于上套路,为了在JDBC程序中让多个SQL语句作为一个事务执行:
-
调用
Connection
对象的setAutoCommit(false)
;取消对事务的自动提交 -
在所有SQL语句都成功执行后,调用
commit();
方法提交事物 -
在出现异常时,调用
rollback();
方法进行回滚若此时Connection没有关闭,还可能被重复使用,则需要恢复自动提交状态,应当设置
setAutoCommit(true)
,尤其是在使用数据库连接池技术时,执行close()
方法前,建议恢复自动提交状态。
6.3 事务的ACID属性
- 原子性(Atomicity)
- 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency)
- 事务必须使数据库从一个一致性状态转换到另一个一致性状态。
- 隔离性(Isolation)
- 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能相互干扰
- 持久性(Durability)
- 持久性是指一个事务一旦被提交,它对数据库中数据的更改就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
6.3.1 数据的并发问题
对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采取必要的隔离机制,就会导致各种并发问题:
- 脏读:对于两个事务T1,T2,T1读取了已经被T2更新但还没有提交的字段。之后T2回滚,T1读取的内容就是临时且无效的(读到了脏东西)。
- 不可重复读:对于两个事务T1、T2,T1读取了一个字段,然后T2更新了该字段。之后T1在此读取同一个字段,值就不同了(重复读时数据不同)。
- 幻读:对于两个事务T1、T2,T1从表中读取了一个字段,然后T2在表中choral一些行。之后,如果T1在此读取同一个表,就会多出几行(出现幻觉了?)。
数据库事务的隔离性:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。
隔离级别:一个事务与其他事务隔离的程度称为隔离界别。数据库规定了多种事务隔离级别,不同隔离界别对应不同的干扰程度。隔离级别越高,数据一致性就越好,但并发性越弱。
⭐️最需要处理的是脏读问题,幻读在实际开发过程中不太影响用户体验。
6.3.2 四种隔离级别
是关于上述三种问题分别处理到什么程度而划分的级别
隔离级别 | 描述 |
---|---|
READ UNCOMMITTED (读未提交数据) | 允许事务读取未被其他事务提交的变更。脏读,不可重复读和幻读的问题都会出现 |
READ COMMITED (读已提交数据) | 只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可重复读和幻读问题仍然可能出现 |
REPEATABLE READ (可重复读) | 确保事务可以多次从一个字段中读取相同的值。在这个事务持续期间,禁止其他事务对这个字段进行更新, 可以避免脏读和不可重复读,但幻读问题仍然存在 |
SERIALIZABLE(串行化) | 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入,更新和删除操作, 所有并发问题都可以避免,但性能十分低下 |
- Oracle支持的两种事务隔离级别:READ COMMITED,SERIALIZABLE。Oracle默认的事务隔离级别为:READ COMMITED
- Mysql支持4种事务隔离界别,其默认的事务隔离级别为:REPEATABLE READ
上述四种隔离级别,越往下并发性越差,一致性越好
6.3.3 在mysql中设置隔离级别
-
每启动一个mysql程序,就会偶的一个单独的数据库连接,每个数据库连接都有一个全局变量
@@tx_isolatio
,表示当前的隔离事物级别 -
查看当前的隔离级别
SELECT @@tx_isolatio
-
设置当前mySQL连接的隔离级别:
set transaction isolation level read committed;
-
设置数据库系统的全局的隔离级别:
set global transaction isolation level read committed;
-
补充操作:
-
创建mysql数据库用户:
create user tom identified by 'abc123';
-
授予权限
# 授予通过网络方式登陆的tom用户,对所有苦所有表的全部权限,密码设为abc123 grant all privileges on "." to tom@'%' identified by 'abc123'; # 给tom用户使用本地命令行方式,授予xxxxx库下的所有表的插删改查的权限 grant select,insert,delete,update on xxxxx.* to tom@localhost identified by 'abc123';
-
在java代码中实现上述过程
略
第七章 DAO及相关实现类
-
DAO:Data Access Object访问数据信息的类和接口,包括了对数据的CRUD(Create、Retrival、Update、Delete),而不包含任何业务相关的信息,有时也被称作:BaseDAO
-
作用:为了实现功能的模块化,更有利于代码的维护和升级
-
例如下方是针对于本机上的user表单的DAO使用的体现
其中至少应当包含以下三种文件:
-
Bean
文件:用于存储数据表所构成的对象。表中的每一项都是改对象的成员变量。 -
BaseDAO
:其利用泛型封装了关于各种表单的基本操作;(这里使用泛型是因为有些表单的操作具有共性,如果某个表单具有其特殊的方法,可以再写一个abstract
类继承BaseDAO
类)如下图所示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmCNhxCs-1614307035078)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JDBC/image-20210201155422477.png)]
-
DAOimpl
:关于上述抽象父类的实现类,利用父类BaseDAO
提供的一些方法,进一步完成有关操作的封装,使得只需要根据外界传入的个别重要信息即可实现对数据库的增删改查:具体代码如下所示 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nqOS11nF-1614307035079)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JDBC/image-20210201160022159.png)]
-
另外:关于对于每一个写好的DAO,都应在test中存放相应的测试单元,存放格式如下图所示:
⭐️ 重要小结
考虑数据库事务后的操作
-
获取数据库连接
- 手动连接
Connection conn = JDBCUtils.getConnection();
- 数据库连接池(为了体现事务,需要加上如下代码)
Conn.setAutoCommit(false)
-
如下的多个DML操作,作为一个事务出现:(数据库操作可以手动使用
PreparedStatement
实现,也可以使用dbutils.jar
中的QueryRunner
类)操作1:需要通用的增上改查操作
操作2:需要通用的增上改查操作
操作3:需要通用的增上改查操作
conn.commit();
-
如果出现异常,则回滚
conn.rollback();
-
关闭资源
JDBCUtils.closeResource(..,..,..)
第八章 数据库连接池
8.1 JDBC数据库连接池的必要性
在开发基于数据库的web程序时,传统模式将按照如下流程执行
- 在主程序中建立数据库连接(
servlet
或者bean
中) - 执行sql操作
- 断开数据库连接
如果使用上述模式,怎会存在如下的问题:
- 普通的JDBC数据库连接会加载驱动使用
DriverManager
来获取连接,每次建立连接时都会花费很多时间,获取的连接在执行完语句后又会很快被关闭,这造成了大量资源和时间的浪费。数据连接的资源并没有很好被利用。频繁地进行数据库连接操作会占用很多系统资源。 - 对于每一次数据库连接,使用完成后都得关闭,否则如果程序出现异常未能关闭,将会导致数据库系统中内存泄漏。
- 这种开发并不能控制被创建的连接对象数目,系统的资源会毫无顾忌的被分配出去,如果连接过多,也可能导致内存泄漏,服务器崩溃。
8.2 数据库连接池技术
因此,使用数据库连接池技术就是为了克服上述开发模式的缺点,数据库连接池的基本思想如下:
为数据库连接建立一个”缓冲池“。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接的时候,只需要从”缓冲池“中取出,用完再放回即可。
数据库连接池在初始化时将创建一定数量的数据库连接放入池中,这些数据库连接是由最小数据库连接数量设定。无论这些数据库连接池是否被使用,都会有至少这些数量的连接。同理,还有最大数据库连接数量,设定了一个数据库连接池最多有多少数量的连接,当又多余该数量的请求时,多余的请求会被加入到等待队列中。
工作原理如下图所示:
数据库连接池技术的优点:(资源利用率,系统响应时间,动态分配,统一管理)
- 资源重用率高:由于数据库的连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,增加了系统运行环境的平稳性。
- 更快的系统反应速度:数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接到初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统响应时间。
- 新的资源分配手段:对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大空用数据库的限制,避免某一应用独占所有数据库资源
- 统一连接管理,避免数据库连接泄漏:在较为完善的数据库连接池实现中,可根据预先的占用时常设定,强制回收被占用的连接,从而避免了常规数据库连接操作中可能出现的资源泄漏
8.3 多种开源的数据库连接池
JDBC的数据库连接池使用javax.sql.DataSource
来表示,DataSource
只是一个接口,该接口通常由服务器提供实现,也有开元组织提供实现:
- DBCP是Apache提供的数据库连接池。tomcat服务器自带dbcp数据库连接池。速度相对于c3p0较快,但自身存在BUG,Hibernate3已经不再提供支持
- C3P0是一个开元组织提供的数据库连接池,速度相对较慢,性能过得去,hibernate官方推荐使用
- BoneCP是一个开元组织提供的数据连接池,速度快
- Druid是阿里提供的数据库连接池,据说是集DBCP、C3P0、Proxool优点于一身的数据库连接池,后面也经常使用该数据库连接池
DataSource
通常被称为数据源,其包含连接池和连接池管理两部分,习惯上也经常把DataSource
成为连接池
8.3.1 C3P0数据库连接池
了解即可
8.3.2 DBCP数据库连接池
了解即可
8.3.3 Druid(德鲁伊)数据库连接池
以后使用的更多是这种数据库连接池,直接调用相关库方法即可。学习了上面的内容之后,该数据库中相关API就能很快理解上手了。
总结
JDBC学习是之后学习框架技术的基础,虽然有很多已过时的技术,但学习这些对了解JDBC底层如何运作,其发展历程大有裨益,所以个人认为JDBC底层技术还是很有了解的必要的。