JDBC(Java Data Base Connectivity)即Java数据库连接,它是由Sun官方提供的2个类库包java.sql
和javax.sql
所构成。这种技术遵循了Sun官方的接口,所以JDBC就能使用一套代码就能方便地连接和操作各种不同的数据库,这也正是面向接口编程的应用之一。
体验
每次学习新知识之前,写一些类似于HelloWorld之类的小demo是很有帮助的,所以这里也先来弄一个JDBC的demo,观摩一下JDBC这个技术是如何入手使用的。
准备数据
创建数据库、表,并且插入几条数据。这里创建了名为jdbc_practice
的数据库,并在里面创建了一张user
表然后再插入了两条数据。
1 | create database jdbc_practice; |
导入相关的jar包
在使用JDBC进行编程之前,我们必须要先导入这个jar包:mysql-connector-java-x.x.xx.jar
,它包含了连接数据库时所需要的驱动程序。
如果使用maven,则使用以下依赖配置即可。
1 | <dependency> |
使用JDBC编写程序
1 | import java.sql.Connection; |
这就是使用JDBC进行数据库查询的小demo,我们也可以发现这个代码步骤也十分简单:
- 注册JDBC驱动程序
- 通过连接信息创建数据库的连接
- 执行SQL操作数据库
- 处理执行后的结果
- 释放连接等资源
大体上来说,使用JDBC进行编程就这4大步骤。
运行结果
id = 1, name = jack, age = 17
id = 2, name = kate, age = 20
注册JDBC驱动程序的原理
使用Class.forName注册数据库驱动类即可
在查看com.mysql.jdbc.Driver
这个驱动类源码的时候我们会发现这一段代码。
1 | public class Driver extends NonRegisteringDriver implements java.sql.Driver { |
在静态块中,调用DriverManager
的registerDriver
方法注册本驱动实例。所以,当我们第一次使用com.mysql.jdbc.Driver
这个类,即类加载器把它的加载到JVM时,就会执行静态块中的注册驱动语句。
所以我们在开发的时候只需要使用一句话就可以加载驱动程序:Class.forName("com.mysql.jdbc.Driver")
。
所有的数据库驱动类都是这样统一实现的,我们想注册什么数据库驱动类,就直接这样做即可:Class.forName("driver_class_qualified_name")
。
至于上面体验时候所演示的那加载驱动程序的两行代码所做的工作就显得有点多余了,毕竟只是个刚入门的demo,纠结太多原理性的东西也没什么用。
隐秘于DriverManager中所做的注册工作
如果我们没有做任何手工注册驱动的工作,那会如何呢?
在调试DriverManager
类的源码时,其实可以发现它一开始就帮我们注册好了一些默认的驱动类。
1 | public class DriverManager { |
loadInitialDrivers
方法就做一件事情:加载初始驱动类,但在里面使用了两种方式进行加载。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52public class DriverManager {
// 省略部分代码
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());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
// 省略部分代码
}
加载方式一:
第一种加载驱动类的方式是最直观的,从loadInitialDrivers
方法这几行代码直接得知。去除无关代码后,逻辑就显而易见了。
1 | String drivers; |
加载方式二:
第二种加载驱动类的方式隐藏得比较深,从loadInitialDrivers
方法中这段代码也没看出什么,但这块主体代码所做的工作确实就是加载驱动类。
1 | AccessController.doPrivileged(new PrivilegedAction<Void>() { |
所以,要继续深入查看ServiceLoader
类的源码,我们才能了解到其中具体的加载步骤是如何的。
1 | public final class ServiceLoader<S> implements Iterable<S> { |
PREFIX
以及service
这两个域对于后面理解加载过程是十分重要的,这里列出来留个印象。而接下来所展示的源码片段都是ServiceLoader
类中的。
我们首先从ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
的load
方法开始查看。
1 | public static <S> ServiceLoader<S> load(Class<S> service) { |
源码片段到这里,ServiceLoader
类的service
域被初始化为java.sql.Driver.class
。
最终,ServiceLoader
类中service
域的值传递给了成员内部类LazyIterator
。
那么接下来我们要重点关注的就是ServiceLoader
类中的成员内部类LazyIterator
的源码。
1 | private class LazyIterator implements Iterator<S> { |
从这个迭代器LazyIterator
的代码首先可得知3件事:
ServiceLoader
外部类service
域的值会传递给这个迭代器类的service
域hasNext
方法必定会调用hexNextService
方法next
方法必定会调用nextService
方法
我们最终重点关注的就是迭代器类中那两个实现加载工作的私有方法。
16~35行
hasNextService
方法中最重要的一个语句
1 | String fullName = PREFIX + service.getName(); |
其中,PREFIX
和service
的值已经知道了,而fullName
的值自然而然就是META-INF/services/java.sql.Driver
。而下面所做的工作就是在找到fullName
所指定的资源文件,并读取它。资源文件里面就有初始驱动类的全限定驱动类名列表。
不信的话,你可以自己展开mysql-connector-java-x.x.xx.jar
这个jar包,根据fullName
值所表示的目录就可以找到java.sql.Driver
这个文件,
里面就有初始的驱动类的全类名列表,比如我这里使用的是mysql-connector-java-5.1.37.jar
,里面就有:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
1 | nextName = pending.next(); |
最后,nextName
就会被赋予上一个驱动类名以供调用nextService
方法时使用。
最后来看37~48行
nextService
方法的具体实现
nextName
将它存储的驱动类名赋值给cn
,并重置为空。那接下来的两个语句就起到的作用就是加载驱动类了。1
2c = Class.forName(cn, false, loader);
S p = service.cast(c.newInstance());
第一行forName
方法并不会触发cn
对应驱动类的静态块进行加载。而第二行newInstance
这个调用就会触发驱动类加载注册了。
总结
在我们手工注册指定的数据库驱动之前,DriverManager
会做这个工作:
- 首先通过系统属性中
jdbc.drivers
这个键获取对应的数据库驱动类名,并注册它们 - 然后再通过读取数据库驱动jar包的配置文件
META-INF/services/java.sql.Driver
,并注册里面所存储好的数据库驱动类类名
JDBC中相关的类与接口
学习JDBC,我们只要把其中相关的类与接口都熟悉一遍,就能轻松地它们它们了。
DriverManager类
java.sql.DriverManager
类是工具类,它主要负责数据库的驱动注册和连接获取。
其中这个类常用的方法有:
public static Driver getDriver(String url)
根据指定的数据库URL地址获取JDBC驱动public static Enumeration<Driver> getDrivers()
获取驱动管理器中所有已经加载好的JDBC驱动public static void registerDriver(Driver driver)
注册指定的数据库驱动public static Connection getConnection(String url, String user, String password)
获取数据库连接
连接数据库的URL路径格式:主协议:子协议://主机地址:端口/数据库名
,如果连接本地数据库,那么可以省略主机地址和端口,斜杠是绝对不能省略。
Connection接口
java.sql.Connection
是SUN定义的顶级接口,一般的数据库厂商所创建的接口都会继承该接口。
获取到数据库的连接,我们就可以对数据库进行操作了,而对数据库的操作可以分为两大部分:
执行SQL语句
要执行SQL语句,则必须使用Connection
接口中的方法创建相应的语句对象才行。
Statement createStatement()
-Statement
对象执行静态SQL语句,它不能接收参数PreparedStatement prepareStatement(String sql)
-PreparedStatement
对象执行预编译SQL语句,它可以接收参数CallableStatement prepareCall(String sql)
-CallableStatement
对象执行存储过程,并且它也可以接收参数
执行事务操作
有关事务操作的常用方法比较多的,这些方法同样是在Connection
接口中。
void setAutoCommit(boolean autoCommit)
- 设置事务是否自动提交,一般设置为false
void rollback()
- 回滚事务Savepoint setSavepoint()
- 创建一个匿名的回滚点Savepoint setSavepoint(String name)
- 根据指定的名称创建一个带名称的回滚点void rollback(Savepoint savepoint)
- 回滚到指定的回滚点void commit()
- 提交事务void setTransactionIsolation(int level)
- 设置事务隔离级别
Statement接口
正如前面所讲,java.sql.Statement
类型的对象用于执行静态SQL语句,并且我们不能传递参数给它。其中常用于执行SQL的方法有:
boolean execute(String sql)
- 执行指定的SQL,返回true
表示执行结果是一个ResultSet
,false
表示执行结果是更新统计数或没有结果。ResultSet executeQuery(String sql)
- 执行SQL进行查询并返回结果集ResultSet
对象。int executeUpdate(String sql)
- 执行SQL进行更新并返回影响的记录数
ResultSet接口
java.sql.ResultSet
接口代表的是查询结果。
其中,查询后得出这个类型的对象后,我们常用以下方法来处理查询结果:
boolean next()
- 将游标从当前位置移动到下一行boolean previous()
- 将游标从当前位置移动到上一行boolean last()
- 将游标移动到最后一行boolean first()
- 将游标移动到第一行boolean absolute(int row)
- 将游标移动到指定的行数
除了这些方法,还有的就是各种getXXX
方法用于获取数据。
这里,使用JDBC技术进行普通的SQL查询。
1 | public class JDBC { |
PreparedStatement接口
使用java.sql.Statement
接口类型对象只能执行普通的SQL语句,而且我们不能通过它来给SQL语句指定参数,想要指定参数,我们必须将参数一个一个且准确无误地拼接到SQL语句中。所以当我们所要执行的SQL需要传参时,使用它是十分麻烦且容易出错的。
但使用扩展java.sql.Statement
接口的子接口java.sql.PreparedStatement
接口来做这个工作就方便得多了。
1 | public class PreparedStatementTest { |
在SQL语句中,在需要使用参数的地方插入?
作为参数占位符,在这之后调用预编译语句对象的setXXX
方法设置参数即可。
CallableStatement接口
如果要使用JDBC技术执行存储过程或函数,这就必须使用java.sql.CallableStatement
接口才行。
我们要使用这个接口之前,首先要知道调用存储过程或函数的语法,这个语法可以在API文档中找到:
- 调用函数 -
{? = call <function-name>[(<arg1>,<arg2>, ...)]}
- 调用存储过程 -
{call <procedure-name>[(<arg1>,<arg2>, ...)]}
细节:
1.使用大括号{}包围中间的语句对于调用函数来说是必须的,省略它,调用函数时就会出错。
2.存储过程和函数的每个参数都要用问号?作为占位符。
3.如果是调用函数,第一个参数就是结果参数,必须使用CallableStatement接口中的registerOutParameter方法将它注册为输出参数。
4.CallableStatement接口中的对应各种类型的set方法是传递参数值给存储过程或函数,而get方法是获取输出参数值或函数返回值。
1 | public class CallableStatementTest { |
代码优化
我们可以发现上面的代码例子是有问题的。如果说单独用作学习例子确实没什么问题,但这种代码应用于实际的项目中是十分不好的。
问题:
- 连接数据库所要用的参数信息都写死在代码中
- 释放资源这种重复代码每次都要写
所以,现在我们可以写一个工具类。
1 | public class DBUtils { |
这样,我们在项目中就能应用这个数据库工具类。
1 | public class TestDao { |