JDBC连接数据库

JDBC

在前面的学习,通过CMD命令窗口或者图形界面工具连接并使用MySQL数据库,在一款软件的开发过程中,我们一般使用命令窗口或者图形界面工具调试或者测试数据,用不同的语言去连接数据库,进行数据操作,以Java为例,Java为我们提供并封装了一系列操作数据库的接口(这里的数据库不单单指MySQL,也包括其他关系型数据库例如:SqlServer或者oracle等)。在本章节内,我们将详细学习如果使用。

JDBC简介

JDBC 的全称是 Java Database Connectivity,即 Java 数据库连接,它是一种可以执行 SQL 语句的 Java API。程序可通过 JDBC API 连接到关系数据库,并使用结构化查询语言(SOL,数据库标准的查询语言)来完成对数据库的查询、更新。
与其他数据库编程环境相比,JDBC 为数据库开发提供了标准的 API,所以使用 JDBC 开发的数据库应用可以跨平台运行,而且可以跨数据库(如果全部使用标准的 SQL)。也就是说,如果使用 JDBC开发一个数据库应用,则该应用既可以在 Windows 平台上运行,也可以在 UNIX 等其他平台上运行;既可以使用 MySQL 数据库,也可以使用 Oracle 等数据库,而程序无须进行任何修改。

Java 语言的各种跨平台特性,都采用相似的结构,因为它们都需要让相同的程序在不同的平台上运行,所以都需要中间的转换程序(为了实现 Java 程序的跨平台性,Java为不同的操作系统提供了不同的虚拟机)。同样,为了使JDBC程序可以跨平台,则需要不同的数据库厂商提供相应的驱动程序。

上图显示了JDBC驱动示意图。
正是通过 JDBC驱动的转换,才使得使用相同 JDBC API 编写的程序,在不同的数据库系统上运行良好。

Sun 提供的 JDBC 可以完成以下三个基本工作。



  1. 建立与数据库的连接
  2. 执行SQL语句
  3. 获取SQL语句的执行结果

JDBC常用接口和类

学习JDBC之前需要先了解常用的接口和类,了解完这些接口和类以后就可以使用JDBC连接并操作数据库。

DriverManager类

该接口用于管理JDBC驱动的服务类,该类的主要作用是获取Java与数据库连接的Connection对象,Connection对象可以想象成Java与数据库通信的桥梁。DriverManager的主要方法如下:

  • public static Connection getConnection(String url,String user, String password) throws SQLException:该方法用于获取url对应的数据库连接,user指数据库的用户名,password指数据库的用户密码

Connection接口

该接口代表数据库的连接对象,每个Connection代表一个物理连接会话,要想访问数据库,必须先获得数据库连接。该接口的常用方法如下:

  • Statement createStatement() throws SQLExcetpion∶ 该方法返回一个 Statement 对象。

  • PreparedStatement prepareStatement(String sql)throws SQLExcetpion∶ 该方法返回预编译的
Statement 对象,即将 SOL 语句提交到数据库进行预编译。

  • CallableStatement prepareCall(String sql) throws SQLExcetpion∶ 该方法返回 CallableStatement
对象,该对象用于调用存储过程。





上面三个方法都返回用于执行 SQL 语句的 Statement 对象,PreparedStatement、CallableStatement是 Statement 的子类,只有获得了 Statement 之后才可执行 SQL 语句。

除此之外,Connection 还有如下几个用于控制事务的方法。

  • Savepoint setSavepoint()∶创建一个保存点。
  • Savepoint setSavepoint(String name)∶ 以指定名字来创建一个保存点。
  • void setTransactionIsolation(int level)∶ 设置事务的隔离级别。
  • void rollback()∶ 回滚事务。
> void rollback(Savepoint savepoint)∶ 将事务回滚到指定的保存点。
  • void setAutoCommit(boolean autoCommit)∶关闭自动提交,打开事务。> void commit()∶提交事务。

Statement接口

