说起JDBC,估计大家都能说出个一、二、三出来,毕竟JDBC的大名如雷灌耳,Mybaitis、Hibernate、Sharding-JDBC等,都是对JDBC进行的封装,因此理解JDBC的理论基础,对我们以后的开发大有裨益。
下面,就让我们一起来看下JDBC,让自己的武器库中多一种基础理论。
文章目录
1.JDBC简介
在开发过程中,数据库的开发是非常重要的,用户的个人信息、操作记录、内容信息(如商品、新闻)等等等,几乎所有的数据都是需要存储在数据中的。尤其是Web系统在10世纪90年代的起飞,Java和数据库之间发生了一些美妙的故事,Sun公司为了可以使Java能支持数据库的访问,为Java提供了一套访问数据库的标准的类库------JDBC。这套类库位于jdk提供的基础jar包–rt.jar中,具体的位置在java.sql包中。
那什么是JDBC呢?JDBC的全称为Java数据库连接(Java DataBase Connectivity),它是一套用与执行SQL语句的Java API,是Java访问数据库的一整套规范。
从上图中,可以看到,JDBC和各种数据库之间的连接(我们也称之为连接驱动),连接JDBC这一端的都是统一规格的,而连接到MySQL、Oracle、PostgreSQL等数据库的一端是各不相同的,这也是为什么说JDBC是Java操作数据库的标准,因为他(JDBC)要求各个数据库厂商按照统一的规范来提供数据库连接驱动,各个连接驱动中,都需要实现JDBC中的接口,而JDBC不去关心各个驱动中具体的实现细节。
通过这种方式,使得在Java程序中连接数据库,只需要对接JDBC即可,如果连接MySQL数据库,则装入MySQL Connector,**如果某天数据库更换为Oracle,只需更换驱动程序、数据库驱动注册(后文有解释)、特殊的SQL函数(某数据库专有的)**即可。这样开发人员就可以不必直接与底层的数据库交互,使得代码的通用性更强,且更加不容易出错,数据库开发也就更容易。
2.JDBC中常用的接口
工欲善其事,必先利其器。我们直接对接JDBC,那就需要我们对JDBC设计的这套游戏规则有足够的了解。上文我们也说过了,JDBC涉及的接口主要在java.sql包中,让我们一起来了解一些重要的接口和类。
2.1 Driver接口
Driver接口是所有数据库连接程序必须要实现的接口。JDBC要求,当驱动程序中实现Driver接口的类被加载,它应创建其自身的实例并在DriverManager中注册。
我们以MySQL Connector为例来进行深点的分析(可自行去MySQL官网下载),其中的Driver类如下所示:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
java.sql.Driver
接口中的方法是在NonRegisteringDriver
类中实现的,我们从上面代码的静态代码块中可以看到,当com.mysql.jdbc.Driver
被加载到内存中时,会自动的创建一个自身的实例(new Driver()
),并且将其注册到java.sql.DriverManager
中。
在注册完成之后,我们就可以不用去关心任何MySQL Connector中的技术细节了,但是想研究源码的除外😝。
因此,我们注册的数据库驱动的方式有两种:
java.sql.DriverManager.registerDriver(new Driver)
,其中的Driver为数据库驱动中的Driver接口的实现类;Class.forName("xxx.Driver")
,xxx为包路径,如mysql-connector中路径为"com.mysql.jdbc.Driver"
对于上面两种方式,我们推荐使用Class.forName("xxx.Driver")
方式,实例化时让其自动注册;如果使用第一种方式,则会Driver会被实例化两次(静态代码块 + new)。
Driver接口中还提供了一些来获取驱动信息的类,方法如下:
方法名 | 请求重定向 |
---|---|
Connection connect(String url, java.util.Properties info) | 尝试建立到给定URL的数据库连接。如果子协议错误,则返回null |
boolean acceptsURL(String url) | 检索驱动程序是否认为它可以打开给定URL的连接 |
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) | 获取有关此驱动程序可能的属性的信息 |
int getMajorVersion() | 检索驱动程序的主版本号 |
int getMinorVersion() | 获取驱动程序的次要版本号 |
boolean jdbcCompliant() | 判断驱动程序是否能通过JDBC遵从性测试 |
其中子协议为url中"jdbc:mysql://localhost:3306/java_web"中的"mysql"。
2.2 DriverManager类
DriverManager
类的基本服务是用来管理一组JDBC驱动(因为所有的数据库驱动都会注册到DriverManager
类中),并且可以用于加载JDBC驱动和创建与数据库之间的的连接(Connection
)。
DriverManager
类提供了注册驱动程序的方法,除了上面我们提到的registerDriver
方法外,DriverManager
类会自动的加载系统属性(system property)中的"jdbc.drivers"中设计到的驱动类(Driver的实现类),示例代码如下(可以同时写多个驱动类的地址,使用’:'隔开)。我们可以通过设置JVM的启动参数来配置默认注册数据库驱动。
jdbc.drivers=foo.bah.Driver:wombat.sql.Driver:bad.taste.ourDriver
在DriverManager源码中,类说明中有这么一段话,其中的意思就是在应用中再也不用去使用Class.forName()
显示的加载JDBC的驱动了。
Applications no longer need to explicitly load JDBC drivers using <code>Class.forName()</code>. Existing programs which currently load JDBC drivers using <code>Class.forName()</code> will continue to work without modification.
What,刚教了三种注册JDBC驱动的方法,你告诉我以后用不到了。我们来看下DriverManager类中初始化部分,源码如下,我们可以看到,静态代码块中会调用loadInitialDrivers
方法,该方法通过检查系统属性(“jdbc.drivers”)和ServiceLoader
机制来加载JDBC的驱动程序。
public class DriverManager {
//...
/**
* 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");
}
//...
}
既然您(JDBC)都这么费心了,只能说一句您厉害了。不过项目中原有的使用了Class.forName()
的地方,也不需要修改了,并不会影响功能,不过还是要吐槽一句,我原来都已经习惯了啊~~~~。
鉴于DriverManager类中的方法比较多,我们这里只介绍几个重要的方法:
方法名 | 功能描述 |
---|---|
Connection getConnection(String url, String user, String password) | 建立给定URL的数据库连接,并返回表示连接的Connection对象 |
Driver getDriver(String url) | 根据给定的url从已经注册的驱动集合中选取一个最适当的驱动返回 |
void registerDriver(java.sql.Driver driver) | 注册给定的驱动程序 |
其中的getConnection
还有另两个重载的方法,如下所示:
Connection getConnection(String url)
:用户名、密码在url中,如:“jdbc:mysql://localhost:3306/java_web?user=root&password=123456”;Connection getConnection(String url, java.util.Properties info)
:将用户名和密码放入Properties中,用户名、密码对应的key分别为"user"和"password";
非常有意思的是,数据库连接的建立,是由DriverManager类自动找到一个最适合的驱动(通过源码查看,发现其实是获取所有的数据库驱动,循环测试是否可以建立连接),并将建立连接的工作交由JDBC驱动中的Driver类来处理。这里可以参考2.1中的connect
方法.
2.3 Connection接口
Connection
接口表示与特定数据库的连接(Session),在连接上下文中执行sql语句并返回结果。一个连接到数据库的Connection
对象的能够提供描述库中表的信息,它支持的SQL语法,它的存储过程,这个连接的功等等等。这些信息是通过Connection
中的getMetaData
方法获得的。
这里有个概念需要明确下,不管是后面要将的Statemen
t还是PrePareStatement
,其执行sql都是在连接的上下文中执行的,可以理解成Connection
的保持是应用程序(客户端)和数据库服务器的一次会话。
下面我们来介绍Connection
接口中定义的常用的几种方法:
方法名 | 功能描述 |
---|---|
DatabaseMetaData getMetaData() | 返回一个DatabaseMetaData对象,该对象包含关于数据库的元数据。 |
Statement createStatement() | 创建并返回一个Statement对象,用于向数据库发送SQL语句。 |
PreparedStatement prepareStatement(String sql) | 创建并返回一个PreparedStatement对象,用于向数据库发送SQL语句。传入的SQL语句会被预编译。 |
CallableStatement prepareCall(String sql) | 创建并返回一个CallableStatement对象,用于调用数据库中的存储过程。 |
其中的Statement
对象适合执行无参数的sql语句,但是如果需要执行许多次,PreparedStatement
对象效率会更好,并且PreparedStatement
对象支持SQL预编译,可以用来高效的执行同一个语句多次。
2.4 Statement接口
Statement
对象用与执行静态的SQL语句,并返回产生的结果对象。Statement接口的实例化对象可通过createStatement
方法获得。
需要注意的是,一个Statement
对象同一时间只能返回一个ResultSet对象。因此当发生并发时,同一时刻的ResultSet对象要由不同的Statement
对象生成。如果起冲突的话,当前的的ResultSet对象会被关闭。
下面我们一起来看下Statement
接口中定义的常用的几种方法:
方法名 | 功能描述 |
---|---|
boolean execute(String sql) | 执行给定的SQL,返回执行一个boolean类型的结果 |
ResultSet getResultSet() | 以ResultSet的形式返回当前SQL执行结果。每次SQL执行执行后调用一次,如果结果更新数量或者没有执行结果,则返回null |
boolean getMoreResults() | 将 |
int getUpdateCount() | 获取当前作为更新数量,如果结果是一个ResultSet或者没有执行结果,则返回-1 |
ResultSet executeQuery(String sql) | 执行给定的查询(SELECT)SQL,返回一个ResultSet对象 |
int executeUpdate(String sql) | 创建并返回一个PreparedStatement对象,用于向数据库发送SQL语句。传入的SQL语句会被预编译。 |
需注意getResultSet
、getUpdateCount
方法,需要在execute
方法执行返回成功后才能获取到值,并且一次执行只能获取一次。因此如果不是在执行一个动态的未知SQL,尽量不要使用execute
方法。
2.5 PreparedStatement接口
PreparedStatement
是一个用于表示预编译的SQL语句的对象。传入的SQL语句已完成预编译并存储在PreparedStatement
对象中,之后就可以可以使用该对象有效地多次执行该语句。
通过上面一段话,我们就可以简单的看出Statement
和PreparedStatement
的区别,即出于提高多次执行同一个SQL的效率,PreparedStatement对象会先对SQL进行预编译,并且SQL语句可以使用’?'占位符来代替其中的参数,支持带有参数的SQL的执行。
对于SQL中的’?'占位符,可以通过PreparedStatement
接口提供的setter
方法来设置入参的值,但是设置时必须要指定与输入参数已定义SQL类型兼容的类型。理解起来有点绕,我们举个例子:如果入参的SQL类型为Integer
,则应该调用setInt
方法来设置入参。
下面我们一起来看下PreparedStatement
接口中定义的常用的几种方法:
方法名 | 功能描述 |
---|---|
ResultSet executeQuery() | 在PreparedStatement对象中执行SQL查询语句,并返回此次查询生成的ResultSet对象 |
int executeUpdate() | 在对象中执行SQL语句,如INSERT、UPDATE、DELETE或者无返回的SQL语句;如果为改变任何数据或者无返回值则返回0 |
boolean execute() | 执行给定的SQL,返回执行一个boolean类型的结果,通过getResultSet、getUpdateCount方法获取执行结果 |
void setXxx(int parameterIndex, xxx x) | 设置SQL语句中的入参,其中Xxx为数据类型,如setSring、setBoolean等;其中的参数索引从1 开始 |
PreparedStatement
给定了设置所有SQL类型参数的方法,需要注意的是,setTime、setDate等方法,其中的Time、Date为java.sql包中的,而不是我们常用的java.util包中的。
如何使用这些set方法呢,我们通过一个小栗子来简单示范下:
PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES SET " +
"SALARY = ? WHERE ID = ?");
pstmt.setBigDecimal(1, 153833.00)
pstmt.setInt(2, 110592)
//接收SQL语句执行结果
int result = pstmt.executeUpdate()
2.6 CallableStatement接口
CallableStatement
接口是用于执行SQL存储过程。JDBC API提供了一种存储过程SQL转义语法,该语法允许所有RDBMS可以通过标准方式调用存储过程。并且CallableStatement
接口继承了PreparedStatement
接口,因此其也可以执行非存储过程的SQL语句。
如果存储过程中有参数(In,入参),可以通过集成自PreparedStatement
接口中的setXxx
方法来赋值。
如果存储过程有输出参数(Out,出参),在调用存储过程前必须通过registerOutParameter
先注册,存储过程执行后可以通过getXxx
方法来获取结果。
同样的,同样也可以将查询结果放入ResultSet
对象中,通过迭代的方式获取执行结果。如果存储过程返回多个结果集,需要通过调Statement
对象中的getMoreResults
方法切换到下一个结果集,并通过getResult
方法获取。
存储过程调用的示例代码如下(在这里简单演示下,后续博客会详细讲解):
//第一个参数为In参数,第二个为Out参数
CallableStatement callableStatement = connection.prepareCall(
"{ call queryUsersById(?,?)} ");
callableStatement.setString(1, "1");
//注册返回参数
callableStatement.registerOutParameter(2, Types.CHAR);
//执行存储过程
ResultSet rs = callableStatement.executeQuery();
//处理返回的结果集 如果返回多个结果集,这里会获取到第一个
while (rs.next()) {
//...
}
//移动指针到下一个ResultSet对象
while (callableStatement.getMoreResults()) {
rs = callableStatement.getResultSet();
//...
}
CallableStatement
接口中只定义了getXxx
方法和registerOutParameter
方法,因此这里就不扩展开来讲了,registerOutParameter
方法有许多重载的方法,但是其功能都是进行输出参数的注册,这里使用java.sql.Types
来指定类型即可。
2.7 ResultSet接口
ResultSet
对象用于保存通过执行查询语句查询数据库产生的结果集,使用一个逻辑表格来表示一个数据库查询结果集。ResultSet
接口内部有一个指向当前数据行的游标(cursor),初始时,游标指向第一行数据之前(可以理解成第0行),调用next方法可将游标移动至下一行,如果下一行没有数据(移动到了最后一行之后),则返回false(表示到了末尾),我们通常可以使用此方法来完成结果集的遍历,代码如下所示:
//...
ResultSet rs = stmt.executeQuery("SELECT * FROM TABLE2")
while(rs.next()){
//处理逻辑
}
默认的ResultSet
对象是不可更新的,并且游标只能向后(next)移动。 因此,在程序中只能迭代ResultSet
一次,并且只能从第一行到最后一行进行迭代。这样显然不是JDBC的做事风格,虽然一个next
方法足够我们使用了,但是JDBC还是为我们提供了更丰富的功能。我们首先来看下,除了next方法外,其他的游标移动方法。
方法名 | 功能描述 |
---|---|
boolean next() | 将游标移动到下一行,当移动到数据最后一行之后时,返回false |
boolean previous() | 将游标移动到上一行,当移动到数据第一行之前时,返回false |
boolean relative(int rows) | 将游标移动相对的行数(正数或负数),正数向后、负数向前 |
boolean absolute( int row ) | 将游标移动到数据集中指定的行数 |
void beforeFirst() | 将游标移动到数据第一行之前,相当于absolute(0) |
void afterLast() | 将游标移动到数据最后一行之后 |
boolean first() | 将游标移动到数据第一行 |
boolean last() | 将游标移动到数据最后一行 |
我们在上一段中说了,默认的ResultSet
对象是不可更新的,并且游标只能向后(next)移动,那上面的这么多方法,要如何才能使用呢?这里需要我们在创建Statement
、PreparedStatement
时指定结果集类型和并发类型,我们来看一下其中的几个值(这几个常量在java.sql.ResultSet中):
方法名 | 功能描述 |
---|---|
TYPE_FORWARD_ONLY(1003) | 结果集类型,只能向后移动 |
TYPE_SCROLL_INSENSITIVE(1004) | 结果集类型,可滚动,但是对数据库中的数据变更不care |
TYPE_SCROLL_SENSITIVE(1005) | 结果集类型,可滚动,并且数据库中的数据变更会直接影响结果集 |
CONCUR_READ_ONLY(1007) | 并发类型,只读,结果集获取后不会被更新 |
CONCUR_UPDATABLE(1008) | 并发类型,结果集获取后可能被更新 |
因此我们在创建Statement
、PreparedStatement
时指定这两个参数即可,这里推荐使用1004和1007,否则可能会发生你意想不到的错误,示例代码如下。这样创建的Statement
、PreparedStatement
对象在执行查询时返回的结果集,游标就可以自由的飞翔了。
//也可直接使用con.createStatement(1004, 1008)
Statement pstmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet
.CONCUR_READ_ONLY);
PreparedStatement pstmt = con.prepareStatement("your sql", 1004, 1008);
当我们把游标移动到数据行后,就可以使用JDBC提供的getXxx
方法来获取对应记录中字段的值了。这里的getXxx方法和PreparedStatement中的setXxx方法类似,会自动的将SQL数据类型和Java中的数据类型进行转换。我们通过代码简单的看下如何使用
PreparedStatement pstmt = con.prepareStatement("select * from users");
// 获取查询结果集
resultSet = statement.executeQuery(sql);
//resultSet移动游标(Cursor)到结果的第一行
while (resultSet.next()) {
//通过getXxx方法获取查询结果
System.out.println("name:" + resultSet.getString("name"));
System.out.println("password:" + resultSet.getString("password"));
System.out.println("birthday:" + resultSet.getDate("birthday"));
}
3.JDBC编程步骤
讲完了理论,让我们来简单的动动手,了解通过JDBC进行数据库操作的步骤。
- 加载并注册数据驱动
我们可以通过显示的注册驱动,可通过如下三种方式:
//java.sql可省略,import正确的类即可,Driver为数据库驱动中实现了Driver接口的驱动类
java.sql.DriverManager.registerDriver(new Driver);
//将Driver类加载至内存,由其中的静态方法自动完成注册,xxx表示Driver类在connetor jar包中的位置
Class.forName("xxx.Driver")
//设置应用中的系统属性jdbc.drivers,或者增加JVM的启动参数
System.setProperty("jdbc.drivers", "xxxx.Driver");
我们还可以什么都不做,让DriverManager
自动帮我们注册。
- 通过
DriverManager
获取数据库的连接Connection
这里同样有三种方式来建立连接,代码如下:
//第一种
String url = "jdbc:mysql://localhost:3306/java_web?useSSL=false&characterEncoding=utf-8";
String userName = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, userName, password);
//第二种
//java.util.Properties
Properties properties = new Properties();
properties.put("user", userName);
properties.put("password", password);
//第一种和第二种本质上是相同的
connection = DriverManager.getConnection(url, properties);
//第三种 将用户名密码写入url中
url = "jdbc:mysql://localhost:3306/java_web?user=root&password=123456" + "&useSSL=false&characterEncoding=utf-8";
connection = DriverManager.getConnection(url);
- 通过Connection对象获取
Statment
对象或PreparedStatement
对象或CallableStatement
对象
代码如下,以下三个对象都是位于java.sql包中,import的时候需注意:
//获取Statment对象
Statement statement = connection.createStatement();
//获取PreparedStatement对象
PreparedStatement preparedStatement = connection.prepareStatement("your sql");
//获取CallableStatement对象
CallableStatement callableStatement = connection.prepareCall("your sql");
- 使用获取的
Statment
对象执行SQL语句(以Statment
为例)
可使用如下三种方式执行SQL语句:
boolean execute()
:可以执行任何SQL语句;ResultSet executeQuery(String sql)
:通常执行查询(SELECT)语句,执行后返回代表结果集的ResultSet
对象;int executeUpdate(String sql)
:主要用于执行DML、DDL语句,返回数据库中此次执行影响的行数,如果执行DDL语句(无返回),则返回0
。
- 处理返回结果
这里的返回结果分为三种,也就是上面执行SQL语句的三种方式的返回值。我们需要获取SQL的执行结果,并完成解析,之后完成项目自身的业务逻辑。
ResultSet
的解析在上文已经讲解过了,故不在赘述。
- 关闭连接,释放资源
需要执行的SQL执行完毕,即此次的数据库操作已经完成,我们需要关闭数据库连接、Statement
对象、ResultSet
对象。
代码如下:
//关闭连接,释放资源
public static void release(ResultSet resultSet, Statement statement, Connection connection) {
//关闭结果集
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
resultSet = null;
}
//关闭Statement对象
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
statement = null;
}
//关闭Connection连接
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
connection = null;
}
}
一个完成的操作实例可以参考这篇文章:Java Web数据库开发(MySQL)之环境准备。
4.总结
本文到这,JDBC中的重要的接口和类都已讲解完毕,后续会单独开文章来讲解Statment
对象、PreparedStatement
对象、CallableStatement
对象的具体使用。
因为本文主要是对JDBC的详细介绍,只稍微的提到了MySQL Connector中的Driver类,后续有机会会单独的写文章进行MySQL Connectior的源码分析。
参考阅读:
又到了分隔线以下,本文到此就结束了,本文内容全部都是由博主自己进行整理并结合自身的理解进行总结,如果有什么错误,还请批评指正。
Java web这一专栏会是一个系列博客,喜欢的话可以持续关注,如果本文对你有所帮助,还请还请点赞、评论加关注。
有任何疑问,可以评论区留言。