您的位置 首页 java

我的Java Web之路49 – JDBC初步使用

本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多(Java)码农和想成为(Java)码农的人。 

目录

  1. 介绍
  2. jdbc 规范
  3. 添加JDBC驱动的JAR包
  4. 注册JDBC驱动器类
  5. 连接数据库
  6. 数据库运维
  7. 执行 sql
  8. 总结

介绍

介绍了数据库应用的开发步骤,一般包括数据库选型、 数据库设计 、编写数据库访问代码三个步骤。而数据库选型又因开发环境、测试环境和生产环境的不同而不同。

所以,我们在编写数据库访问代码时,要尽量使用符合SQL标准的语法。关于SQL和SQL标准知识,大家可以自行搜索。

因为我们的租房网应用只是用作演示,所以它的数据库选型和数据库设计就从简而行,我就直接使用H2Database(简称H2,可以参考 )这个数据库作为JDBC的驱动。

JDBC规范

要想了解JDBC如何使用,那最权威的资料莫过于JDBC规范了。

JDBC规范的下载与Servlet等规范类似,都可以在JCP官网()找到,具体如何下载可以参考 。

截止到本文成稿之日,JDBC规范的最新版本好像是4.3,对应JSR221,是在2017年完成的。

JDBC规范的内容也不算少,足足有二百多页,包括:

  1. 介绍
  2. 目标
  3. 新特性
  4. 概述
  5. 类和接口
  6. 兼容性
  7. 数据库元数据
  8. 异常
  9. 数据库连接(Connections)
  10. 事务
  11. 连接池
  12. 分布式事务
  13. SQL声明( statement s)
  14. 批量更新
  15. 结果集
  16. 高级数据类型
  17. 自定义类型映射
  18. 与连接器的关系
  19. 包装器(Wrapper)接口

由于涉及内容太多,我们只能介绍JDBC最基本的用法。

添加JDBC驱动的JAR包

的结尾我们提到过,JDBC仅仅是一套接口,它的使用模式与接口的普遍性使用模式是类似的。

租房网应用作为使用JDBC的外部程序,它也需要 配置一个具体的JDBC实现 才行,这就是 JDBC驱动 。基本上,市面上各个数据库(特别是关系数据库)厂商都会提供自己的JDBC驱动,H2也不会例外。

那如何在租房网应用中添加H2的JDBC驱动的JAR包呢?我们当然可以到H2官网()去下载它,然后在IDE中配置第三方依赖库(可以参考 )。

不对,我们不是学习过 Maven 了吗?可以让Maven来帮助我们管理依赖啊。我们在 中也已经把租房网应用改造成使用Maven来管理依赖了,比如Spring等依赖。我们当然也可以直接使用Maven来添加H2的JDBC驱动这个依赖啊。

H2的所有东西都在一个JAR包里,当然也包括了它的JDBC驱动了,我们在 中下载的JAR包是 h2-1.4.200.jar 。

现在,我们像添加Spring依赖那样先到Maven仓库中( )搜索H2这个依赖,后面的在POM文件中添加依赖等具体步骤就不再赘述了,可以参考 。

H2的依赖在Maven中的描述是这样的:

<!--  -->
<dependency>
 <groupId>com.h2database</groupId>
 <artifactId>h2</artifactId>
 <version>1.4.200</version>
 <scope>test</scope>
</dependency>
 

将它复制到我们租房网应用的POM文件中,这样,添加H2的JDBC驱动的JAR包就一切大功告成了,当然,你必须确保你的机器能够连接上互联网,因为Maven会自动到Maven仓库中下载。

其他数据库的JDBC驱动的JAR包也可以同样方式添加到我们的租房网应用中。

注册JDBC驱动器类

在使用JDBC的外部程序(这里就是我们的租房网应用) 配置一个JDBC驱动 ,首先要有该驱动,上面已经解决这个问题了。

然后需要在外部程序中注册该驱动的驱动器类。如何注册呢?事实上,满足 JDBC 4 规范的驱动会自动注册到外部程序中,而我们添加的H2的JDBC驱动是符合 JDBC 4 规范的。因此,这一步就可以跳过了。

事实上,JDBC驱动的这种自动注册机制是使用了JAR规范的一个特性,包含 META-INF/services/java.sql.Driver 文件的JAR文件可以自动注册驱动器类。我们可以用压缩/解压缩工具打开 h2.1.4.200.jar 看看是否有这么个文件,当然,在Eclipse中可以直接看到:

不过,还是介绍一下如何解决JDBC驱动不能自动注册的话该如何手动注册的问题。有两个方法:

  • 在使用JDBC的外部程序中编写:
Class.forName("org.h2.Driver"); 
  • 是在启动外部程序的命令行参数中指定 jdbc.drivers 这个属性:
Java -Djdbc.drivers=org.h2.Driver 外部程序的名字 

当然,这种方式的变种是在外部程序中编写API接口来设置这个属性:

System.setProperty("jdbc.drivers", "org.h2.Driver"); 

假设我们的H2的JDBC驱动是不能自动注册的,那么我们应该手动注册它(当然,手动注册也是没有任何问题的,只不过是多此一举而已)。

那么,应该在哪个地方添加手动注册的代码呢?因为目前我们只有在房源服务(HouseService类)里使用到了模拟的房源数据,而未来我们是要将房源数据持久化到数据库中的,所以我们暂且把它加到这个类中吧。

与模拟的房源数据一样,把注册JDBC驱动器类的代码放到HouseService类的构造函数中:

public HouseService() throws ClassNotFoundException {
Class.forName("org.h2.Driver");
 
mockHouses = new ArrayList<House>();
mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息"));
mockHouses.add(new House("2", "万科橙9-1-501", "详细信息"));
} 

当添加 Class.forName() 这一句代码后,可以看到Eclipse自动提示有编译错误:

Unhandled exception type ClassNotFoundException 

这是因为Class类的forName()方法会抛出必须要我们处理的异常,如果不处理它,那就继续抛给调用者,因此,我们在构造方法的声明中添加了 throws ClassNotFoundException 这一部分,关于Java异常,我们以后详细介绍。

好,现在让我们在Eclipse中发布租房网应用到Tomcat,并启动Tomcat(可以参考 ),然而在Eclipse的Console(控制台)窗口中却看到众多异常,这些异常形成一条 异常链 (即最底层的异常抛到外部调用者后,外部调用者捕获后包装成另一个异常再抛给它的外部调用者,如此重复下去,直到最顶层异常被处理后不再往外抛出):

可以看到,最底层的异常就是 Class.forName() 因为找不到 org.h2.Driver 这个类而抛出的异常,随后导致 HouseService 的构造函数抛出异常,因此不能实例化 HouseService ,再导致Spring IoC框架找不到 HouseService 实例来注入到 HouseRenterController 这个Bean,最后导致 dispatcher 这个Servlet初始化失败。

那到底是何原因找不到H2的JDBC驱动器类 org.h2.Driver 呢?原来是POM文件中的H2依赖的配置在作怪,我们必须把 <scope> 标签的内容设置为 compile ,或直接去掉该标签

<!--  -->
<dependency>
 <groupId>com.h2database</groupId>
 <artifactId>h2</artifactId>
 <version>1.4.200</version>
 <!-- <scope>test</scope> 必须去掉此标签-->
</dependency> 

从标签的名字可以得知它是设置某种范围的,配置为 test ,则表示该依赖只在执行测试时有效;配置为 compile (此为默认值),则表示该依赖在执行编译时有效(实际上是任何时候都有效,是程度最强的依赖)。

关于Maven的依赖的相关知识,以后再详细介绍。

现在,我们重新发布应用,启动Tomcat,可以发现一切正常矣!

连接数据库

接下来,我们就可以使用JDBC API来建立与数据库的连接(数据库往往单独部署在独立的 数据库服务器 上,因此,需要通过网络来访问数据库)。

JDBC API主要被包含在 java.sql 和 javax.sql 这两个包中,在JDK的 rt.jar 中可以找到它们,属于Java的标准(运行时)库。

使用JDBC API中的 DriverManager 类的 getConnection() 方法即可建立与数据库的连接。

getConnection() 方法有多个重载版本(关于重载,可以参考 ),其中一个版本就是:

Connection getConnection(String url, String user, String password) throws SQLException
 

它返回一个数据库连接,但需要传入一些参数,一般情况下,连接数据库最基本的要求是要知道如下信息:

  • 数据库URL,它包括:数据库服务器的种类(对应具体的数据库厂商)、IP地址或域名、端口、数据库名字等等;
  • 能够访问该数据库的用户名、密码。

用户名和密码都有对应的参数,是字符串类型的,显然是由DBA为我们分配的。那这个数据库URL该怎么写呢?