Statement∶用于执行 SQL 语句的工具接口。该对象既可用于执行DDL、DCL 语句,也可用于
执行 DML 语句,还可用于执行 SQL 查询。当执行 SQL 查询时,返回查询到的结果集。它的常用方法如下。


  • ResultSet executeQuery(String sql)throws SQLException∶该方法用于执行查询语句,并返回查
询结果对应的 ResultSet 对象。该方法只能用于执行查询语句。

  • int executeUpdate(String sql)throws SQLExcetion∶ 该方法用于执行 DML 语句,并返回受影响
的行数; 该方法也可用于执行 DDL 语句,执行 DDL 语句将返回 0。

  • boolean execute(String sql)throws SOLException∶ 该方法可执行任何 SOL 语句。如果执行后
第一个结果为 ResultSet 对象,则返回 true; 如果执行后第一个结果为受影响的行数或没有任何结果,则返回 false。

PreparedStatement:预编译的 Statement 对象。PreparedStatement 是 Statement 的子接口,它允
许数据库预编译 SOL 语句(这些 SOL 语句通常带有参数),以后每次只改变 SOL 命令的参数,避免数据库每次都需要编译 SQL 语句,因此性能更好。相对于 Statement 而言,使用 PreparedStatement 执行 SOL 语句时,无须再传入 SOL 语句,只要为预编译的 SOL 语句传入参数值即可。所以它比 Statement 多了如下方法。


  • void setXxx(int parameterlndex,Xxx value)∶该方法根据传入参数值的类型不同,需要使用不
同的方法。传入的值根据索引传给 SQL 语句中指定位置的参数。

ResultSet接口

ResultSet:结果集对象。该对象包含访问查询结果的方法,ResultSet 可以通过列索引或列名获
得列数据。它包含了如下常用方法来移动记录指针。

  • void close():释放 ResultSet 对象。

  • boolean absolute(int row):将结果集的记录指针移动到第 row 行,如果 row 是负数,则移动
到倒数第 row 行。如果移动后的记录指针指向一条有效记录,则该方法返回 true。

  • void beforeFirst():将 ResultSet 的记录指针定位到首行之前,这是 ResultSet 结果集记录指针
的初始状态——记录指针的起始位置位于第一行之前。

  • boolean first():将 ResultSet 的记录指针定位到首行。如果移动后的记录指针指向一条有效记
录,则该方法返回 true。

  • boolean previous():将 ResultSet 的记录指针定位到上一行。如果移动后的记录指针指向一条
有效记录,则该方法返回 true。

  • boolean next():将 ResultSet 的记录指针定位到下一行,如果移动后的记录指针指向一条有效
记录,则该方法返回 true。

  • boolean last():将 ResultSet 的记录指针定位到最后一行,如果移动后的记录指针指向一条有
效记录,则该方法返回 true。
·
  • void afterLast(): 将 ResultSet 的记录指针定位到最后一行之后。

当把记录指针移动到指定行之后,ResultSet 可通过 getXxx(int columnIndex)或 getXxx(String columnLabel)方法来获取当前行、指定列的值,前者根据列索引获取值,后者根据列名获取值。Java 7新增了T getObject(int columnIndex,Class type)和T getObject(String columnLabel, Class type)两个泛型方法,它们可以获取任意类型的值。


JDBC编程步骤

大致了解了JDBC API的相关接口和类之后,下面就可以进行 JDBC 编程了,JDBC编程大致按如下步骤进行。

  1. 加载驱动
1
2
3
4
//加载MySQL驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//如果要加载Oracle驱动
Class.forName("Oracle.jdbc.driver.OracleDriver");
  1. 获取连接
1
2
//获取连接
DriverManager.getConnection(String url,String user,String pass);

当使用 DriverManager 获取数据库连接时,通常需要传入三个参数;数据库 URL、登录数据库的用户名和密码。这三个参数中用户名和密码通常由 DBA(数据库管理员)分配,而且该用户还应该具有相应的权限,才可以执行相应的 SQL 语句。
数据库 URL通常遵循如下写法∶

1
jdbc:subprotoclo:othre stuff

