JDBC操作全攻略
JDBC簡介
JDBC(Java Database Connectivity),也稱為Java數(shù)據(jù)庫連接,是Java用于連接數(shù)據(jù)庫的接口規(guī)范,需要注意的是,JDBC并沒有提供的實(shí)現(xiàn),具體的實(shí)現(xiàn)是由數(shù)據(jù)庫提供商,比如MySQL,Oracle等負(fù)責(zé)提供,JDBC提供了一整套完整的連接規(guī)范,包括了Driver,DriverManager,Connection,Statement,PreparedStatement,ResultSet,DatabaseMetaData,ResultSetMetaData,ParameterMetaData等等
連接數(shù)據(jù)庫
在Java程序中,操作數(shù)據(jù)庫時(shí),通常需要幾個(gè)步驟
- 導(dǎo)入對應(yīng)的JDBC實(shí)現(xiàn):由于JDBC本身沒有提供對應(yīng)的實(shí)現(xiàn),也不太可能提供對應(yīng)的實(shí)現(xiàn),所以需要導(dǎo)入對應(yīng)數(shù)據(jù)庫供應(yīng)商提供的實(shí)現(xiàn),比如MySQL的實(shí)現(xiàn)
- 在程序中手動(dòng)注冊對應(yīng)的數(shù)據(jù)庫驅(qū)動(dòng)
- 建立并且打開對應(yīng)的連接
- 通過連接獲取一個(gè)Statement對象,用于發(fā)送SQL,并且獲得對應(yīng)的結(jié)果
- 從Statement中獲得結(jié)果集對象
- 關(guān)閉對應(yīng)的結(jié)果集、Statement以及連接
接下來通過代碼來具體查看如何操作
首先是導(dǎo)入MySQL的實(shí)現(xiàn),對應(yīng)的Maven依賴為
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
然后建立測試類TestJDBC.java
注冊MySQL驅(qū)動(dòng)
// 注冊MySQL驅(qū)動(dòng)
Class.forName("com.mysql.jdbc.Driver");
建立連接
private static final String SQL_URL = "jdbc:mysql://localhost:3306/spring"; // 數(shù)據(jù)庫連接地址
private static final String SQL_USER_NAME = "root"; // 賬號
private static final String SQL_PASSWORD = "huanfeng"; // 密碼
// 建立連接
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
// 獲得Statment對象
Statement statement = connection.createStatement();
完整的過程如下所示
private static final String SQL_URL = "jdbc:mysql://localhost:3306/spring";
private static final String SQL_USER_NAME = "root";
private static final String SQL_PASSWORD = "huanfeng";
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 注冊MySQL驅(qū)動(dòng)
Class.forName("com.mysql.jdbc.Driver");
// 建立連接
connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
String sql = "SELECT * FROM user";
// 獲得Statement
statement = connection.createStatement();
// 獲得ResultSet
resultSet = statement.executeQuery(sql);
// 操作ResultSet
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}finally {
// 注意關(guān)閉的順序,應(yīng)該從ResultSet再到Statement再到Connection
// 關(guān)閉ResultSet
if (resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 關(guān)閉Statement
if (statement != null){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 關(guān)閉Connection
if (connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
這里一定要注意,資源用完之后一定要關(guān)閉,因?yàn)榻⒁粋€(gè)對應(yīng)的連接是非常消耗資源的,而如果使用完之后不關(guān)閉資源,會(huì)造成非常大的資源浪費(fèi),為了證實(shí)這一點(diǎn),下面進(jìn)行一個(gè)簡單的測試,測試建立一個(gè)Connection所消耗的時(shí)間
@Test
public void testConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
long start = System.currentTimeMillis();
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
long end = System.currentTimeMillis();
connection.close();
System.out.printf("cost time %d ms\n", (end - start));
}
對應(yīng)的輸出結(jié)果如下所示
cost time 360 ms
可以看到,建立一個(gè)Connection連接需要消耗非常多的時(shí)間,原因在于,建立連接的時(shí)候,本質(zhì)上是通過驅(qū)動(dòng)程序與數(shù)據(jù)庫之間建立一個(gè)Socket連接,對于Socket有了解的朋友應(yīng)該知道,建立Socket連接是比較消耗時(shí)間的,而且維持一個(gè)Socket連接也是非常消耗資源的,所以當(dāng)資源使用完畢之后,應(yīng)該關(guān)閉對應(yīng)的資源,當(dāng)然,更好的做法是使用數(shù)據(jù)庫連接池技術(shù),將Connection對象緩存起來,來避免頻繁創(chuàng)建Connection對象。
JDBC實(shí)戰(zhàn)
上面大概了解了JDBC的操作步驟以及基本的注意事項(xiàng),接下來通過一個(gè)例子來使用JDBC操作數(shù)據(jù)庫,加深對JDBC使用的了解
建立連接的過程同上,這里不重復(fù)敘述
插入數(shù)據(jù)
@Test
public void testInsert() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
String sql = "Insert into user(id, name) value(4, 'Tom')";
Statement statement = connection.createStatement();
int result = statement.executeUpdate(sql);
System.out.println(result);
statement.close();
connection.close();
}
上面是最簡單的插入操作,不過一般來說,我們會(huì)插入?yún)?shù)而不是固定的數(shù)值,所以上面的內(nèi)容會(huì)變成
int id = 4;
String name = "Tom";
// 拼接SQL語句
String sql = "Insert into user(id, name) value("+ id +", '"+ name +"')";
通過拼接SQL語句,然后使用Statement來執(zhí)行語句是解決插入?yún)?shù)的一種方式,但是,通過這種方式進(jìn)行數(shù)據(jù)庫操作會(huì)出現(xiàn)SQL注入的危險(xiǎn),比如說,當(dāng)有人把參數(shù)name的內(nèi)容填寫為 Tom'); DELETE FROM USER; --,那么此時(shí)拼接后的SQL語句就會(huì)變成insert into user(id, name) values(4, 'tom'); delete from user; -- 此時(shí)就非常危險(xiǎn)了,雖然這里是不會(huì)出現(xiàn)問題的,因?yàn)閟tatement一次只能執(zhí)行一條語句,但是如果是查詢后面被加上這些內(nèi)容,那就非常危險(xiǎn)了。為了避免被注入的情況發(fā)生,JDBC提供了另外一種方式,采用預(yù)編譯處理SQL,然后使用PreparedStatement設(shè)置參數(shù)以及執(zhí)行對應(yīng)的SQL語句,如下。
// 采用占位符來處理
String sql = "Insert into user(id, name) value(?, ?)";
PreparedStatement statement = connection.prepareStatement(sql);
// 設(shè)置對應(yīng)的參數(shù),注意這里是從1開始,不是從0開始
statement.setInt(1, id);
statement.setString(2, name);
// 這里也可以直接使用
// statement.setObject(1, id);
// statement.setObject(2, name);
// 不需要指定對應(yīng)的參數(shù)類型,將其交給JDBC進(jìn)行判斷即可
JDBC是推薦使用PreparedStatement來進(jìn)行SQL處理,而不是Statement來處理,原因在于PreparedStatement提供了預(yù)處理機(jī)制,可以預(yù)防SQL注入的發(fā)送,而且由于是采用預(yù)處理機(jī)制,所以PreparedStatement的執(zhí)行效率要高于Statement
刪除數(shù)據(jù)
@Test
public void testDelete() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
int id = 3;
String sql = "DELETE FROM USER where id = ?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setObject(1, id);
int result = statement.executeUpdate();
statement.close();
connection.close();
}
修改數(shù)據(jù)
@Test
public void testUpdate() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
int id = 2;
String name = "Jack";
String sql = "UPDATE USER SET NAME = ? where id = ?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setObject(1, name);
statement.setObject(2, id);
int result = statement.executeUpdate();
statement.close();
connection.close();
}
查詢數(shù)據(jù)
談到查詢數(shù)據(jù),這里就有一個(gè)新的對象需要介紹,ResultSet,在JDBC中,查詢的結(jié)果一般都是以行為單位的,可以是單行也可以是多行,JDBC將每一行都封裝成一個(gè)ResultSet對象,對于ResultSet的操作,其實(shí)就是一個(gè)迭代的過程,在獲取的時(shí)候,需要通過 ResultSet.getXX(ID) 或者ResultSet.getXX(CLO_NAME)來獲取對應(yīng)行的數(shù)據(jù),然后通過ResultSet.next()將指針移動(dòng)到下一行。
@Test
public void testQuery() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
int id = 1;
String sql = "SELECT id, name FROM USER where id = ?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setObject(1, id);
// 查詢返回的ResultSet
ResultSet resultSet = statement.executeQuery();
// 遍歷ResultSet
while (resultSet.next()){
// 取出對應(yīng)的數(shù)據(jù)
int n_id = resultSet.getInt("id");
String n_name = resultSet.getString("name");
// 封裝成對應(yīng)的對象
User user = new User(n_id, n_name);
System.out.println("user: " + user);
}
// 關(guān)閉對應(yīng)的連接
resultSet.close();
statement.close();
connection.close();
}
大對象處理
所謂的大對象,指的是數(shù)據(jù)庫中定義的CLOB(Character Large Object )、BLOB(Binary Large Object),這兩個(gè)數(shù)據(jù)類型是用于把包含信息量比較大的數(shù)據(jù)存儲(chǔ)在列中,當(dāng)做一個(gè)屬性,其中CLOB主要是用于存放字符類型大對象,比如說一本書的內(nèi)容,BLOB主要用于存放二進(jìn)制類型的大對象,比如說一部小電影。JDBC同樣為操作這兩種數(shù)據(jù)類型提供支持,不過,操作起來跟普通的數(shù)據(jù)類型有所不同,具體如下所示
@Test
public void testBLOB() throws ClassNotFoundException, SQLException, IOException, InterruptedException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
// 存入Blob類型的數(shù)據(jù)
File file = new File("d:/img.jpg");
int id = 121;
String name = "jack";
String sql = "insert into user(id, name, b_data) values(?, ?, ?)";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setObject(1, id);
statement.setObject(2, name);
// 這里需要注意,類型應(yīng)該為Blob,直接傳一個(gè)輸入流即可
statement.setBlob(3, new FileInputStream(file));
int result = statement.executeUpdate();
System.out.println(result);
// 讀取Blob類型的數(shù)據(jù)
sql = "SELECT b_data FROM user WHERE id = ?";
statement = connection.prepareStatement(sql);
statement.setObject(1, id);
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()){
// 注意這里,返回的類型是BLob
Blob b_data = resultSet.getBlob("b_data");
InputStream inputStream = b_data.getBinaryStream();
byte[] buf = new byte[inputStream.available()];
FileOutputStream fileOutputStream = new FileOutputStream(new File("d:/new_img.jpg"));
inputStream.read(buf);
fileOutputStream.write(buf);
fileOutputStream.flush();
System.out.println("finish");
}
resultSet.close();
statement.close();
connection.close();
}
CLOB的操作基本同BLOB,只是對應(yīng)的類型設(shè)置為CLOB即可,需要注意的是,在MySQL中,沒有數(shù)據(jù)類型為CLOB的數(shù)據(jù)類型,其對應(yīng)的是text,這里在創(chuàng)建數(shù)據(jù)表的時(shí)候需要注意一下。
批量處理
所謂的批處理,其實(shí)就是同時(shí)指定多個(gè)SQL語句,通常由兩種做法,一種是循環(huán)執(zhí)行多個(gè)SQL語句,另外一種則是采用JDBC提供的Batch功能,具體如下所示
@Test
public void testBatch() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
String sql = "insert into user(id, name) values(?, 'Tom')";
PreparedStatement statement = connection.prepareStatement(sql);
long start = System.currentTimeMillis();
// 普通循環(huán)執(zhí)行
for (int i = 0; i < 1000; i++){
statement.setObject(1, i);
statement.executeUpdate();
}
long end = System.currentTimeMillis();
System.out.printf("cost time %d\n", end - start);
statement.close();
statement = connection.prepareStatement(sql);
start = System.currentTimeMillis();
// 使用Batch執(zhí)行
for (int i = 1001 ; i < 2000; i++){
statement.setObject(1, i);
statement.addBatch();
}
statement.executeBatch();
end = System.currentTimeMillis();
System.out.printf("cost time %d\n", end - start);
statement.close();
connection.close();
}
有點(diǎn)可惜的是,經(jīng)過測試,這兩種方式的性能很接近,不知道是我操作的問題還是事實(shí)確實(shí)是如此,還望有懂這個(gè)的朋友指點(diǎn)指點(diǎn),
元數(shù)據(jù)
所謂的元數(shù)據(jù),也就是是用于定義其他數(shù)據(jù)的數(shù)據(jù),在SQL中,元數(shù)據(jù)可以理解為就是數(shù)據(jù)庫、表的定義數(shù)據(jù),在JDBC中,有三種類型的元數(shù)據(jù),分別是DatabaseMetaData,ResultSetMetaData,ParameterMetaData,分別是數(shù)據(jù)庫相關(guān)信息,結(jié)果集相關(guān)信息,以及PreparedStatement語句中的相關(guān)參數(shù)信息,具體操作如下所示
@Test
public void testMetaData() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(SQL_URL, SQL_USER_NAME, SQL_PASSWORD);
// DatabaseMetaData 相關(guān)的信息,比如數(shù)據(jù)庫供應(yīng)商名稱,版本等
DatabaseMetaData databaseMetaData = connection.getMetaData();
System.out.println("version : " + databaseMetaData.getDatabaseMajorVersion());
System.out.println("name : " + databaseMetaData.getDatabaseProductName());
int id = 4;
String sql = "select id, name from user where id < ? ";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setObject(1, id);
// ParameterMetaData 相關(guān)信息,比如參數(shù)個(gè)數(shù)
ParameterMetaData parameterMetaData = statement.getParameterMetaData();
int paramCount = parameterMetaData.getParameterCount();
System.out.println("parameter count : " + paramCount);
ResultSet resultSet = statement.executeQuery();
// ResultSetMetaData 相關(guān)信息,比如取出來的數(shù)據(jù)的列名
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
int resultCount = resultSetMetaData.getColumnCount();
System.out.println("resultCount : " + resultCount);
for (int i = 0; i < resultCount; i++) {
System.out.print(resultSetMetaData.getColumnName(i + 1) + " ");
}
System.out.println();
}
事務(wù)管理
所謂的事務(wù),是指做一件事情,這件事情有多個(gè)步驟,這多個(gè)步驟這件,要么都完成,要么都不完成,事務(wù)具有四個(gè)特性ACID,分別是原子性,一致性,獨(dú)立性,持久性
在JDBC中,可以通過connection.setAutoCommit(true/false);開控制事務(wù),默認(rèn)為true,也就是默認(rèn)自動(dòng)提交事務(wù)。通過connection.commit()手動(dòng)提交事務(wù),connection.setSavepoint();設(shè)置保存點(diǎn),connection.rollback();回滾到保存到。