JDBC编程流程与原理
JDBC编程流程与原理
JDBC介绍
JDBC是指JAVA数据库连接,Java database connectivity的缩写,是Java提供的一个独立于特定数据库,通用的数据库存储和操作的公共接口。
它为Java开发人员操作数据库提供了一个标准的API,可以为多种数据库提供统一访问。
JDBC优势:
- Java语言访问数据库操作完全面向抽象接口编程。
- 代码不依赖于任何的数据库,只要少量的修改就可以访问其它数据库。
- 程序的可移植性增强。
现在操作数据库的框架像Mybatis,Hibernate都非常优秀,使用起来比较方便。
但我们仍需了解这些框架最底层的JDBC原理。
JDBC编程的使用
-
导入依赖包 mysql-connector-java
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>
-
加载JDBC驱动
Class.forName("com.mysql.jdbc.Driver");
通过反射加载驱动。
但目前手动注册驱动可以省去了,看如下代码:
在DriverManager.java中有一个静态代码块。/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); //加载 Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); //Class.forName } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
ServiceLoader.load(Driver.class);
上面这行代码可以把类路径下所有jar包中META-INF/services/java.sql.Driver文件中定义的类加载上来,此类必须继承自java.sql.Driver。
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());这行代码,正是将手动加载改成了自动。 -
创建执行操作语句的对象
Statement statement = connection.createStatement();
-
执行sql语句
String sql = "select id,name,age,date from user"; resultSet = statement.executeQuery(sql);
ResultSet对象用于接收结果。
-
处理结果
while (resultSet.next()) { //通过列名查询 id = resultSet.getInt("id"); name = resultSet.getString("name"); age = resultSet.getInt("age"); date = resultSet.getDate("date"); System.out.println("id: " + id + ",name: " + name + ",age: " + age + ",date:" + date); }
-
关闭资源
resultSet statement connection 关闭资源示例: if (resultSet != null) { try { resultSet.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } }
现在看一下完整的代码
import java.sql.*;
/**
* @author zxg
* @date 2020/12/28
* @description JDBC编程流程
*/
public class JDBCTest {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
String url = "jdbc:mysql://localhost:3306/db";
String user = "root";
String password = "root";
try {
// 1 加载JDBC程序驱动
//Class.forName("com.mysql.jdbc.Driver");
// 2 建立数据库连接
// 格式 jdbc:mysql://localhost:3306/db?properties
connection = DriverManager.getConnection(url, user, password);
//3. 创建执行语句的对象
statement = connection.createStatement();
//4. 执行SQL语句并接收返回结果
String sql = "select id,name,age,date from user";
resultSet = statement.executeQuery(sql);
//5. 处理结果
int id;
String name;
int age;
Date date;
while (resultSet.next()) {
//通过索引查询
id = resultSet.getInt(1);
name = resultSet.getString(2);
age = resultSet.getInt(3);
date = resultSet.getDate(4);
System.out.println("id: " + id + ",name: " + name + ",age: " + age + ",date:" + date);
//通过列名查询
id = resultSet.getInt("id");
name = resultSet.getString("name");
age = resultSet.getInt("age");
date = resultSet.getDate("date");
System.out.println("id: " + id + ",name: " + name + ",age: " + age + ",date:" + date);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//6. 关闭资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
}
查看运行结果:
在对结果集resultSet处理时,可以通过索引和列名两种方式来获取数据。索引从1开始。
源码解析
DriverManager
整个流程从DriverManager开始,上面这个类已经看过部分源码,这个类会管理和注册驱动,然后我们通过DriverManager.getConnection方法来获取数据库的连接。
方法 | |
---|---|
getConnection(String url, java.util.Properties info) | 该方法传入url,第二个参数info保存配置信息 |
getConnection(String url,String user, String password) | 该方法传入url,用户名,密码 |
getConnection(String url) | 该方法的参数为将url与user,password拼接的字符串 |
这三种方法底层都会调用一个私有方法,private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller),该方法会返回connection对象。
Connection 接口
Connection接口实现类由数据库提供,比如下面的代码就是mysql提供的ConnectionImpl,用于创建一个语句对象,该对象将生成具有给定类型和并发性的ResultSet对象。
public java.sql.Statement createStatement() throws SQLException {
return this.createStatement(1003, 1007);
}
public java.sql.Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
this.checkClosed();
StatementImpl stmt = new StatementImpl(this.getMultiHostSafeProxy(), this.database);
stmt.setResultSetType(resultSetType);
stmt.setResultSetConcurrency(resultSetConcurrency);
return stmt;
}
public java.sql.Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
if (this.getPedantic() && resultSetHoldability != 1) {
throw SQLError.createSQLException("HOLD_CUSRORS_OVER_COMMIT is only supported holdability level", "S1009", this.getExceptionInterceptor());
} else {
return this.createStatement(resultSetType, resultSetConcurrency);
}
}
第一个参数为返回的结果集类型
/**
* 光标只能向前移动。
*/
int TYPE_FORWARD_ONLY = 1003;
/**
* 该对象是可滚动的,但通常对作为ResultSet基础的数据的更改不敏感。
*/
int TYPE_SCROLL_INSENSITIVE = 1004;
/**
* 该对象是可滚动的,通常对作为ResultSet基础的数据的更改敏感。
*/
int TYPE_SCROLL_SENSITIVE = 1005;
第二个参数为返回的结果集并发类型
/**
* 指示可能不会更新的ResultSet对象的并发模式的常量。
* 只读
*/
int CONCUR_READ_ONLY = 1007;
/**
* 可更新
*/
int CONCUR_UPDATABLE = 1008;
这两个参数主要是设置ResultSet结果集,TYPE_FORWARD_ONLY就意味着光标只能向前移动,只能读取一次数据。
Statement 接口
用于执行静态SQL语句
并返回它产生的结果的对象
。
默认情况下,每个Statement对象只能同时打开一个ResultSet对象。因此,如果对一个ResultSet对象的读取与对另一个对象的读取交织在一起,那么每个ResultSet对象都必须由不同的语句对象生成。如果存在打开的ResultSet对象,则Statement接口中的所有执行方法都隐式关闭语句的当前ResultSet对象。
执行静态语句意味着不能动态生成sql语句,我们只能事先拼接sql语句。
看一下它的方法
方法 | 介绍 |
---|---|
boolean execute(String sql) | 执行一条SQL语句,可能返回多个结果,如果第一个结果是ResultSet对象则为true;如果是更新计数或没有结果,则为false |
int executeUpdate(String sql, int autoGeneratedKeys) | 执行给定的SQL语句,该语句可能会返回多个结果,并向驱动程序发出信号,表示应该让任何自动生成的键可供检索。第二个参数指明是否可以使用getGeneratedKeys方法获取自动生成的键; |
ResultSet executeQuery(String sql) | 执行给定的SQL语句,该语句返回单个ResultSet对象。通常参数为一个静态的SQL语句 |
int executeUpdate(String sql) | 执行给定的SQL语句,可能是INSERT、UPDATE、DELETE语句,也可能是不返回任何结果的SQL语句,如SQL DDL语句。返回操作别的行数。 |
default long executeLargeUpdate(String sql) | 执行给定的SQL语句,可能是INSERT、UPDATE、DELETE语句,也可能是不返回任何结果的SQL语句,如SQL DDL语句。当返回的行数可能超过Integer.MAX_VALUE时,应该使用此方法。 |
PreparedStatement 接口
表示预编译SQL语句的对象。
SQL语句被预编译并存储在PreparedStatement对象中。然后可以使用此对象高效地多次执行此语句。
这个接口继承Statement接口。继承了它的所有方法,是一个预编译的SQL语句。
public interface PreparedStatement extends Statement {
- 有预先编译的功能,提高 SQL 的执行效率。
- 可以有效的防止 SQL 注入的问题,安全性更高。
- 提高程序可读性。
创建PreparedStatement对象
创建一个PreparedStatement对象,用于向数据库发送参数化的SQL语句。
connection.prepareStatement(sql);
这个方法还有其他几种
参数的含义与createStatement这个方法一样。
如何使用PreparedStatement对象
// 创建连接
conn = DriverManager.getConnection(url, props);
// 创建一个PreparedStatement对象,用于向数据库发送参数化的SQL语句。
preparedStatement = conn.prepareStatement("insert into user(name,age) values(?,?)");
//索引从1开始
preparedStatement.setString(1,"阿三");
preparedStatement.setInt(2,22);
//执行参数化语句
int count = preparedStatement.executeUpdate();
if(count>0){
System.out.println("插入成功");
}
编写SQL语句时使用 ? 作为占位符,然后使用setXXX方法来设置实际参数。
ResultSet接口
在执行查询语句比如executeQuery()时都会返回一个ResultSet对象,ResultSet称为结果集。
结果集可以在创建statement的时候设置类型(光标只能向前移动、可滚动。数据更改敏不敏感等)。
保存在ResultSet中的数据可以使用getXXX的方式来获取。
使用while(resultSet.next()){ getXXX()}可获取结果集中的所有数据。
事务
事务是必须满足4个条件(ACID)
- 原子性( Atomicity):一组事务,要么全部成功;要么全部不完成。
- 一致性 (Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的。
通过一个例子看一下
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCTXTest {
public static void main(String[] args) {
/*
account表:两个字段,name姓名,balance余额
开启事务
用户1向用户2转账
提交、回滚 */
Connection conn = null;
PreparedStatement statement = null;
try {
//创建Connection连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/zxg?useSSL=false", "root", "123456");
//开启事务,不自动提交
conn.setAutoCommit(false);
updateBalance(conn, "zxg", -20);
updateBalance(conn, "ww", 20);
//提交
conn.commit();
} catch (SQLException throwables) {
if (conn != null) {
try {
//回滚
conn.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
throwables.printStackTrace();
}finally {
if(conn!=null){
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
/**
* 更新操作
*
* @param conn 数据库连接
* @param name 用户名
* @param balance 更新后余额
*/
public static void updateBalance(Connection conn, String name, int balance) {
try {
//创建PreparedStatement对象
String sql = "update account set balance = balance + ? where name = ?";
PreparedStatement statement = conn.prepareStatement(sql);
//设定参数
statement.setString(2, name);
statement.setInt(1, balance);
//执行语句
statement.executeUpdate();
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
if(statement!=null){
try {
statement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
}
这个程序开启事务,执行了两条操作,要么全部成功,要么全部失败。
我们模拟了一个转账操作,从用户A转出金额,转入用户B,所以开启事务。
如果用户A的余额不够转出(即转出后余额小于0),则这条SQL操作将失败,进行回滚操作。
-
开启事务
- conn.setAutoCommit(false);
开启事务,不自动提交。须使用 conn.commit() 手动提交。 - conn.setAutoCommit(true);
- 开启事务,自动提交。
- conn.setAutoCommit(false);
-
提交事务
conn.commit();
使自上一个提交/回滚以来所做的所有更改成为永久的,并释放该连接对象当前持有的所有数据库锁。只有禁用了自动提交模式时才应该使用此方法。 -
回滚
- conn.rollback();
撤消在当前事务中所做的所有更改,并释放此连接对象当前持有的任何数据库锁。只有禁用了自动提交模式时才应该使用此方法。 - void rollback(Savepoint savepoint)
撤消设置给定保存点对象后所做的所有更改。只有禁用了自动提交时才应该使用此方法。
与setSavepoint()方法一起使用
- conn.rollback();
-
设置保存点
- setSavepoint()
在当前事务中创建一个未命名的保存点,并返回表示该保存点的新保存点对象。
如果在活动事务之外调用setSavepoint,则事务将在这个新创建的保存点上启动。 - setSavepoint(String name)
在当前事务中创建具有给定名称的保存点,并返回表示该保存点的新保存点对象。
- setSavepoint()