前言
这部分的知识如果在了解一些数据库的基本知识与操作之后学习的话会比较有感触,所以如果你没有数据库的基础,我建议大家先阅读一下我推荐的这篇文章,对我们的学习是非常有必要的(使用MysQL数据库)。
现在我们来说一下正题,在Java中,如果我们要使用数据库应该怎么办?别担心,Java给我们提供了JDBC的标准接口,而它就是专门用来执行SQL的解决方案。我刚开始看这部分的内容的时候,有很多地方都不明白,看了两遍才开始在这里写总结,所以如果你也觉得晦涩难懂的话,不妨多看几遍,相信一定可以掌握这部分知识。
JDBC简介
在我们介绍JDBC之前,我们应该先了解一下应用程序是如何与数据库进行沟通的。数据库本身就是个独立的应用程序,我们攥写的应用程序是利用通信协议对数据库进行指令的交换,以进行数据的增删查找。
通常来说,我们的应用程序会利用一组专门与数据库进行通信协议的链接库,以简化与数据库沟通时的程序攥写。我们来看一张图片:
我觉得这个图片是比较清楚的反映了Java应用程序要与数据库进行链接所需要经过的步骤。
也许很多人会问,我们为什么需要JDBC,其实JDBC是JAVA链接数据库的简称,它提供了链接各种常用数据库的能力,即“写一个Java程序,操作所有数据库”的目的。
所以如果我们在开发应用程序操作数据库的时候,是通过JDBC提供的接口来设计程序,理论上在必须更换数据库时,应用程序无须进行修改,只要更换数据库驱动程序,即可对另一个数据库进行操作。
如今厂商在操作JDBC驱动程序时,依操作方式可将驱动程序分为4种类型,我们只要了解第4种类型“Native Protocol Driver”就可以了,它算是最常见的驱动程序类型。它的特点是通常由数据库厂商直接提供,驱动程序操作会将JDBC调用转换为与数据库特定的网络协议,以与数据库进行沟通操作。由于这种类型驱动程序的主要作用就是将JDBC调用转换为特定网络协议,所以驱动程序可以使用纯粹Java技术实现,因此这种类型驱动程序可以跨平台,在效能上也由不错的表现。我们接下来使用的就是这种类型的驱动程序。
连接数据库
为了要链接数据库系统,必须要有厂商操作的JDBC驱动程序,必须在CLASSPATH中设定驱动程序的JAR文档。如果使用IDE,程序项目会有管理CLASSPATH的方式,通常是“新增JAR”之类的指令。接下来我为大家示范怎么使用IntelliJ IDEA添加JAR文档。
1.
首先我们需要在下面的链接中获得MySQL的JDBC驱动程序:
点击进入MySQL的JDBC驱动程序下载页面
点击后进入的页面应该是这个样子的:
下载我圈出来的那一项。点击下载之后会让我们选择平台,直接下载下图中的版本(tar.gz):
下载完之后我们对它进行解压缩,里面是由一个jar包的。
2.
我们将jar包添加到IDEA的项目之中。操作步骤如下:
如图,我们选择File中的Project Structure选项,然后会出现如下窗口:
从上图可以看到,进入窗口之后,先进入Dependencies选项,然后点击右边的绿色小加号就可以选择在目录中选择jar包,可以看到我已经将mysql的jar包添加进去了。
3.
最后我们选择关联的数据库,步骤如下:
点击选项之后进行如下选择:
然后就是最后的步骤了:
我们从上图可以看到,我们需要写入一些东西,Database就填写你要关联的数据库名称(mysql),User就填root,Password填你访问数据库时需要的密码,然后有个Test connection可以测试是否可以连接数据库,点击之后如果出现success,就说明可以连接数据库。
基本数据库操作相关的JDBC接口或类是位于java.sql包中。在程序中要取得数据库联机,我们必须有几个动作:
- 注册Driver操作对象;
- 取得Connection操作对象;
- 关闭Connection操作对象。
注册Driver操作对象
操作Driver接口的对象是JDBC进行数据库存取的起点,以MySQL操作的驱动程序为例,com.mysql.jdbc.Driver类操作了java.sql.Driver接口,管理Driver操作对象的类是java.sql.DriverManager。基本上,必须调用其静态方法registerDriver()进行注册:
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
不过实际上我们很少自行攥写程序代码进行这个动作,只要我们想办法加载Driver接口的操作类.class文档,就会完成注册。例如可以使用java.lang.Class类的forName方法,动态加载驱动程序类:
try{
Class.forName("com.mysql.jdbc.Driver");
}catch(ClassNotFoundException ex){
throw new RuntimeException("找不到指定的类");
}
有兴趣的同学可以看看MySQL的Driver类的操作原始码,其实还是用到了注册Driver实例的动作,也就是上面说的静态方法进行注册。
使用JDBC时,要求加载.class文档的方式有4种,我们上面说的是两种,还有两种感兴趣的伙伴可以下去了解。
当然,应用程序有可能同时联机多个厂商的数据库,所以DriverManager也就可以注册多个驱动程序实例。
取得Connection操作对象
Connection接口的操作对象是数据库联机代表对象,要取得Connection操作对象,可以通过下面的代码来实现:
Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
除了基本的用户名称,用户密码之外,还必须提供JDBC URL,其定义了连接数据库时的协议,子协议,数据源识别:
协议:子协议:数据源识别
除了协议是以jdbc开始之外,JDBC URL格式各家都不相同,必须查询数据库产品的使用手册。下面以MySQL为例,“子协议”是桥接的驱动程序,数据库产品名称或联机机制,MySQL的子协议就是mysql。“数据源识别”标出数据库的地址,端口号,名称,用户,密码等信息。举个例子,MySQL的JDBC的URL攥写方式如下:
jdbc:mysql://主机名:端口/数据库名称?参数=值&参数=值
主机名可以是本机(localhost)或其他联机主机名,地址,MySQL端口默认为3306。如果要使用中文存取,还必须给定参数useUnicode及characterEncoding,表明是否使用Unicode,并指定字符编码方式。例如:
jdbc:mysql://localhost:3306/demo?user=root&password=123&useUnicode=true&characterEncoding=UTF8
如果要直接通过DriverManager的getConnection()连接数据库,一个比较完整的代码段如下:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Test {
public static void main(String[] args) throws SQLException {
String url = "jdbc:mysql://localhost:3306/demo";
String user = "root";
String password = "123";
Connection conn = null;
SQLException ex = null;
try {
conn = DriverManager.getConnection(url, user, password); //取得Connection操作对象
} catch (SQLException e) {
ex = e;
}
finally {
if(conn != null){
try{
conn.close(); //取得Connection对象之后进行关闭
}catch (SQLException e){
if(ex == null){
ex = e;
}
else {
ex.addSuppressed(e);
}
}
}
}
if(ex != null){
throw new RuntimeException(ex);
}
}
}
在上面的代码中,SQLException是在处理JDBC时常遇到的对象,为数据库操作过程中发生错误时的代表对象,SQLException是受检异常,必须使用try…catch…finally明确处理,在异常发生时尝试关闭相关资源。
关闭Connection操作对象
取得Connection对象之后,可以使用isClosed方法测试与数据库的连接是否关闭。在操作完数据库之后,若确定不在需要连接,则必须使用close来关闭与数据库的连接,以释放连接时相关的必要资源,像是联机相关对象,授权资源等。
对于上面代码中的try…catch…finally部分,由于JDK7之后,JDBC的Connection,Statemet,ResultSet等接口都是java.lang.AutoClosable的子接口,所以我们可以尝试自动关闭语法来简化程序的攥写。
那么在底层,DriverManager使如何进行联机的呢?DriverManager会在循环中逐一取出注册的每个driver实例,使用指定的JDBC URL来调用Driver的connect方法,尝试取得Connection实例。对于底层的实现,有兴趣的伙伴可以看看DriverManager中相关的原始码。
我们现在看一个例子,测试一下是否可以联机数据库并取得Connection实例,在这之前我们先使用create database 【数据库名】指令在MySQL数据库中创建demo数据库。
public class ConnectionDemo {
public static void main(String[] args) throws ClassNotFoundException, SQLException{
Class.forName("com.mysql.jdbc.Driver"); //加载驱动程序
String jdbcUrl = "jdbc:mysql://localhost:3306/demo";
String user = "root";
String passwd = "******";
//尝试自动关闭资源
try(Connection conn = DriverManager.getConnection(jdbcUrl, user, passwd)){
out.printf("已 %s 数据库联机\n", conn.isClosed()? "关闭" : "开启");
}
}
}
在上面的代码中我将密码进行了掩盖,这个程序是可以正常运行的,有兴趣的同学可以试着自己运行一下。
实际上很少直接从DriverManager中取得Connection。想一想,如果你设计一个API,用户如果无法提供JDBC URL,名称,密码时,你要怎么取得Connection?这就要用到我接下来要介绍的java.sql.DataSource。
使用Statement,ResultSet
如果说Connection建立起了与数据库系统的连接,那么 如果我们要操作数据库系统,就需要java.sql.Statement操作对象,它是SQL描述的代表对象,我们可以使用Connection的createStatement方法建立Statement对象:
Statement stmt = conn.createStatement();
取得Statement对象之后,可以使用executeUpdate方法,executeQuery(查询)方法来执行SQL。executeUpdate方法主要用来执行CREATE_TABLE, INSERT, DROP_TABLE, ALTER_TABLE(了解MySQL基础知识后会知道这些在MySQL中也是基本命令)等会改变数据库内容的SQL。
例如我们可以在demo数据库中建立一个t_message表格:
Use demo;
CREATE TABLE t_message(
id INT NOT ALL AUTO_INCREMENT PRIMARY KEY,
name CHAR(20) NOT ALL,
email CHAR(40),
msg TEXT NOT ALL
)CHARSET=UTF8;
如果我们要在这个表格中插入一笔数据,可以这样使用Statement的executeUpdate方法:
stmt.executeUpdate("INSERT INTO t_message VALUES(1, 'justin', " + " 'justin@mail.com', 'mesage...')");
Statement的executeQuery方法用于SELECT等查询数据库的SQL,StatementUpdate方法会返回int结果,表示数据变动的笔数,executeQuery方法会返回java.sql.ResultSet对象,代表查询查询结果,查询结果会是一笔一笔的数据。可以使用ResultSet的next方法移动至下一笔数据,他会返回true或false表示是否由下一笔数据,接着可以使用get×××方法取得数据,如getString方法,getInt方法等,分别取得相对应的字段类型数据。get×××方法都提供有依域名取得数据,或是依字段顺序取得数据的方法。
我们可以看看指定域名来取得数据:
ResultSet result = stmt.executeQuery("SELECT * FROM t_message");
while(result.next()){
int id = result.getInt("id");
String name = result.getString("name");
String email = result.getString("email");
String msg = result.getString("msg");
}
我们也可以看看根据字段顺序来显示结果的方式:
ResultSet result = stmt.executeQuery("SELECT * FROM t_message");
while(result.next()){
int id = result.getInt(1);
String name = result.getString(2);
String email = result.getString(3);
String msg = result.getString(4);
}
我们还可以使用Statement的execute方法,他可以测试SQL是执行更新还是查询,返回true表示SQL执行将返回ResultSet作为查询结果,此时可以使用getResultSet方法取得ResultSet对象,如果返回false,表示SQL执行会返回更新笔数或没有结果,此时可以用getUpdateCount方法取得更新笔数。如果我们无法事先得知SQL是进行查询或更新,就可以使用execute方法。
当Statement或ResultSet在不使用的时候,可以使用close将之关闭,以释放相关的资源。Statement在关闭的时候,所关联的ResultSet也会自动关闭。
我们就以一个简单的留言板为示范,制作一个MessageDAO来存取数据库:
先来看看Message(留言信息类):
import java.io.Serializable;
public class Message implements Serializable{
private Long id;
private String name;
private String email;
private String msg;
public Message(){}
public Message(String name, String email, String msg){
this.name = name;
this.email= email;
this.msg = msg;
}
public String getEmail(){
return email;
}
public void setEmail(String email){
this.email = email;
}
public Long getId(){
return id;
}
public void setId(Long id){
this.id = id;
}
public String getMsg(){
return msg;
}
public void setMsg(String msg){
this.msg = msg;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
再来看看关联数据库,数据库操作类:
import java.sql.*;
import java.util.*;
public class MessageDAO {
private String url;
private String user;
private String passwd;
public MessageDAO(String url, String user, String passwd){
this.url = url;
this.user = user;
this.passwd = passwd;
}
//在数据库中新增留言
public void add(Message message){
//尝试建立数据库连接,操作数据库
try(Connection conn = DriverManager.getConnection(url, user, passwd);
Statement statement = conn.createStatement()){
String sql = String.format("INSERT INTO t_message(name, email, msg) " +
"VALUES('%s', '%s', '%s')", message.getName(), message.getEmail(), message.getMsg());
statement.executeUpdate(sql);
}catch (SQLException ex){
throw new RuntimeException(ex);
}
}
//从数据库中查询所有留言
public List<Message> get(){
List<Message> messages = new ArrayList<>(); //收集Message
try(Connection conn = DriverManager.getConnection(url, user, passwd);
Statement statement = conn.createStatement()){
//从数据库中得到所有数据
ResultSet result = statement.executeQuery("SELECT * FROM t_message");
while (result.next()){
Message message = toMessage(result);
messages.add(message); //将得到的数据收集起来
}
}catch (SQLException ex){
throw new RuntimeException(ex);
}
return messages;
}
//将从数据库中得到的数据存放到Message类中
private Message toMessage(ResultSet result) throws SQLException{
Message message = new Message();
message.setId(result.getLong(1));
message.setName(result.getString(2));
message.setEmail(result.getString(3));
message.setMsg(result.getString(4));
return message;
}
}
最后看看主函数(main):
import static java.lang.System.out;
import java.util.Scanner;
public class MessageDAODemo {
public static void main(String[] args) throws Exception{
MessageDAO dao = new MessageDAO(
"jdbc:mysql://localhost:3306/demo?"+
"useUnicode=true&characterEncoding=UTF8",
"root","******"
);
Scanner console = new Scanner(System.in);
while (true){
out.print("(1) 显示留言 (2)新增留言:");
switch (Integer.parseInt(console.nextLine())){
case 1:
dao.get().forEach(message -> {
out.printf("%d\t%s\t%s\t%s\n",
message.getId(),
message.getName(),
message.getEmail(),
message.getMsg());
});
break;
case 2:
out.print("姓名:");
String name = console.nextLine();
out.print("邮件:");
String email = console.nextLine();
out.print("留言:");
String msg = console.nextLine();
dao.add(new Message(name, email, msg));
}
}
}
}
我们可以看一下程序运行的结果:
在看看数据库这边对应的结果:
我们发现程序运行的结果和想象中的完全吻合,在此,我们成功的实现了用Java程序关联数据库,并成功的制作了一个简易的留言板。不知道你们感觉怎么样,反正我是很激动~~
使用PreparedStatement,CallableStatement
Statement在执行executeQuery方法,executeUpdate等方法时,有些部分是动态的数据,必须使用+运算符串接字符串以组成完整的SQL语句,十分不方便。如果有些操作只是SQL语句当中某些参数有所不同,其余的SQL子句皆相同,则可以使用java.sql.PrepareStatement。可以使用Connection的prepareStatement方法建立好预先编译的SQL语句,当中参数会变动的部分,先指定”?”这个占位字符。例如:
PreparedStatement stmt = conn.prepareStatement("INSERT INTO t_message VALUES(?,?,?,?)");
等到我们需要真正指定参数执行时在使用对应的setInt(),setString()等方法,指定”?”处真正应该有的参数。例如:
stmt.setInt(1, 2);
stmt.setString(2, "momor");
stmt.setString(3, "momor@mail.com");
stmt.setString(4, "message2...");
stmt.executeUpdate();
stmt.clearParameters();
在我们SQL语句执行完之后,可以调用clearParameters()清楚设置的参数,之后就能再次使用PreparedStatement实例。
大家可以尝试一下使用这个方法改写前面MessageDAO中的add方法,具体代码我就不在贴出。
当然使用PreparedStatement的好处不仅如此,事实上我是希望大家遗忘上面留言板中的那种写法,之所以没有直接使用PreparedStatement这种写法,我是想将这个方法留给大家练习,现在我来说说为什么要将上面的写法遗忘。
- PreparedStatement可以重复使用,我们之前已经说过了,它可以减少生成对象的负担;
- 因为可以将SQL描述预编译为数据库的执行指令,所以执行速度就快了许多;
- 使用这种写法,写出来的代码也会更加安全,有兴趣的同学可以了解SQL Injection,相信你会对安全方面有一些了解,在这里我就不细说了;
- 使用前面留言板代码中的写法会造成效能上的负担,因为串接或格式化字符串会产生新的String对象,如果串接字符串动作经常进行,那么将会是效能上的隐忧。
对于CallableStatement的使用,基本上与PreparedStatement差别不大,除了必须调用prepareCall方法建立CallableStatement异常之外,一样是使用set×××()设定参数。
对于这两个实例有兴趣的,我这里说的并不详细,希望大家下去后可以进行详细的学习。