既然我们用的是H2,那我们到它的官网上肯定能找到答案吧。否则的话,我们得到它却不知如何使用,那H2的作者图个啥!费过一番九牛二虎之力后,终于在其官网()的左侧导航栏中的 Features 中找到了相关内容:

它支持的URL远比我们想象的要复杂的多,但基本格式都是 jdbc:h2: 开头,前缀 jdbc: 实际上是JDBC规范规定的格式,后面的 h2: 当然是指H2Database这个数据库了。

可以看到,H2为我们提供了多种模式,我们可以选择适合我们的模式。但对我们租房网应用来说,或者说对于在开发阶段的应用来说,使用嵌入式模式或者内存模式是最简单的,因为我们无需单独部署数据库服务器。我们这里就直接使用嵌入式模式吧: jdbc:h2:~/h2db/houserenter 。这里没有数据库服务器的IP地址和端口,有的只是一个路径,最后的houserenter就是一个文件,它就代表一个数据库,实际的数据就存在该文件中。当然,文件的格式我们是不知道的也不用知道。

那用户名和密码是什么呢?实际上,这里可以随便填写。但在测试环境和生产环境中,或者数据库是独立部署的环境中,用户名和密码是由DBA负责分配的。

现在,我们的HouseService类的构造函数变成:

public HouseService() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
String url = "jdbc:h2:~/h2db/houserenter";
String user = "sa";
String password = "";
Connection conn = DriverManager.getConnection(url, user, password);
 
mockHouses = new ArrayList<House>();
mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息"));
mockHouses.add(new House("2", "万科橙9-1-501", "详细信息"));
} 

注意,getConnection()方法也会抛出必须处理的异常 SQLException ,我这选择直接抛给HouseService类的构造函数的调用者。

现在,让我们重新发布应用并启动Tomcat,验证一下是否有问题,结果正常,我们也可以到 ~/h2db (波浪线实际是指你登录操作系统的用户的家目录,在Windows系统中,一般是 C:Users用户名)这个目录下看看是否存在 houserenter 这个文件:

数据库运维

数据库的运维,即数据库服务器的部署和搭建,数据库、表、索引等的创建、授权,数据的备份和迁移,数据的分库分表分区等工作。这些工作往往有专门人员来负责,就是数据库管理员,即DBA。

我们做好数据库选型和设计之后,在测试环境和生产环境中(有的公司开发环境也需要),一般是向DBA申请服务器资源,让他们部署和搭建数据库服务器,建库建表等。

当然,你也可以自己一人包揽所有这些工作。

最后,一定是启动数据库服务器,这样我们的数据库应用才能建立数据库连接,当然必须保证数据库URL是正确的。即数据库连接能够建立的条件必须包括:

  • 数据库服务器在运行中;
  • URL必须正确,即数据库服务器的IP、端口必须正确,数据库的名字所指定的数据库必须已经存在;
  • 访问该数据库的用户名、密码必须正确。

而H2支持嵌入式模式,则没有诸多限制,所以在开发环境中使用是最合适不过了!

执行SQL

现在,连接已经建立好了,我们就可以用它来执行SQL语句了。不过,Connection接口并没有执行SQL的方法,而是提供了一个创建 Statement 对象的方法:

Statement createStatement() throws SQLException 

而Statement接口拥有众多执行SQL的方法,最常用的有两个:

int java.sql.Statement.executeUpdate(String sql) throws SQLException

ResultSet java.sql.Statement.executeQuery(String sql) throws SQLException


 

正如方法名字所示,executeUpdate()方法主要是用来执行insert、update或者delete等数据更新(写)操作的SQL语句的(这些语句叫数据操纵语言,即Data Manipulation Language,简称DML)。当然,它还可以执行建立、删除数据表等操作的SQL语句(这些语句叫数据定义语言,即Data Definition Language,简称DDL)。

而executeQuery()方法是用来执行select等数据查询操作的SQL语句的(也属于DML)。它返回查询结果集,我们就可以使用ResultSet接口来访问查询结果了。

大家可以通过IDE或者其他方式查看它们的JavaDoc。

我们之前建立的数据库连接中使用的是 jdbc:h2:~/h2db/houserenter (简称houserenter数据库),这个库中也是H2刚刚建立的,因此该库中还没有任何数据表,所以我们要先使用 create table 语句建立数据表。

根据 我们对租房网应用的数据库设计,房源表包含三个字段,于是执行SQL的代码如下:

statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))"); 