上面 URL 写法中的 jdbc 是固定的,而 subprotocol 指定连接到特定数据库的驱动,而后面的 other和 stuff也是不固定的——也没有较强的规律,不同数据库的 URL 写法可能存在较大差异。例如,MySQL数据库的 URL写法如下∶


1
jdbc:mysql://hostname:port/databasename

在上面的链接中hostname指的是数据库所在主机的IP或者域名,如果要连接本机则可以写作“localhost”或者“127.0.0.1”,port则是指数据库的端口号,MySQL的端口号默认是3306,database则是只要连接数据库的名称。

Oracle数据库的URL写法如下:

1
jdbc:oracle:thin:@hostname:port:databasename
  1. 通过Connection获取Statement对象
1
Statement statement = connection.createStatemen();
  1. 使用Statement对象执行SQL语句
1
2
String sql = "INSERT INTO STUDENT VALUES(1,'张三',18,'男')";
int k = statement.executeUpdate(sql);
  1. 获取结果,此处使用的是Statement对象的executeUpdate方法,该方法返回的是影响的行数,如果要调用executeQuery方法,则返回值是一个ResultSet对象。
1
2
3
4
5
6
7
ResultSet rs = statement.executeQuery(sql);
while(rs.next()) {
System.out.println(rs.getInt("id"));
System.out.println(rs.getString("student_name"));
System.out.println(rs.getInt("student_age"));
System.out.println(rs.getInt("gender"));
}
  1. 释放资源

下面,将上述步骤进行整合,示范JDBC编程步骤:

