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對象了