JDBC核心技术
一、概述
1. 数据的持久化
- 持久化:把数据保存到可掉电式存储设备中以供之后使用。数据持久化意味着将内存中的数据保存到硬盘上加以“固化”,而持久化的实现过程大多通过各种关系数据库来完成。
- 持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中。
2. Java中的数据存储技术
- 在Java中,数据存取技术可分为如下几类:
- JDBC直接访问数据库
- JDO(Java Data Object)技术
- 第三方O/R工具,如Hibernate,Mybatis等
- JDBC是Java访问数据库的基石,JDO、Hibernate、MyBatis等只是更好的封装了JDBC
3. JDBC介绍
- JDBC(Java Database Connectivity)是一个独立于特定数据库管理系统、通用的SQL数据库存取数据和操作的公共接口(一组API),定义了用来访问数据库的标准Java类库,(Java.sql,Javax.sql)使用这些类库可以以一种标准的方法、方便的访问数据库资源。
- JDBC为访问不同的数据库提供了一种统一的途径,为开发者屏蔽了一些细节问题
- JDBC的目标是使Java程序员使用JDBC可以连接任何提供JDBC驱动程序的数据库系统
- 若没有JDBC则Java程序访问数据库时是这样的:
- JDBC,Java访问数据库时:
4. JDBC体系结构
- JDBC接口(API)包括两个层次:
- 面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库、执行SQL语句、获得结果)
- 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序使用
5. JDBC程序编写步骤
二、获取数据库连接
public class ConnectionTest {
/**
* 方式一
* @throws SQLException
*/
@Test
public void testConnection1() throws SQLException {
//1、获取Driver的实现类对象
Driver driver=new com.mysql.jdbc.Driver();
//2、提供要连接的数据库:stu为其中的一个数据库名
String url="jdbc:mysql://localhost:3306/stu";
//3、封装用户名和密码
Properties info=new Properties();
info.setProperty("user","root");
info.setProperty("password","123456");
Connection conn = driver.connect(url, info);
System.out.println(conn);
}
/**
* 方式二:对方式一的迭代:在如下的程序中不出现第三方的API,使程序具有更好的可移植性
*/
@Test
public void testConnection2() throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException {
//1、获取Driver实现类对象
Class aClass = Class.forName("com.mysql.jdbc.Driver");
Driver driver= (Driver) aClass.newInstance();
//2、提供需要连接的数据库
String url="jdbc:mysql://localhost:3306/stu";
Properties info=new Properties();
info.setProperty("user","root");
info.setProperty("password","123456");
//4、获取连接
Connection conn = driver.connect(url, info);
System.out.println(conn);
}
/**
* 方式三:迭代方式二:使用DriverManger替换Driver
* @throws Exception
*/
@Test
public void testConnection3() throws Exception {
//1、获取Driver实现类对象
Class aClass = Class.forName("com.mysql.jdbc.Driver");
Driver driver= (Driver) aClass.newInstance();
//2、获取基本信息
String url="jdbc:mysql://localhost:3306/stu";
String user="root";
String password="123456";
//3、注册驱动
DriverManager.registerDriver(driver);
//4、获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
}
/**
* 方式四:只加载驱动,不用显式注册驱动
* @throws Exception
*/
@Test
public void testConnection4() throws Exception {
//1、获取基本信息
String url="jdbc:mysql://localhost:3306/stu";
String user="root";
String password="123456";
//2、加载Driver
Class.forName("com.mysql.jdbc.Driver");
//3、获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
}
/**
* 最终版
*/
@Test
public void testConnection5() throws ClassNotFoundException, SQLException, IOException {
//1、读取配置文件中的基本信息
FileInputStream fs = new FileInputStream("jdbc.properties");
Properties ps = new Properties();
ps.load(fs);
String user = ps.getProperty("user");
String password = ps.getProperty("password");
String url = ps.getProperty("url");
String driver = ps.getProperty("driver");
//2、加载驱动
Class.forName(driver);
//3、获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
}
}
三、PrepareStatement
1. 操作和访问数据库
- 数据库连接被用于向数据库服务器发送命令和SQL语句,并接受数据库服务器返回的结果。
- 在Java.sql包中有三个接口分别定义了对数据库的调用的不同方式:
- Statement:用于执行静态SQL语句并返回它所生成结果的对象
- PrepatedStatement:SQL语句被预编译并存储在此对象中,可以使用此对象多次高效的执行该语句
- CallableStatement:用于执行SQL存储过程
2. 使用Statement操作数据表的弊端
-
通过调用Connection对象的createStatement()方法创建对象。该对象执行静态的SQL语句,并且返回执行结果
-
Statement接口中定义了下列方法用于执行SQL语句
int excuteUpdate(String sql);//执行更新操作INSERT、UPDATE、DELETE ResultSet executeQuery(String sql);//执行查询操作SELECT
-
使用Statement操作数据表存在弊端:
- 问题一:存在拼串操作,繁琐
- 问题二:存在SQL注入问题
-
SQL注入是利用某些系统没有对用户输入的数据进行充分的检验,而在用户输入数据中注入非法的SQL语句段或命令(如:SELECT user, password FROM user_table WHERE user='a’OR1='ADN password=‘OR’1’=‘1’),从而利用系统对SQL引擎完成恶意行为的做法
-
对于Java而言,要防范SQL注入,则使用PreparedStatement取代Statement
3. PreparedStatement VS Statement
- 代码的可读性和可维护性
- PreparedStatement能最大可能提高性能:
- DBServe会对预编译语句提供性能优化。因为预编译语句有可能被重复使用,语句在被DBServer的编译器编译后的执行代码缓存下来,下次在调用时只要是相同的编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中就会被执行
- 在Statement语句中,即使是相同的操作但因为数据内容不一样,所以整个语句本身不能匹配,没有缓存语句的意义,没有数据库会对普通语句编译后的执行代码缓存。这样每执行一次都要对传入的语句编译一次。
- PreparedStatement可以防止SQL注入
4. 使用PreparedStatement操作数据表
4.1 添加数据
public class PreparedStatementUpdateTest {
@Test
public void test() {
Connection conn = null;
PreparedStatement pps = null;
try {
FileInputStream fs = new FileInputStream("jdbc.properties");
Properties ps = new Properties();
ps.load(fs);
String user=ps.getProperty("user");
String password=ps.getProperty("password");
String url=ps.getProperty("url");
String driver=ps.getProperty("driver");
//2、加载驱动
Class.forName(driver);
//3、获取连接
conn = DriverManager.getConnection(url, user, password);
//4、预编译SQL语句,返回PreparedStatement实例
String sql="insert into book(name,author,price,type)values(?,?,?,?)";
pps = conn.prepareStatement(sql);
//5、填充占位
pps.setString(1,"三国演义");
pps.setString(2,"罗贯中");
pps.setString(3,"30");
pps.setString(4,"小说");
//6、执行操作
pps.execute();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
//7、关闭资源
try {
if(pps!=null){
pps.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if(conn!=null){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
4.2 批量操作
- update、delete本身就具有批量操作的效果
- 此处批量操作主要指批量插入
public class InsertTest {
@Test
public void test(){
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCUtils.getConnection();
//方式三:设置不允许自动提交数据
conn.setAutoCommit(false);
String sql="insert into goods(name)values(?)";
ps = conn.prepareStatement(sql);
final int value=1000000;
long start = System.currentTimeMillis();
for (int i = 0; i <value ; i++) {
ps.setObject(1,"name_"+i);
//方式一
// ps.execute();
//方式二
//1、“攒”SQL
ps.addBatch();
if(i%500==0){
//2、执行batch
ps.executeBatch();
//3、清空batch
ps.clearBatch();
}
}
//统一提交数据
conn.commit();
long end = System.currentTimeMillis();
System.out.println("执行时间为:"+(end-start));
//方式一执行时间为:29135
//方式二执行时间为:279 1000000--->执行时间为:9568
//方式三:1000000--->执行时间为:5000
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn,ps);
}
}
}
- 注意:MYSQL服务器默认是关闭批处理操作,需要通过在配置文件url后添加参数开启批处理。
四、操作BLOB类型字段
1. BLOB类型
- MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据
- 插入BLOB类型的数据类型必须使用PreparedStatement,因为BLOB类型的数据无法使用字符串拼接
- MySQL的四种BLOB类型(除了在存储的最大信息量上不同外,他们是等同的)
类型 | 大小(单位:字节) |
---|---|
TinyBlob | 最大 255 |
Blob | 最大 65K |
MediumBlob | 最大 16M |
LongBlob | 最大4 G |
- 实际使用中根据需要存入的数据大小定义不同的BLOB类型
- 如果存储文件过大数据库性能会下降
2. 对BLOB类型数据的操作
2.1 插入操作
public class BlobTest {
/**
* 向数据表customer中插入Blob类型的字段
* @throws Exception
*/
@Test
public void testInsert() throws Exception {
Connection conn = JDBCUtils.getConnection();
String sql="insert into customers(name,image)values(?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1,"房东的猫");
FileInputStream fs = new FileInputStream(new File("房东的猫1.jpg"));
ps.setObject(2,fs);
ps.execute();
JDBCUtils.closeResource(conn,ps);
}
}
2.2 查询操作
public class BlobTest {
@Test
public void testQuery() {
Connection conn = null;
PreparedStatement ps = null;
FileOutputStream fos =null;
InputStream bs =null;
ResultSet rs =null;
try {
conn = JDBCUtils.getConnection();
String sql="select id,name,image from customers where id=?";
ps = conn.prepareStatement(sql);
ps.setInt(1,2);
rs = ps.executeQuery();
if(rs.next()){
//1、方式一
/*int id=rs.getInt(1);
String name = rs.getString(2);*/
//2、方式二
int id = rs.getInt("id");
String name = rs.getString("name");
Customer cust = new Customer(id, name);
System.out.println(cust.toString());
//将Blob类型字段下载下来,以文件形式保存在本地
Blob image = rs.getBlob("image");
bs = image.getBinaryStream();
fos = new FileOutputStream(new File("zjl2.jpg"));
byte[] buffer = new byte[1024];
int len;
while((len=bs.read(buffer))!=-1){
fos.write(buffer,0,len);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(rs!=null){
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if(fos!=null){
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(bs!=null){
bs.close();
}
} catch (IOException e) {
e.printStackTrace();
}
JDBCUtils.closeResource(conn,ps);
}
}
}
五、数据库事务
1. 数据库事务介绍
- 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态
- 事务处理(事务操作):保证所有事务都作为一个工作单元执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,那么所有的事务都被提交(Commit),那么这些修改就永久的保存下来;要么数据库管理系统将放弃所做的所有修改,整个事务回滚(rollback)到最初状态
- 为确保数据库中数据的一致性,数据的操纵应当是离散的成组的逻辑单元,当他全部完成时,数据的一致性可以保持,而当这个单元的一部分操作失败,整个事务应全部视为错误,所以从起始点以后的操作应全部回退到开始状态。
2. 数据提交
- 数据一旦提交就不可回滚
- DDL操作一旦执行,都会自动提交
- DML默认情况下,一旦执行,就会自动提交
- 通过set autocommit=false的方式取消DML操作的自动提交
- 默认在关闭连接时会自动提交数据
/**
* @Description TODO
* @Author YunShuaiWei
* @Date 2020/6/22 11:44
* @Version
**/
public class TransactionUpdate {
@Test
public void testUpdate() {
Connection conn = null;
PreparedStatement ps1 = null;
PreparedStatement ps2 = null;
try {
conn = JDBCUtils.getConnection();
//关闭自动提交
conn.setAutoCommit(false);
String sql1 = "update account set money=money-100 where id=?";
String sql2 = "update account set money=money+100 where id=?";
ps1 = conn.prepareStatement(sql1);
ps1.setInt(1, 2);
ps1.execute();
//模拟异常
int i = 1 / 0;
ps2 = conn.prepareStatement(sql2);
ps2.setInt(1, 3);
ps2.execute();
//数据的提交
conn.commit();
} catch (Exception e) {
e.printStackTrace();
try {
if (conn != null) {
//事务回滚
conn.rollback();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
} finally {
try {
if (ps1 != null) {
ps1.close();
}
if (ps2 != null) {
ps2.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3. 事务的ACID属性
- 原子性(Atomicity)
- 事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生
- 一致性(Consistency)
- 必须使数据库从一个一致性状态变化为另一个一致性状态
- 隔离性(Isolation)
- 一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并执行的各个事务之不能相互干扰
- 持久性(Durability)
- 一个事务一旦提交,它对数据库中数据的改变是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响
4. 事务的隔离级别
隔离级别 | 描述 |
---|---|
READ UNCOMMITED(读未提交数据) | 允许事务读取未被其他事务提交的变更,脏读、不可重复读和幻读的问题都会出现 |
READ COMMITED (读已提交数据) | 只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可重复读和幻读问题仍然可能出现 |
REPEABLE READ (不可重复读) | 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读,但幻读的问题仍然存在 |
(SERIALIZABLE (串行化)) | 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入,更新和删除操作,所有并发问题都可以避免,但性能十分低 |
六、数据库连接池
1. 问题引入
- 在使用开发基于数据库的web程序时,传统的模式基本是按一下步骤:
- 在主程序中建立连接
- 进行sql操作
- 断开数据库连接
- 存在的问题
- 普通的JDBC数据库连接使用DriverManager来获取,每次向数据库建立连接时都要将Connection加载到内存,再验证用户名和密码。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式会消耗许多资源和时间,数据库连接资源并没有得到很好的重复利用
- 对于每一个数据库连接,使用完后都得断开,否则如果程序出现异常而未能关闭,将会导致数据库系统中内存泄漏,最终将导致重启数据库
- 这种开发不能控制被创建的连接对象数,系统资源会被毫无顾忌的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃
2. 概述
- 为解决传统开发中的数据库连接问题,可以采用数据库连接池技术
- 数据库连接池的基本思想:为数据库连接建立一个"缓冲池"。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从"缓冲池"中取出一个,使用完毕后再放回
- 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个
- 数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定在这个连接池能占有的最大连接数,当应用程序向连接池请求数超过最大连接数量时,这些请求将被加入等待队列中
3. 多种开源数据库连接池
- JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由服务器(Weblogic、WebSphere、Tomcat)提供实现,也有一些开源组织提供实现:
- DBCP是Apache提供的数据库连接池。Tomcat服务器自带的dbcp数据库连接池,速度相对c3p0较快
- C3P0是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性较好
- Proxool是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能
- BoneCP是一个开源组织提供的数据库连接池,速度快
- Druid是阿里提供的数据库连接池,集DCCP、C3P0、Proxool优点于一身
- DataSource通常被称为数据源,它包含连接池和连接池管理两部分,习惯上经常把DataSource称为连接池
- DataSource用来取代DriverManager来获取Connection,获取速度快,同时可以大幅度提高数据库访问速度
3.1 C3P0
- 测试连接
/**
* @Description TODO
* @Author YunShuaiWei
* @Date 2020/6/22 21:21
* @Version
**/
public class DataSourceTest {
@Test
public void testGetConnection() throws Exception {
//获取c3p0数据库连接池
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://localhost:3306/ysw");
cpds.setUser("root");
cpds.setPassword("****");
//初始数据库连接数
cpds.setInitialPoolSize(10);
//获取连接
Connection conn = cpds.getConnection();
System.out.println(conn);
}
//使用配置文件
@Test
public void testGetConnection1() throws Exception {
ComboPooledDataSource cpds = new ComboPooledDataSource("hello");
Connection conn = cpds.getConnection();
System.out.println(conn);
}
}
- 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<!-- This app is massive! -->
<named-config name="hello">
<property name="user">root</property>
<property name="password">****</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/ysw</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<!--当数据库连接池中的连接数不够时,c3p0一次性向数据库服务器申请的连接数-->
<property name="acquireIncrement">5</property>
<!--数据库连接池中初始化时的连接数-->
<property name="initialPoolSize">10</property>
<!--维护最小的连接-->
<property name="minPoolSize">50</property>
<!--维护最大的连接数-->
<property name="maxPoolSize">1000</property>
<!--数据库连接池最多维护的Statement的个数-->
<property name="maxStatements">50</property>
<!--每个连接中最多可以使用的Statement的个数-->
<property name="maxStatementsPerConnection">5</property>
</named-config>
</c3p0-config>
3.2 Druid
- 测试连接
@Test
public void druidTest() throws Exception {
Properties p = new Properties();
FileInputStream fs = new FileInputStream(new File("druid.properties"));
p.load(fs);
DataSource ds = DruidDataSourceFactory.createDataSource(p);
Connection conn = ds.getConnection();
System.out.println(conn);
}
- druid.properties文件
url=jdbc:mysql://localhost:3306/ysw
username=root
password=****
driverClassName=com.mysql.jdbc.Driver
七、Apache-DBUtils
1. 简介
- Commons-dbutils是Apache组织提供的一个开源JDBC工具类库,它是对JDBC的简单封装,学习成本低
- 封装了针对数据库的增删改查操作
2. 测试
- JDBCUtils工具类
public class JDBCUtils {
//关闭资源
public static void closeResource(Connection conn, PreparedStatement pps){
try {
if(pps!=null){
pps.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if(conn!=null){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 通过druid连接池获取连接
* @Param []
* @return java.sql.Connection
**/
public static Connection getDruidConnection(){
Properties ps = new Properties();
Connection conn = null;
try {
FileInputStream fs = new FileInputStream(new File("druid.properties"));
ps.load(fs);
DataSource ds = DruidDataSourceFactory.createDataSource(ps);
conn = ds.getConnection();
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
}
- CRUD
/**
* @Description TODO
* @Author YunShuaiWei
* @Date 2020/6/23 10:19
* @Version
**/
public class dbUtilsTest {
//插入测试
@Test
public void insertTest() {
Connection conn = null;
int i = 0;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils.getDruidConnection();
String sql = "insert into account(name,money)values(?,?)";
i = runner.update(conn, sql, "yunsw", "1000");
System.out.println("成功添加了 " + i + " 条记录!");
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, null);
}
}
//查询测试
@Test
public void testQuery() {
QueryRunner runn = new QueryRunner();
Connection conn = JDBCUtils.getDruidConnection();
String sql = "select * from account where id=?";
BeanHandler<AccountDAO> handler = new BeanHandler<>(AccountDAO.class);
AccountDAO query = null;
try {
query = runn.query(conn, sql, handler, 7);
System.out.println(query);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, null);
}
}
//查询多条记录:BeanListHandler
@Test
public void testQueryBeanListHandler() {
QueryRunner runner = new QueryRunner();
Connection conn = JDBCUtils.getDruidConnection();
String sql = "select id,name,money from account where money<?";
BeanListHandler handler = new BeanListHandler(AccountDAO.class);
try {
List<AccountDAO> list = (List<AccountDAO>) runner.query(conn, sql, handler, 10000);
list.forEach(System.out::println);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, null);
}
}
//查询特殊值
@Test
public void countTestScalarHandler() {
Connection conn = null;
try {
QueryRunner runner = new QueryRunner();
conn = JDBCUtils.getDruidConnection();
String sql = "select Max(money) from account";
ScalarHandler handler = new ScalarHandler();
Double query = (Double) runner.query(conn, sql, handler);
System.out.println(query);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(conn, null);
}
}
}
八、参考资料
- 尚硅谷JDBC核心技术(新版jdbc)
- 其他可关注个人博客!不将就