JDBC常用接口和类简介
JDBC提供了独立于数据库的同一API,用以执行SQL命令。JDBC API由以下常用的接口和类组成。
DriverManager
用于管理JDBC驱动的服务类。程序中使用该类的主要功能是获取Connection对象,该类包含如下方法
该方法获得url对应数据库的连接
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
Connection
代表数据库连接对象,每个Connection代表一个物理连接会话。要想访问数据库,必须先获得数据库连接。该接口的常用方法如下。
该方法返回一个Statement对象
Statement createStatement() throws SQLException;
该方法返回预编译的Statement对象,即将SQL语句提交到数据库进行预编译
PreparedStatement prepareStatement(String sql)
throws SQLException;
该方法返回CallableStatement对象,该对象用于调用存储过程
CallableStatement prepareCall(String sql) throws SQLException;
上面3个方法都返回用于执行SQL语句的Statement对象,PreparedStatement 和CallableStatement是Statement的子类,只有获得了Statement之后才可执行SQL语句。
除此之外,Connection还有如下几个用于控制事务的方法。
创建一个保存点
Savepoint setSavepoint() throws SQLException;
以指定名字来创建一个保存点
Savepoint setSavepoint(String name) throws SQLException;
设置事务的隔离级别
void setTransactionIsolation(int level) throws SQLException;
回滚事务
void rollback() throws SQLException;
将事务回滚到指定的保存点
void rollback(Savepoint savepoint) throws SQLException;
关闭自动提交,打开事务
void setAutoCommit(boolean autoCommit) throws SQLException;
提交事务
void commit() throws SQLException;
Statement
用于执行SQL语句的工具接口。该对象既可用于执行DDL、DCL语句,也可用于执行DML语句,还可用于执行SQL查询。当执行SQL查询时,返回查询到的结果集。它的常用方法如下。
该方法用于执行查询语句,并返回查询结果对应的ResultSet对象。该方法只能用于执行查询语句
ResultSet executeQuery(String sql) throws SQLException;
该方法用于执行DML语句,并返回受影响的行数;该方法也可用于执行DDL语句,执行DDL语句将返回0
int executeUpdate(String sql) throws SQLException;
该方法可执行任何SQL语句。如果执行后第一个结果为ResultSet对象,则返回true;如果执行后第一个结果为受影响的行数或没有任何结果,则返回false
boolean execute(String sql) throws SQLException;
PreparedStatement
预编译的Statement对象。PreparedStatement是Statement的子接口,它允许数据库预编译SQL语句(这些SQL语句通常带有参数),以后每次只改变SQL命令的参数,避免数据库每次都需要编译SQL语句,因此性能更好。相对于Statement而言,使用PreparedStatement执行SQL语句时,无须再传入SQL语句,只要为预编译的SQL语句传入参数值即可。所以它比Statement多了如下方法。
该方法根据传入参数值的类型不同,需要使用不同的方法。传入的值根据索引传给SQL语句中指定位置的参数
void setXxx(int parameterIndex,Xxx value)
ResultSet
结果集对象,该对象包含访问查询结果的方法,ResultSet可以通过列索引或列名获得列数据。它包含了如下常用方法来移动记录指针。
释放ResultSet对象
void close() throws SQLException;
将结果集的记录指针移动到第row行,如果row是负数,则移动到倒数第row行。如果移动后的记录指针指向一条有效记录,则该方法返回true
boolean absolute( int row ) throws SQLException;
将ResultSet的记录指针定位到首行之前,这是ResultSet结果集记录指针的初始状态,记录指针的起始位置位于第一行之前
void beforeFirst() throws SQLException;
将ResultSet的记录指针定位到首行。如果移动后的记录指针指向一条有效记录,则该方法返回true
boolean first() throws SQLException;
将ResultSet的记录指针定位到上一行。如果移动后的记录指针指向一条有效记录,则该方法返回true
boolean previous() throws SQLException;
将ResultSet的记录指针定位到下一行,如果移动后的记录指针指向一条有效记录,则该方法返回true
boolean next() throws SQLException;
将ResultSet的记录指针定位到最后一行,如果移动后的记录指针指向一条有效的记录,则该方法返回true
boolean last() throws SQLException;
将ResultSet的记录指针定位到最后一行之后
void afterLast() throws SQLException;
JDBC编程步骤
大致了解了JDBC API的相关接口和类之后,下面就可以进行JDBC编程了,JDBC编程大致按如下步骤进行。
(1)加载数据库驱动
通常我们使用Class类的forName()静态方法来加载驱动。
Class.forName(driverClass)
(2)通过DriverManager获取数据库连接
DriverManager.getConnection(String url,String user,String password)
当使用DriverManager获取数据库连接时,通常需要传入3个参数:数据库URL,登录数据库的用户名和密码。
MySQL数据库的URL写法如下
jdbc:mysql://hostname:port/databasename
Oracle数据库的URL写法如下
jdbc:oracle:thin:@hostname:port:databasename
(3)通过Connection对象创建Statement对象
Connection创建Statement的方法有如下3个
创建基本的Statement对象
Statement createStatement() throws SQLException;
根据传入的SQL语句创建预编译的Statement对象
PreparedStatement prepareStatement(String sql)
throws SQLException;
根据传入的SQL语句创建CallableStatement对象
CallableStatement prepareCall(String sql) throws SQLException;
(4)使用Statement执行SQL语句
所有的Statement都有如下3个方法来执行SQL语句
可以执行任何SQL语句
boolean execute(String sql) throws SQLException;
主要用于执行DML和DDL语句。执行DML语句返回受SQL语句影响的行数,执行DDL语句返回0
int executeUpdate(String sql) throws SQLException;
只能执行查询语句,执行后返回代表查询结果的ResultSet对象
ResultSet executeQuery(String sql) throws SQLException;
(5)操作结果集
如果执行的SQL语句是查询语句,则执行结果将返回一个ResultSet对象,该对象里保存了SQL语句查询的结果。程序可以通过操作该ResultSet对象来取出查询结果。
(6)回收数据库资源
包括关闭ResultSet、Statement和Connection等资源
代码演示
public class ConnMySql {
public static final String url = "jdbc:mysql://127.0.0.1:3306/test";
public static final String user = "root";
public static final String password = "123";
public static final String sql = "select s.*, teacher_name from student_table s, teacher_table t where t.teacher_id=s.java_teacher";
public static void main(String[] args) throws Exception{
//1.加载驱动
Class.forName("com.mysql.jdbc.Driver");
try (
//2.使用DriverManager获取数据库连接
//其中返回的Connection就代表了Java程序和数据库的连接
Connection conn = DriverManager.getConnection(url, user, password);
//3.使用Connection来创建一个Statement对象
Statement stmt = conn.createStatement();
//4.执行SQL语句
ResultSet rs = stmt.executeQuery(sql)
){
while (rs.next()){
System.out.println(rs.getInt(1) + "\t"
+ rs.getString(2) + "\t"
+ rs.getString(3) + "\t"
+ rs.getString(4));
}
}
}
}
本程序采用了自动关闭资源的try语句来关闭各种数据库资源,Java7改写了Connection、Statement、ResultSet等接口,它们都继承了AutoCloseable接口,因此它们都可以由try语句来关闭
执行SQL语句的方式
使用executeUpdate方法执行DDL和DML语句
代码演示
public class ExecuteDDL {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public void createTable(String sql) throws Exception{
//加载驱动
Class.forName(driver);
try (
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, password);
//使用Connection来创建一个Statement对象
Statement stmt = conn.createStatement()
){
//执行DDL语句,创建数据表
stmt.executeUpdate(sql);
}
}
public static void main(String[] args) throws Exception{
String sql = "create table jdbc_test(\n" +
"\tjdbc_id int auto_increment primary key,\n" +
"\tjdbc_name varchar(255),\n" +
"\tjdbc_desc text\n" +
");";
ExecuteDDL ed = new ExecuteDDL();
ed.initParam("mysql.ini");
ed.createTable(sql);
System.out.println("--------建表成功---------");
}
}
运行上面程序,执行成功后会看到test数据库中添加了一个jdbc_test数据表,这表明JDBC执行DDL语句成功
使用executeUpdate()执行DML语句与执行DDL语句基本相似,区别是executeUpdate()执行DDL语句后返回0,而执行DML语句后返回受影响的记录条数。
使用execute方法执行SQL语句
Statement的execute()方法几乎可以执行任何SQL语句,但它执行SQL语句时比较麻烦,通常我们没有必要使用execute()方法来执行SQL语句,而使用executeQuery()或executeUpdate()方法更简单。但如果不清楚SQL语句的类型,则只能使用execute()方法来执行该SQL语句了。
代码演示
public class ExecuteSQL {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public void executeSql(String sql) throws Exception{
//加载驱动
Class.forName(driver);
try (
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user, password);
//使用Connection来创建一个Statement对象
Statement stmt = conn.createStatement()
){
//执行SQL语句,返回boolean值表示是否包含ResultSet
boolean hasResultSet = stmt.execute(sql);
if (hasResultSet){
try (
//获取结果集
ResultSet rs = stmt.getResultSet()
){
//ResultSetMetaData是用于分析结果集的元数据接口
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
while (rs.next()){
//依次输出每列的值
for (int i=0; i<columnCount; i++){
System.out.print(rs.getString(i + 1) + " ");
}
System.out.println("\n");
}
}
}else {
System.out.println("该SQL语句影响的记录有" + stmt.getUpdateCount() + "条");
}
}
}
public static void main(String[] args) throws Exception{
ExecuteSQL es = new ExecuteSQL();
es.initParam("mysql.ini");
System.out.println("-----执行删除表的DDL语句-----");
es.executeSql("drop table if exists my_test");
System.out.println("-----执行建表的DDL语句-----");
es.executeSql("create table my_test(\n" +
"\ttest_id int auto_increment primary key,\n" +
"\ttest_name varchar(255)\n" +
");");
System.out.println("-----执行插入数据的DML语句-----");
es.executeSql("insert into my_test(test_name) \n" +
"select student_name from student_table;");
System.out.println("-----执行查询数据的查询语句-----");
es.executeSql("select * from my_test");
}
}
运行结果
-----执行删除表的DDL语句-----
该SQL语句影响的记录有0条
-----执行建表的DDL语句-----
该SQL语句影响的记录有0条
-----执行插入数据的DML语句-----
该SQL语句影响的记录有1条
-----执行查询数据的查询语句-----
1 _zhangsan
上面程序获得SQL执行结果时没有根据各列的数据类型调用相应的getXxx()方法,而是直接使用getString()方法来取得值,这是可以的。ResultSet的getString()方法几乎可以获取除Blob之外的任意类型列的值,因为所有的数据类型都可以自动转换成字符串类型。
使用PreparedStatement执行SQL语句
JDBC提供了PreparedStatement接口,它是Statement接口的子接口,它可以预编译SQL语句,预编译后的SQL语句被存储在PreparedStatement对象中,然后可以使用该对象多次高效地执行该语句。简而言之,使用PreparedStatement比使用Statement的效率更高。
下面程序示范了使用Statement和PreparedStatement分别插入100条记录的对比。使用Statement需要传入100条SQL语句,但使用PreparedStatement则只需要传入1条预编译的SQL语句,然后100次为该PreparedStatement的参数设值即可。
代码演示
public class PreparedStatementTest {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
//加载驱动
Class.forName(driver);
}
public void insertUseStatement() throws Exception {
long start = System.currentTimeMillis();
try (
Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement()
){
//需要使用100条SQL语句来插入100条记录
for (int i=0; i<100; i++){
stmt.executeUpdate("insert into student_table values(null, '姓名" + i + "', 1)");
}
System.out.println("使用Statement费时:" + (System.currentTimeMillis() - start));
}
}
public void insertUsePrepare() throws Exception{
long start = System.currentTimeMillis();
try (
Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement pstmt = conn.prepareStatement("insert into student_table values(null, ?, 1)")
){
//100次为PreparedStatement的参数设值,就可以插入100条记录
for (int i=0; i<100; i++){
pstmt.setString(1, "姓名" + i);
pstmt.executeUpdate();
}
System.out.println("使用PreparedStatement费时:" + (System.currentTimeMillis() - start));
}
}
public static void main(String[] args) throws Exception{
PreparedStatementTest pt = new PreparedStatementTest();
pt.initParam("mysql.ini");
pt.insertUseStatement();
pt.insertUsePrepare();
}
}
运行结果
使用Statement费时:331
使用PreparedStatement费时:106
除了PreparedStatement的执行效率比Statement高之外,使用PreparedStatement还有一个优势,当SQL语句中要使用参数时,无须拼接SQL字符串,使用PreparedStatement则只需要使用问号占位符来代替这些参数即可,降低了变成复杂度。
使用PreparedStatement还有一个很好的作用——用于防止SQL注入。
使用CallableStatement调用存储过程
代码演示
public class CallableStatementTest {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public void callProcedure() throws Exception{
//加载驱动
Class.forName(driver);
try (
Connection conn = DriverManager.getConnection(url, user, password);
//使用Connection来创建一个CallableStatement对象
CallableStatement cstmt = conn.prepareCall("{call add_pro(?,?,?)}")
){
cstmt.setInt(1, 4);
cstmt.setInt(2, 5);
//注册CallableStatement的第三个参数是int类型
cstmt.registerOutParameter(3, Types.INTEGER);
//执行存储过程
cstmt.execute();
//获取并输出存储过程传出参数的值
System.out.println("执行结果是:" + cstmt.getInt(3));
}
}
public static void main(String[] args) throws Exception{
CallableStatementTest ct = new CallableStatementTest();
ct.initParam("mysql.ini");
ct.callProcedure();
}
}
管理结果集
JDBC使用ResultSet来封装执行查询得到的查询结果,然后通过移动ResultSet的记录指针来取出结果集的内容。除此之外,JDBC还允许通过ResultSet来更新记录,并提供了ResultSetMetaData来获得ResultSet对象的相关信息。
下面代码通过这两个参数创建了一个PreparedStatement对象,由该对象生成的ResultSet对象将是可滚动、可更新的结果集
//使用Connection创建一个PreparedStatement对象
//传入控制结果集可滚动、可更新的参数
PreparedStatement pstm = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
需要指出的是,可更新的结果集还需要满足如下两个条件。
- 所有数据都应该来自一个表
- 选出的数据集必须包含主键列
通过该PreparedStatement创建的ResultSet就是可滚动、可更新的,程序可调用ResultSet的updateXxx(int columnIndex, Xxx value)方法来修改记录指针所指记录、特定列的值,最后调用ResultSet的updateRow()方法来提交修改。
代码演示
public class ResultSetTest {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public void query(String sql) throws Exception{
//加载驱动
Class.forName(driver);
try (
Connection conn = DriverManager.getConnection(url, user, password);
//使用Connection来创建一个PreparedStatement对象
//传入控制结果集可滚动、可更新的参数
PreparedStatement pstmt = conn.prepareStatement(sql,
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = pstmt.executeQuery()
){
rs.last();
int rowCount = rs.getRow();
for (int i=rowCount; i>0; i--){
rs.absolute(i);
System.out.println(rs.getString(1) + "\t"
+ rs.getString(2) + "\t" + rs.getString(3));
//修改记录指针所指记录、第2列的值
rs.updateString(2, "学生名" + i);
//提交修改
rs.updateRow();
}
}
}
public static void main(String[] args) throws Exception{
ResultSetTest rt = new ResultSetTest();
rt.initParam("mysql.ini");
rt.query("select * from student_table");
}
}
上面程序中示范了如何自由移动记录指针并更新记录指针所指的记录。运行上面程序,将会看到student_table表中的记录被倒过来输出了,因为是从最大记录行开始输出的。而且当程序运行结束后,student_table表中所有记录的student_name列的值都被修改了
如果要创建可更新的结果集,则使用查询语句查询的数据通常只能来自于一个数据表,而且查询结果集中的数据列必须包含主键列,否则将会引起更新失败。
使用ResultSetMetaData分析结果集
当执行SQL查询后可以通过移动记录指针来遍历ResultSet的每条记录,但程序可能不清楚ResultSet里包含哪些数据列,以及每个数据列的数据类型,那么可以通过ResultSetMetaData来获取关于ResultSet的描述信息。
MetaData的意思是元数据,即描述其他数据的数据,因此ResultSetMetaData封装了描述ResultSet对象的数据。
ResultSet包含一个getMetaData()方法,该方法返回该ResultSet对应的ResultSetMetaData对象。一旦获得了ResultSetMetaData对象,就可通过ResultSetMetaData提供的大量方法来返回ResultSet的描述信息。常用的方法有如下3个。
返回该ResultSet的列数量
int getColumnCount() throws SQLException;
返回指定索引的列名
String getColumnName(int column) throws SQLException;
返回指定索引的列类型
int getColumnType(int column) throws SQLException;
虽然ResultSetMetaData可以准确地分析出ResultSet里包含多少列,以及每列的列名、数据类型等,但使用ResultSetMetaData需要一定的系统开销,因此如果在编程过程中已经知道ResultSet里包含多少列,以及每列的列名、类型等信息,就没有必要使用ResultSetMetaData来分析该ResultSet对象了