JDBC(Java DataBase Connectivity),它是java程序访问数据库的标准 接口。
Java访问数据库并不是直接通过TCP连接访问数据库,而是使用JDBC接口访问。而JDBC接口通过JDBC驱动实现真正的数据库访问。JDBC的接口是由JDK
定义并内置在JDK中,而具体的数据库驱动是由厂商提供的,实际上JDBC的驱动就是一个jar包。
由此可看出使用JDBC的好处是:
- 各数据库厂商使用相同的接口,Java代码不需要针对不同的数据库进行开发
- java程序编译期只需要引入
java.sql.*
,不依赖具体的数据库jar包 - 可随时替换底层数据库,只需更换jar包,而访问数据库的代码不变
1 JDBC获取数据库链接
获取数据库链接时,需要三个基本的字符串,分别是:
- JDBC的URL,它是由数据库厂商定义的,例如MySQL的URL是
jdbc:mysql://
加数据库所在服务器的地址及端口号 - 数据库的用户名及密码
然后就可以使用 DriverManganer.getConnection(String url, String userName, String password);
获取数据库链接了。
// JDBC链接的URL
String JDBC_URL = "jdbc:mysql://localhost:3306/test"; // test表示访问数据库的名字
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
Connection conn = DriverManager.getConnection(JDBC_URL,JDBC_USER,JDBC_PASSWORD);
// 访问数据库。。
// 关闭数据库
conn.close();
2 JDBC执行查询语句
JDBC执行查询流程:
- 创建数据库链接
- 创建查询语句对象
Statement
(不推荐)或PrepareStatement
对象,并执行其executeQuery
方法 - 查询的结果是
ResultSet
,通过while
循环它,调用形如getString('name')
形式的方法获取相应的数值
eg:
// 获取数据库链接
public static Connection getConnection() throws SQLException {
Connection connection = DriverManager.getConnection(JDBCurl, userName, pw);
return connection;
}
// 获取全部学生列表
public static List<Student> getAllStudents() throws SQLException {
// 获取连接
try (Connection connection = getConnection()) {
// 创建 prepareStatement 对象
try (PreparedStatement preparedStatement = connection.prepareStatement("select * from students")) {
// 执行语句,获取查询结果二维表
try (ResultSet resultSet = preparedStatement.executeQuery()) {
List<Student> students = new ArrayList<>();
// 循环 resultSet,获取记录,使用类似 JSONfast 的形式获取具体信息
while (resultSet.next()) {
Long id = resultSet.getLong("id");
Long class_id = resultSet.getLong("class_id");
String name = resultSet.getString("name");
Student student = new Student(name, class_id, id);
students.add(student);
}
return students;
}
}
}
}
// 根据班级获取学生列表
public static List<Student> getStudentsByClass(String classId) throws SQLException {
// 获取连接
try (Connection connection = getConnection()) {
// 创建 prepareStatement 对象
try (PreparedStatement preparedStatement = connection
.prepareStatement("select * from students where class_id = ?")) {
// preparedStatement 对象的替代索引从 1 开始
preparedStatement.setObject(1, classId);
// 执行语句,获取查询结果二维表
try (ResultSet resultSet = preparedStatement.executeQuery()) {
List<Student> students = new ArrayList<>();
// 循环 resultSet,获取记录,使用类似 JSONfast 的形式获取具体信息
while (resultSet.next()) {
Long id = resultSet.getLong("id");
Long class_id = resultSet.getLong("class_id");
String name = resultSet.getString("name");
Student student = new Student(name, class_id, id);
students.add(student);
}
return students;
}
}
}
}
// 获取学生总数
public static long getStudentsCount() throws SQLException {
// 获取连接
try (Connection connection = getConnection()) {
// 创建 prepareStatement 对象
try (PreparedStatement preparedStatement = connection
.prepareStatement("select count(*) count from students")) {
// 执行语句,获取查询结果二维表
try (ResultSet resultSet = preparedStatement.executeQuery()) {
Long count = (long) 0;
// 循环 resultSet,获取记录,使用类似 JSONfast 的形式获取具体信息
while (resultSet.next()) {
count = resultSet.getLong("count");
}
return count;
}
}
}
}
3 JDBC执行更新、插入、删除语句
JDBC执行更新、插入、删除语句与执行查询语句的流程一致,不同在于 preparedStatement
对象的执行是调用 executeUpdate
方法,其返回结果是更新、插入、删除数据的个数。
当执行插入语句时,往往需要获取自增主键的数值,此时就需要在创建 preparedStatement
对象的时候再传入一个常量 Statement.RETURN_GENERATED_KEYS
,表示需要返回数据库生成的主键。
eg:
// 更新id=1的学生的name
public static int updateStudent() throws SQLException {
try (Connection connection = getConnection()) {
try (PreparedStatement preparedStatement = connection
.prepareStatement("update students set name = 'Seiei' where id = 1")) {
int num = preparedStatement.executeUpdate();
return num;
}
}
}
// 增加Student
public static int insertStudent() throws SQLException {
Student newStudent = new Student("Taka", (long) 4, null);
try (Connection connection = getConnection()) {
// 创建preparedStatment 对象时,添加 Statement.RETURN_GENERATED_KEYS 常量
try (PreparedStatement preparedStatement = connection.prepareStatement(
"insert students (name, class_id) values (?, ?)", Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setObject(1, newStudent.name);
preparedStatement.setObject(2, newStudent.class_id);
// 执行语句
int num = preparedStatement.executeUpdate();
// 获取自增主键
try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) {
while (resultSet.next()) {
// 使用索引的形式获取返回的自增主键
System.out.println("返回的自增主键是:" + resultSet.getLong(1));
}
}
return num;
}
}
}
// 删除Student
public static int deleteStudent() throws SQLException {
try (Connection connection = getConnection()) {
// 创建preparedStatment 对象时,添加 Statement.RETURN_GENERATED_KEYS 常量
try (PreparedStatement preparedStatement = connection
.prepareStatement("delete from students where id = 5")) {
// 执行语句
int num = preparedStatement.executeUpdate();
return num;
}
}
}
4 数据库事务
假如要模拟一个转账的情况,A转账100块给B,化作成数据库操作就是A的存款减少100,B的存款增加100,一共两个SQL操作语句,假如A存款SQL操作执行成功,而B的存款增加SQL操作没有执行或执行不成功,此时应用程序的逻辑出现了不一致的情况,此时就需要使用数据库事务了。
数据库事务是指:若干SQL语句构成的一个操作序列,使用数据库事务可以保证多个SQL语句全部执行或全部不执行。
数据库事务具有ACID特性,即:
- Atomicity:原子性,指数据库事务虽然本身是由多个SQL语句构成,但它是一个原子操作,执行要么全不成功,要么全不失败
- Consistency:一致性,指一个事务在开始前和结束后,数据库数据是完整的,不存在冲突和数据不一致
- Isolation:隔离性,指多个事务并发执行的时候,事务之间是隔离的,一个事务不应该影响其他事务
- Durability:持久性,指事务一旦执行成功,这个事务对数据库所作的更改会持久的保存在数据库中,并且不会被回滚
4.1 数据的隔离级别
事务的隔离级别它是由数据库允许多个连接同时执行事务引起的,即比如在Java程序中,当一个线程操作数据库连接的时候,另一个线程可能也在使用一个数据库链接更新数据,它们之间就可能遇到隔离级别的问题。
脏读:一个事务读取了被另一个事务改写但还未提交的数据时。如果这些数据被回滚,那么之前的事务读取的到数据就是无效的。
如下图所示,事务A执行了 update
语句,但还没有提交事务,而事务B读取了该没有提交的更新数据,而此时如果事务A回滚了,那事务B读取的数据就是 脏数据。
非重复读:在同一事务中,多次读取同一数据返回的结果有所不同(读到另一个事务提交的更新的数据)。
如下图所示,事务B在两条 select
语句中返回的结果不一致,这是因为在两条语句执行之间,事务A将数据给 update
了。
幻读:一个事务读取几行记录后,另一个事务插入了一些记录(也可以删除),幻读就发生了。在后来的查询中第一个事务就会发现有些原来没有的记录。
如下图所示,事务B读取数据两次都是返回空数据,而使用 update
数据的时候却返回修改成功,再次执行 select
语句的时候返回数值。
为了避免这些问题,事务定义四种管理级别,分别是:
-
读未提交(
READ_UNCOMMITED
):允许读取还未提交的改变了的数据。可能导致脏读、幻读、不可重复读。 -
读已提交(
READ_COMMITED
):允许在并发事务已经提交后读取。可防止脏读,但幻读、不可重复读仍可能发生。 -
可重复读(
REPEATABLE_READ
):对相同字段的多次读取是一致的,除非数据被事务本身改变。可防止脏读、不可重复读。但幻读仍可能发生。 -
可串行化(
SERIALIZABLE
):完全服从ACID的隔离级别,确保不发生脏读、幻读和不可重复读。他在所有的隔离级别中是最慢的,毕竟要完全锁住在事务中涉及的数据表。 -
Default:使用了后端数据库默认的隔离级别(spring中的选择项,也是
isolation
属性的默认值,Mysql默认采用REPEATABLE_READ
隔离级别,Oracle默认采用READ_COMMITED
隔离级别)。
级别类型 | 脏读 | 非重复读 | 幻读 |
---|---|---|---|
Read Uncommitted(读未提交) | 会出现 | 会出现 | 会出现 |
Read Committed(读已提交) | 会出现 | 会出现 | |
Repeatable Read(可重复读) | 会出现 | ||
Serializable(可串行化) |
管理级别越高,数据库执行时所加的锁就越多,能够同时并发执行的事务就越少。
4.2 创建JDBC事务
创建JDBC事务:
Connection
对象中调用setTranscationTsolation
方法传入事务级别Connection
对象调用setAutoCommit(false)
表示打开事务- 执行完SQL语句,执行
commit
方法提交事务 - 出错时,调用
rollback
方法回滚事务 - 最后事务提交后,调用
setAutoCommit(true)
和close
关闭事务
eg:
// 更新
static void updateName(Connection connection, String name, long id) throws SQLException {
try (PreparedStatement preparedStatement = connection
.prepareStatement("update students set name = ? where id = ?")) {
preparedStatement.setObject(1, name);
preparedStatement.setObject(2, id);
preparedStatement.executeUpdate();
}
}
public static void main(String[] args) throws SQLException {
Connection connection = null;
try {
// 获取连接
connection = getConnection();
// 设置事务级别
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// 开启事务
connection.setAutoCommit(false);
// 执行sql
updateName(connection, "curry", (long) 3);
updateName(connection, "james", (long) 4);
// 提交事务
connection.commit();
} catch (Exception e) {
// 事务回滚
e.printStackTrace();
connection.rollback();
} finally {
if (connection != null) {
// 关闭事务
try {
connection.setAutoCommit(true);
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
5 链接池
线程池可以服用一个线程,这样就能避免反复创建线程的开销,提高运行效率,同样地使用JDBC链接池可以复用JDBC连接 Connection
,避免反复创建新连接,提高运行效率。
JDBC连接池它的接口定义在 javax.sql.DataSource
这个接口中,JDK只提供连接池的定义,具体实现需要引入一些开源包。
在连接池获取到
Connection
对象后,也是要使用try (resource) {}
这样的方式去操作Connection
对象,此时操作完关闭Connection
对象时,并没有真正的关闭Connection
对象,而是将其释放到连接池中。