但是,如果我们把它放到HouseService类的构造函数的话,岂不是每次启动应用时都要建立一次house表,大家可以试试,这样的话第二次启动应用时会抛出house表已经存在的异常。

那该怎么办呢?我们可以在每次启动应用时都使用 drop table 语句将原来的house表删除,然后再执行建表语句:

statement.executeUpdate("drop table if exists house"); 

接着,我们再使用 insert 语句插入模拟的房源数据(因为我们的租房网应用还没有提供发布房源的功能):

statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '详细信息') ");
statement.executeUpdate("insert into house values('2', '万科橙9-1-501', '详细信息') "); 

现在,我们的HouseService类的构造方法变成这样:

public HouseService() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
String url = "jdbc:h2:~/h2db/houserenter";
String user = "sa";
String password = "";
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
 statement.executeUpdate("drop table if exists house");
 statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))");
 statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '详细信息') ");
 statement.executeUpdate("insert into house values('2', '万科橙9-1-501', '详细信息') ");
 
mockHouses = new ArrayList<House>();
mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息"));
mockHouses.add(new House("2", "万科橙9-1-501", "详细信息"));
} 

executeUpdate()方法的返回值是一个整型值,表示执行该SQL语句后受影响的行数,大家可以在上述代码的基础上打印每一次executeUpdate()方法的返回值,看看返回值是多少。

这样,我们的数据准备已经完成,下面应该使用executeQuery()方法来改造查询房源的方法了,原来的是这样的:

public House findHouseById(String houseId) {
for (House house : mockHouses) {
if (houseId.equals(house.getId())) {
return house;
}
}
return null;
} 

首先,我们仍然需要建立数据库连接,创建Statement对象,因为上面使用的都是局部变量。这不就有代码重复了吗?没错,我们需要消除这种重复,不过,我们暂且把这个任务放一边,先重点看看如何查询数据库,如何访问查询结果集。

根据房源ID查询房源的SQL语句很简单:

select id,name,detail from house where id = '这里是房源ID' 

查询条件房源ID是通过方法的参数传入的,我们可以利用Java的字符串拼接功能,拼成上述SQL语句

ResultSet rs = statement.executeQuery("select id,name,detail from house where id = '" + houseId + "'");
 

要特别注意拼接SQL语句时用到的单引号和双引号(使用PreparedStatement可以避免此现象,还可以提高性能,以后讨论),很容易出错,而且容易让黑客执行 SQL注入攻击 ,这里就不介绍了。

当然,你必须确保house表存在,而且它有 id、name、detail 三列,这也是非常容易出错的地方,很明显,我们的代码与数据库设计紧耦合了,因此,我们需要思考一下怎么样才能解耦!

言归正传,得到结果集之后,我们就可以访问结果集中的数据了。因为查询结果可能是多条记录,或者零条记录(当然,我们这里house表是以房源ID为主键的,而查询条件就是房源ID,因此查询结果要么该房源ID的房源不存在,要么就只能有一条房源),所以必须遍历结果集,ResultSet接口提供的是 next()方法:

 while (rs.next()) {
return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"));
 } 

当然,一旦访问到一条房源记录,那就可以直接返回了。

使用next()方法指向结果集的下一条记录后(若无下一条则返回false),就可以用ResultSet接口提供的getXXX()方法来访问每一列的值了,这里的 XXX 可以是各种数据类型,比如Byte、Short、Int、Long、Float、Double、Date、String等等,传入的参数既可以是该列在select语句中的位置,也可以是selcet语句中出现的列名。

于是,我们的查询房源的方法就变为了:

public House findHouseById(String houseId) {
try {
String url = "jdbc:h2:~/h2db/houserenter";
String user = "sa";
String password = "";
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select id,name,detail from house where id = '" + houseId + "'");

while (rs.next()) {
return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"));
 }
} catch (SQLException e) {
System.out.println("HouseService findHouseById SQLException: " + e. getMessage () + "! houseId=" + houseId);
}

return null;
} 

需要注意的是,我这里采用了另外一种处理异常的方式,即使用 try-catch 语句捕获抛出的必须处理的异常,并处理它(仅仅打印日志而已, 往往要把异常的上下文信息打印出来,便于查找问题 )。

另外一个查询用户感兴趣房源的方法,也可以做相应的修改,HouseService类的构造方法中原来的添加模拟房源数据的代码也可以删除了,最后的HouseService变成了这样:

package houserenter.service;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import houserenter.entity.House;
@Service
public class HouseService {
public HouseService() throws ClassNotFoundException, SQLException {
Class.forName("org.h2.Driver");
String url = "jdbc:h2:~/h2db/houserenter";
String user = "sa";
String password = "";
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
 statement.executeUpdate("drop table if exists house");
 statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))");
 statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '详细信息') ");
 statement.executeUpdate("insert into house values('2', '万科橙9-1-501', '详细信息') ");
}
public List<House> findHousesInterested(String userName) {
// 这里查找该用户感兴趣的房源,省略,改为用模拟数据
List<House> houses = new ArrayList<>();
try {
String url = "jdbc:h2:~/h2db/houserenter";
String user = "sa";
String password = "";
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select id,name,detail from house");


while (rs.next()) {
houses.add(new House(rs.getString("id"), rs.getString("name"), rs.getString("detail")));
 }
return houses;
} catch (SQLException e) {
System.out.println("HouseService findHousesInterested SQLException: " + e.getMessage() + "! userName=" + userName);

}

return houses;
}
public House findHouseById(String houseId) {
try {
String url = "jdbc:h2:~/h2db/houserenter";
String user = "sa";
String password = "";
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select id,name,detail from house where id = '" + houseId + "'");

while (rs.next()) {
return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"));
 }
} catch (SQLException e) {
System.out.println("HouseService findHouseById SQLException: " + e.getMessage() + "! houseId=" + houseId);
}

return null;
}
} 

我们可以重新发布应用并启动Tomcat,然后用浏览器访问租房网应用进行验证,基本上没有问题,唯一存在功能上的问题就是,我们编辑的房源没有存到数据库中,所以我们需要为HouseService类增加一个更新房源的方法,具体实现如下:

public void updateHouseById(House house) {
try {
String url = "jdbc:h2:~/h2db/houserenter";
String user = "sa";
String password = "";
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
statement.executeUpdate("update house set id='" + house.getId()
+ "', name='" + house.getName()
+ "', detail='" + house.getDetail()
+ "' where id = '" + house.getId() + "'");
} catch (SQLException e) {
System.out.println("HouseService updateHouseById SQLException: " + e.getMessage() + "! house=" + house.toString());
}
} 

要非常注意各列的类型是字符串,因此SQL语句中各列的值要加上单引号!

而我们的HouseRenterController控制器类的处理房源编辑表单的Handler方法由原来的:

@PostMapping("/house-form.action")
public ModelAndView postHouseForm(String userName, House house) throws UnsupportedEncodingException {
// 这里需要验证用户是否登录,省略
//更新指定房源的详情
House target = houseService.findHouseById(house.getId());
target.setName(house.getName());
target.setDetail(house.getDetail());
//将请求转发到查找房源详情的动作
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + house.getId());
return mv;
} 

修改为:

@PostMapping("/house-form.action")
public ModelAndView postHouseForm(String userName, House house) throws UnsupportedEncodingException {
// 这里需要验证用户是否登录,省略
//更新指定房源的详情
houseService.updateHouseById(house);
//将请求转发到查找房源详情的动作
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + house.getId());
return mv;
} 

现在,再重新验证一下,编辑房源也没有问题了!

总结

  • JDBC仅仅是一套接口,实际使用要配置JDBC驱动;
  • JDBC API的使用步骤依次是:注册驱动器类、连接数据库、获取Statement对象、执行SQL语句;
  • 使用Statement接口的executeUpdate()方法执行更新操作的SQL语句(insert、update、delete和DDL等);executeQuery()方法执行查询操作的SQL语句(select);
  • 遍历查询结果集使用ResultSet接口的next()方法和getXXX()方法;

但是,我们使用JDBC的代码还有很多不足:

  • 有很多代码重复的地方;
  • 访问数据库的代码没有独立出来(事实上,应该从Service层独立出来形成DAO层);
  • 访问数据库的一些资源没有释放,比如连接、Statement、结果集;
  • 每次访问都要建立数据库连接,性能低下;
  • 与数据库设计耦合严重;
  • 正文代码中仍然有用于测试的添加模拟数据的代码;
  • 数据库访问的异常处理不够;
  • 等等

不管怎样,我们向数据持久化迈出了一步!

文章来源:智云一二三科技

文章标题:我的Java Web之路49 – JDBC初步使用

文章地址:https://www.zhihuclub.com/184437.shtml

关于作者: 智云科技

热门文章

发表回复

您的电子邮箱地址不会被公开。

网站地图