首先创建数据库和数据表:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
--创建数据库
CREATE DATABASE TEST_DB;
--切换数据库
USE TEST_DB;
--创建表
CREATE TABLE STUDENT(
ID INT(11),
STUDENT_NAME VARCHAR(25),
STUDENT_AGE VARCHAR(11),
STUDENT_GENDER CHAR(2)
);
--插入数据
INSERT INTO STUDENT VALUES (1,"张三",18,'男');
INSERT INTO STUDENT VALUES (2,"李四",18,'男');
package cn.bytecollege;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class JDBCDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test_db?serverTimezone=Asia/Shanghai";
String user = "root";
String password = "root";
//1.加载驱动
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
System.out.println("驱动未加载");
e.printStackTrace();
}
//2.获取连接
Connection connection = null;
Statement statement = null;
try {
connection = DriverManager.getConnection(url, user, password);
//3.获取Statement
statement = connection.createStatement();
//4.执行SQL语句
// String sql = "INSERT INTO STUDENT (1,'张三',18,'男')";
// int k = statement.executeUpdate(sql);
String sql = "SELECT * FROM STUDENT";
//5.获取结果
ResultSet rs = statement.executeQuery(sql);
while(rs.next()) {
System.out.print(rs.getInt("id"));
System.out.print(rs.getString("student_name"));
System.out.print(rs.getInt("student_age"));
System.out.print(rs.getString("gender"));
System.out.println();
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
statement.close();
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

运行上面的程序,结果如下图:

使用PrepareStatement执行SQL语句

如果经常需要反复执行一条结构相似的 SQL 语句,例如如下两条 SQL 语句∶


1
2
INSERT INTO STUDENT VALUES (1,"张三",18,'男');
INSERT INTO STUDENT VALUES (2,"李四",18,'男');

对于这两条 SOL 语句而言,它们的结构基本相似,只是执行插入时插入的值不同而已。对于这种情况,可以使用带占位符(?)参数的 SOL语句来代替它;


1
INSERT INTO STUDENT VALUES (?,?,?,?);

但 Statement 执行 SOL 语句时不允许使用问号占位符参数,而且这个问号占位符参数必须获得值后才可以执行。为了满足这种功能,JDBC提供了PreparedStatement接口,它是 Statement接口的子接口,它可以预编译 SQL 语句,预编译后的 SQL 语句被存储在 PreparedStatement 对象中,然后可以使用该对象多次高效地执行该语句。简而言之,使用 PreparedStatement 比使用 Statement 的效率要高。
创建 PreparedStatement 对象使用 Connection 的 prepareStatement()方法,该方法需要传入一个 SQL字符串,该 SQL 字符串可以包含占位符参数。如下代码所示∶


1
2
String sql = "INSERT INTO STUDENT (?,?,?,?)";
PreparedStatement ps = connection.prepareStatement(sql);

PreparedStatement 也提供了execute()、executeUpdate()、executeQuery()三个方法来执行 SQL 语句,不过这三个方法无须参数,因为 PreparedStatement 已存储了预编译的 SOL 语句。
使用 PreparedStatement 预编译 SOL 语句时,该 SOL 语句可以带占位符参数,因此在执行 SOL 语句之前必须为这些参数传入参数值,PreparedStatement 提供了一系列的 setXxx(int index,Xxx value)方法来传入参数值。

下面通过示例学习PrepareStatement的用法:

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
package cn.bytecollege;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class JDBCDemo2 {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test_db?serverTimezone=Asia/Shanghai";
String user = "root";
String password = "root";
//1.加载驱动
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
System.out.println("驱动未加载");
e.printStackTrace();
}
//2.获取连接
Connection connection = null;
PreparedStatement ps = null;
try {
connection = DriverManager.getConnection(url, user, password);
String sql = "INSERT INTO STUDENT VALUES (?,?,?,?)";
ps = connection.prepareStatement(sql);
ps.setInt(1, 3);
ps.setString(2, "王五");
ps.setInt(3, 20);
ps.setString(4, "女");
int k = ps.executeUpdate();
System.out.println(k);
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
ps.close();
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

需要注意的是使用PrepareStatement的setXxx()方法时,第一个参数是占位符的索引,第二个参数是占位符的真实值,占位符的索引是从1开始。

总体来看,使用 PreparedStatement 比使用 Statement 多了如下三个好处。

  1. PreparedStatement 预编译 SQL 语句,性能更好。
  2. PreparedStatement 无须拼接SQL 语句,编程更简单。
  3. PreparedStatement 可以防止 SQL 注入,安全性更好。


基于以上三点,通常推荐避免使用 Statement 来执行 SQL 语句,改为使用 PreparedStatement 执行 SQL语句。

可滚动的结果集

前面提到,ResultSet 定位记录指针的方法有 absolute(O)、previous(等方法,但前面程序自始至终都只用了 next()方法来移动记录指针,实际上也可以使用 absoluteO、previousO、lastO)等方法来移动记录
指针。可以使用 absolute()、previous(、afterLast()等方法自由移动记录指针的 ResultSet 被称为可滚动的结果集。



在 JDK 1.4 以前,默认打开的 ResultSet 是不可滚动的,必须在创建 Statement 或 PreparedStatement 时传入额外的参数。从 Java 5.0 以后,默认打开的 ResultSet 就是可滚动的,无须传入额外的参数。


以默认方式打开的 ResultSet 是不可更新的,如果希望创建可更新的 ResultSet,则必须在创建 Statement 或 PreparedStatement 时传入额外的参数。Connection 在创建 Statement 或 PreparedStatement 时还可额外传入如下两个参数。


  1. resultSetType∶ 控制 ResultSet 的类型,该参数可以取如下三个值:
    • 
ResultSet.TYPE FORWARD ONLY∶该常量控制记录指针只能向前移动。这是 JDK1.4 以前
的默认值。

    • ResultSet.TYPE SCROLL INSENSITIVE∶该常量控制记录指针可以自由移动(可滚动结果
集),但底层数据的改变不会影响 ResultSet 的内容。

    • ResultSet.TYPE SCROLL SENSITIVE∶该常量控制记录指针可以自由移动(可滚动结果集),
而且底层数据的改变会影响 ResultSet 的内容。

  1. resultSetConcurrency∶控制 ResultSet 的并发类型,该参数可以接收如下两个值
·:
    • ResultSet.CONCUR_READ_ONLY∶该常量指示 ResultSet 是只读的并发模式(默认)。·
    • ResultSet.CONCUR UPDATABLE∶该常量指示 ResultSet是可更新的并发模式。

注意:TYPE SCROLL INSENSITIVE、TYPE SCROLL SENSITIVE 两个常量的作用需要底层数据库驱动的支持,对于有些数据库驱动来说,这两个常量并没有太大的区别。


需要指出的是,可更新的结果集还需要满足如下两个条件。

  1. 所有数据都应该来自一个表。
  2. 选出的数据集必须包含主键列。

通过该 PreparedStatement 创建的 ResultSet 就是可滚动、可更新的,程序可调用 ResultSet 的 updateXxx(int columnIndex,Xxx value)方法来修改记录指针所指记录、特定列的值,最后调用 ResultSet的 updateRow()方法来提交修改。

下面通过示例来学习可滚动的结果集。

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
52
53
54
55
56
57
58
59
60
package cn.bytecollege;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Iterator;

public class JDBCDemo2 {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test_db?serverTimezone=Asia/Shanghai";
String user = "root";
String password = "root";
//1.加载驱动
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
System.out.println("驱动未加载");
e.printStackTrace();
}
//2.获取连接
Connection connection = null;
PreparedStatement ps = null;
try {
connection = DriverManager.getConnection(url, user, password);
String sql = "SELECT * FROM STUDENT";
ps = connection.prepareStatement(sql,ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
ResultSet rs = ps.executeQuery();
//将游标定位到最后一行
rs.last();
//获取总行数
int row = rs.getRow();
for (int i = row; i >0; i--) {
//定位读取的行数
rs.absolute(i);
System.out.print(rs.getInt("id")+"\t");
System.out.print(rs.getString("student_name")+"\t");
System.out.print(rs.getInt("student_age")+"\t");
System.out.print(rs.getString("gender")+"\t");
System.out.println();
//更新第二列,即学生姓名
rs.updateString(2, "姓名"+i);
rs.updateRow();
System.out.println();
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
ps.close();
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

运行上面的程序结果如下图:

从运算结果可以看出输入数据时是倒序输出的,这是因为在程序中先将游标定位到了最后一行,然后倒序循环,通过absolute()定位到每一行数据并读取。读取完数据后,将数据表中的第2列数据进行了修改,此时查询数据库如下图:

使用ResultSetMetaData分析结果集

当执行 SOL 查询后可以通过移动记录指针来遍历 ResultSet 的每条记录,但程序可能不清楚该 ResultSet 里包含哪些数据列,以及每个数据列的数据类型,那么可以通过 ResultSetMetaData 来获取关于 ResultSet 的描述信息。

ResultSet 里包含一个 getMetaData()方法,该方法返回该 ResultSet 对应的 ResultSetMetaData 对象。一旦获得了 ResultSetMetaData 对象,就可通过 ResultSetMetaData 提供的大量方法来返回 ResultSet 的描述信息。常用的方法有如下三个。


  • int getColumnCount()∶ 返回该 ResultSet 的列数量。
  • String getColumnName(int column)∶返回指定索引的列名。
  • String getColumnClassName(int column)∶返回指定索引的列类型。

下面通过示例来学习ResultSetMetaData的用法

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
52
53
54
55
56
57
58
59
package cn.bytecollege;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Iterator;


public class ResultSetMetaDataDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test_db?serverTimezone=Asia/Shanghai";
String user = "root";
String password = "root";
//1.加载驱动
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
System.out.println("驱动未加载");
e.printStackTrace();
}
//2.获取连接
Connection connection = null;
PreparedStatement ps = null;
try {
connection = DriverManager.getConnection(url, user, password);
String sql = "SELECT * FROM STUDENT";
ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
//获取结果集元数据
ResultSetMetaData metaData = rs.getMetaData();
//获取结果集列数
int count = metaData.getColumnCount();
for (int i = 1; i <= count; i++) {
//获取每列的字段名
System.out.print(metaData.getColumnName(i)+"\t\t");
//获取每列的数据类型
System.out.print(metaData.getColumnTypeName(i)+"\t\t");
//获取每列对应的Java数据类型
System.out.print(metaData.getColumnClassName(i)+"\t\t");
System.out.println();
}

} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
ps.close();
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

运行上面的程序,结果如下