写给求甚解的你---JDBC详解

​ 说起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中的技术细节了,但是想研究源码的除外😝。

​ 因此,我们注册的数据库驱动的方式有两种:

  1. java.sql.DriverManager.registerDriver(new Driver),其中的Driver为数据库驱动中的Driver接口的实现类;
  2. 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还有另两个重载的方法,如下所示:

  1. Connection getConnection(String url):用户名、密码在url中,如:“jdbc:mysql://localhost:3306/java_web?user=root&password=123456”;
  2. 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方法获得的。

​ 这里有个概念需要明确下,不管是后面要将的Statement还是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语句会被预编译。

​ 需注意getResultSetgetUpdateCount方法,需要在execute方法执行返回成功后才能获取到值,并且一次执行只能获取一次。因此如果不是在执行一个动态的未知SQL,尽量不要使用execute方法。

2.5 PreparedStatement接口

PreparedStatement是一个用于表示预编译的SQL语句的对象。传入的SQL语句已完成预编译并存储在PreparedStatement对象中,之后就可以可以使用该对象有效地多次执行该语句

​ 通过上面一段话,我们就可以简单的看出StatementPreparedStatement的区别,即出于提高多次执行同一个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)移动,那上面的这么多方法,要如何才能使用呢?这里需要我们在创建StatementPreparedStatement时指定结果集类型和并发类型,我们来看一下其中的几个值(这几个常量在java.sql.ResultSet中):

方法名功能描述
TYPE_FORWARD_ONLY(1003)结果集类型,只能向后移动
TYPE_SCROLL_INSENSITIVE(1004)结果集类型,可滚动,但是对数据库中的数据变更不care
TYPE_SCROLL_SENSITIVE(1005)结果集类型,可滚动,并且数据库中的数据变更会直接影响结果集
CONCUR_READ_ONLY(1007)并发类型,只读,结果集获取后不会被更新
CONCUR_UPDATABLE(1008)并发类型,结果集获取后可能被更新

​ 因此我们在创建StatementPreparedStatement时指定这两个参数即可,这里推荐使用1004和1007,否则可能会发生你意想不到的错误,示例代码如下。这样创建的StatementPreparedStatement对象在执行查询时返回的结果集,游标就可以自由的飞翔了。

//也可直接使用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进行数据库操作的步骤。

  1. 加载并注册数据驱动

​ 我们可以通过显示的注册驱动,可通过如下三种方式:

//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自动帮我们注册。

  1. 通过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);
  1. 通过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");
  1. 使用获取的Statment对象执行SQL语句(以Statment为例)

​ 可使用如下三种方式执行SQL语句:

  • boolean execute():可以执行任何SQL语句;
  • ResultSet executeQuery(String sql):通常执行查询(SELECT)语句,执行后返回代表结果集的ResultSet对象;
  • int executeUpdate(String sql):主要用于执行DML、DDL语句,返回数据库中此次执行影响的行数,如果执行DDL语句(无返回),则返回0
  1. 处理返回结果

​ 这里的返回结果分为三种,也就是上面执行SQL语句的三种方式的返回值。我们需要获取SQL的执行结果,并完成解析,之后完成项目自身的业务逻辑。

ResultSet的解析在上文已经讲解过了,故不在赘述。

  1. 关闭连接,释放资源

​ 需要执行的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的源码分析。

参考阅读:

  1. Java Web数据库开发(MySQL)之环境准备
  2. MySQL Connector源码下载地址
  3. mysql-connector jar包及包含源码的jar包的下载地址

​ 又到了分隔线以下,本文到此就结束了,本文内容全部都是由博主自己进行整理并结合自身的理解进行总结,如果有什么错误,还请批评指正。

​ Java web这一专栏会是一个系列博客,喜欢的话可以持续关注,如果本文对你有所帮助,还请还请点赞、评论加关注。

​ 有任何疑问,可以评论区留言。

  • 38
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李子树_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值