JDBC英文全稱是Java Database Connectivity,也就是Java數據庫連接。這是一個Java連接SQL數據庫的標準,包含了常用的API,讓我們能方便的連接盒管理SQL數據庫。每個數據庫廠商都會提供相應的JDBC驅動程序,實現(xiàn)相應的接口。這樣我們就能以統(tǒng)一的方式,操作不同的數據庫了。
建立連接
要使用JDBC,首先要做的事情就是建立一個數據庫連接,這是一個java.sql.Connection對象,提供了很多功能。詳細的使用方法可以參考JavaDoc。要創(chuàng)建一個Connection對象,我們需要使用以下語句:
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
這個方法會拋出SQL異常,所以在使用的時候需要包裹在try-catch塊中或者向上一級拋出異常。
DriverManager的getConnection方法會接受三個參數,URL代表JDBC連接字符串,還有兩個參數是用戶名和密碼。每個數據庫都有自己的連接字符串,這里列舉幾個常用的。DriverManager能夠自動查找和加載驅動程序類,但是有時候(比如使用Hibernate)可能需要手動指定驅動程序類,這時候就需要知道驅動程序類的名稱。
| 數據庫 | 連接字符串 | 驅動程序類 |
|---|---|---|
| MySQL | jdbc:mysql://HOST/DATABASE | com.mysql.jdbc.Driver |
| Postgresql | jdbc:postgresql://HOST/DATABASE | org.postgresql.Driver |
| SQL Server | jdbc:microsoft:sqlserver://HOST:1433;DatabaseName=DATABASE | com.microsoft.jdbc.sqlserver.SQLServerDriver |
下面我們以MySQL數據庫為例。默認情況下MySQL的連接字符串應該是這樣:jdbc:mysql://localhost:3306/jdbclearn,jdbclearn為我們所使用數據庫的名稱。
使用Statement
有了數據庫連接之后,我們就可以執(zhí)行SQL語句了。執(zhí)行SQL語句需要創(chuàng)建一個Statement對象??梢杂靡韵抡Z句創(chuàng)建Statement:
Statement statement = connection.createStatement()
有了Statement對象,就可以調用它的方法來具體執(zhí)行SQL語句了。根據功能可以將SQL語句分為兩種,查詢和更新。查詢語句是對數據庫的查詢,不涉及數據的更改。更新語句包括插入、更新、刪除等操作,會修改數據庫的狀態(tài)。
執(zhí)行更新
執(zhí)行更新需要調用Statement的executeUpdate方法,接受一個SQL更新字符串。這個方法實際上還會返回一個整數,表示受到影響的行數,不過一般情況下我們用不到。
下面的語句簡單的執(zhí)行了一條SQL插入語句。
statement.executeUpdate("INSERT INTO user(username,password) VALUES('yitian','123456')");
執(zhí)行查詢
另一類語句就是查詢語句了。執(zhí)行查詢語句需要調用Statement的executeQuery方法,這個方法接受一個查詢字符串,會返回一個ResultSet對象,也就是查詢的結果集。這個對象會包含所有的查詢結果和一個游標。下面的例子執(zhí)行一個SQL查詢,將結果放到相應的實體類中,然后得到一個List。
List<User> users = new ArrayList<>();
try (Statement statement = connection.createStatement()) {
try (ResultSet rs = statement.executeQuery("SELECT *FROM user")) {
while (rs.next()) {
User user = new User();
user.setId(rs.getInt(1));
user.setUsername(rs.getString(2));
user.setPassword(rs.getString(3));
user.setNickname(rs.getString(4));
user.setBirthday(rs.getDate(5));
users.add(user);
}
}
}
next方法會將游標移動到下一個數據,如果沒有下一個數據,就會返回false。在剛獲取到結果集的時候,游標默認在第一個數據之前,所以我們可以將next方法放到循環(huán)中,來獲取所有數據。結果集對象提供了一組get方法,用來獲取結果。對于大多數數據庫類型都有對應的Java類型,我們調用對應的方法就可以獲取到數據了。這些get方法可以接受列名或者是列編號,如果使用編號的話性能會更好一點,因為不需要查詢列名。需要注意列編號以1開始,不要和以零開始的數組相混淆。
由于數據庫連接、語句和結果集對象都實現(xiàn)了AutoCloseable接口,所以我們可以將其放入到自動資源清理語句中。
預編譯的語句
普通的Statement雖然靈活,可以執(zhí)行任意的SQL語句,但是它有幾個缺點,第一,每次執(zhí)行查詢都需要將語句傳入數據庫中,不夠高效;第二,如果要查詢的語句很長,包含多個參數,需要拼接大量字符串,費時費力;第三,和第二點相關,如果用戶輸入的數據是virus';drop table user;這樣的用戶名,可能會擾亂SQL語句,甚至清除數據庫,這就是所謂的SQL注入。要避免以上問題很簡單,就是使用預編譯的語句,也就是PreparedStatement對象。
使用方法很簡單,和普通的語句類似,只不過換成了PreparedStatement,然后在創(chuàng)建預編譯語句的時候需要在創(chuàng)建時指定SQL字符串,參數使用問號?代替。然后用一組set方法將參數傳入,然后才能執(zhí)行語句。
try (PreparedStatement statement
= connection.prepareStatement("INSERT INTO user(username,password,nickname,birthday) VALUES(?,?,?,?)")) {
statement.setString(1, "test2");
statement.setString(2, "12345678");
statement.setString(3, "張三");
statement.setDate(4, new Date(new java.util.Date().getTime()));
int rows = statement.executeUpdate();
assertThat(rows, is(1));
}
上面普通的語句有什么缺點,預編譯的語句就有什么優(yōu)點。所以如果沒有什么特殊要求,最好在項目中全部使用預編譯的語句。
結果集
執(zhí)行查詢之后JDBC會返回一個結果集對象,結果集對象包含了我們獲取查詢結果的很多方法。最常用的方法就是前面的做法,在循環(huán)中調用結果集的next方法,然后獲取每一行內容。
結果集的常用方法如下:
| 方法名 | 作用 |
|---|---|
| absolute(int i) | 將游標移動到結果集的第i行 |
| afterLast() | 將游標移動到結果集的最后一行的后面 |
| beforeFirst() | 將游標移動到結果集第一行的前面 |
| first() | 將游標移動到第一行 |
| last() | 將游標移動到最后一行 |
| getXXX(int columnIndex) | 一組get方法,按列序號獲取當前行的數據 |
| getXXX(String columnLabel) | 一組get方法,按列名稱獲取當前行的數據 |
| deleteRow() | 刪除當前行的數據,也會從地從數據庫中刪除 |
| updateXXX | 一組update方法,用來更新結果集的,和get方法一樣,存在按照列名和列序號兩種方式更新數據 |
| updateRow() | 將更新之后的行寫入結果集和底層數據庫 |
默認情況下結果集只支持一次遍歷,也就是說游標在遍歷到下一條數據之后,就無法后退了。我們也可以打開結果集的遍歷和編輯功能。要打開結果集的遍歷和編輯功能,需要在創(chuàng)建語句對象的時候同時指定結果集的標志。然后就可以使用上面列舉出的各種方法對結果集進行遍歷和編輯、刪除操作了。
try (Statement statement
= connection.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE)) {
statement.executeUpdate("INSERT INTO user(username,password) VALUES('test3','112233')");
try (ResultSet rs = statement.executeQuery("SELECT *FROM user WHERE username='test3'")) {
rs.absolute(1);
rs.updateString("password", "987654321");
rs.updateRow();
}
try (ResultSet rs = statement.executeQuery("SELECT *FROM user WHERE username='test3'")) {
rs.absolute(1);
assertThat(rs.getString("password"), equalTo("987654321"));
}
}
元數據
利用元數據可以獲取關于JDBC的更多信息我們可以在數據庫連接、結果集等對象上調用getMetaData方法,獲取相應的元數據對象。
下面是一個數據庫元數據的例子,我們可以使用元數據獲取數據庫連接的詳細屬性。
DatabaseMetaData metaData = connection.getMetaData();
logger.info("DriverName:{}", metaData.getDriverName());
logger.info("DriverVersion:{}", metaData.getDriverVersion());
利用結果集元數據,我們可以獲取結果集的詳細信息。下面利用元數據獲取了結果集各列的列名。
try (PreparedStatement statement
= connection.prepareStatement("SELECT *FROM user")) {
try (ResultSet rs = statement.executeQuery()) {
ResultSetMetaData metaData = rs.getMetaData();
for (int i = 0; i < metaData.getColumnCount(); ++i) {
System.out.print(String.format("%s\t", metaData.getColumnName(i + 1)));
}
System.out.println();
while (rs.next()) {
for (int i = 0; i < metaData.getColumnCount(); ++i) {
System.out.print(String.format("%s\t", rs.getString(i + 1)));
}
System.out.println();
}
}
}
列集
ResultSet是對查詢結果的一個抽象,但是結果集有一些局限性。所以出現(xiàn)了一個功能更強的接口就是列集RowSet,它繼承自結果集,所以具備結果集的所有特性,同時還增加了一些功能。
下面就是一個使用JdbcRowSet的小例子。更多列集的使用方法請參考相關文檔和博客。
RowSetFactory factory = RowSetProvider.newFactory();
try (RowSet rs = factory.createJdbcRowSet()) {
rs.setUrl(JdbcUtil.URL);
rs.setUsername(JdbcUtil.USERNAME);
rs.setPassword(JdbcUtil.PASSWORD);
rs.setCommand("select *from user");
rs.execute();
ResultSetMetaData metaData = rs.getMetaData();
for (int i = 0; i < metaData.getColumnCount(); ++i) {
System.out.print(String.format("%s\t", metaData.getColumnName(i + 1)));
}
System.out.println();
while (rs.next()) {
for (int i = 0; i < metaData.getColumnCount(); ++i) {
System.out.print(String.format("%s\t", rs.getString(i + 1)));
}
System.out.println();
}
}
相應的我寫了一個小項目來演示上面的這些例子。項目托管在Github上,地址在這里。有興趣的同學可以看看。
存儲過程
在JDBC中也可以執(zhí)行存儲過程。我們以MySQL存儲過程為例。下面是兩個存儲過程。
CREATE PROCEDURE find_all_blogs_of(IN user_id INT)
BEGIN
SELECT
id,
username,
password,
nickname,
birthday
FROM user
WHERE id = user_id;
END;
CREATE PROCEDURE get_total_user_count(OUT count INT)
BEGIN
SELECT count(id)
FROM user
INTO count;
END;
執(zhí)行存儲過程需要使用CallableStatement。當存儲過程需要IN參數的時候,像普通查詢參數那樣使用setInt這樣的方法設置即可。如果存儲過程是查詢數據的,可以直接使用結果集返回。
CallableStatement statement = connection.prepareCall("CALL find_all_blogs_of(?)");
statement.setInt(1, 1);
ResultSet rs = statement.executeQuery();
如果存儲過程使用OUT參數返回結果,那么情況稍微有些復雜。我們需要使用registerOutParameter方法注冊一個輸出參數。然后在存儲過程執(zhí)行之后獲取該參數的值。
CallableStatement statement = connection.prepareCall("CALL get_total_user_count(?)");
statement.registerOutParameter(1, Types.INTEGER);
statement.execute();
int count = statement.getInt(1);
數據源
前面我們使用DriverManager來獲取連接對象。但是在實際環(huán)境中最好使用數據原來實現(xiàn)相同的功能。JDBC定義了一個DataSource接口,所有的JDBC驅動都實現(xiàn)了該接口。除了JDBC驅動之外,還有一些類庫頁實現(xiàn)了該接口,提供了方便的數據源功能。以MySQL為例,我們來設置一個MysqlConnectionPoolDataSource數據源。
MysqlConnectionPoolDataSource dataSource = new MysqlConnectionPoolDataSource();
dataSource.setUrl(JdbcUtil.URL);
dataSource.setUser(JdbcUtil.USERNAME);
dataSource.setPassword(JdbcUtil.PASSWORD);
dataSource.setUseSSL(false);
有了數據源,我們就可以調用數據源的getConnection方法獲取連接對象了。如果查看MySQL的源代碼或者文檔會發(fā)現(xiàn),MysqlConnectionPoolDataSource還提供了大量set方法設置數據源的各種屬性,因此數據源應該是創(chuàng)建數據庫連接的首選方式。
事務管理
前面我們都是在執(zhí)行了SQL語句之后,立刻獲得了結果。我們還可以使用JDBC的事務管理功能。首先需要調用Connection.setAutoCommit(false)將自動提交關閉,然后使用Connection.commit和Connection.rollback提交或回滾事務。
DataSource dataSource = DataSourceUtils.getDataSource();
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
PreparedStatement selectOne = conn.prepareStatement("SELECT count(id) FROM user WHERE username =?");
PreparedStatement insertOne = conn.prepareStatement("INSERT INTO user(username,password) VALUES(?,?)");
//成功插入
String username1 = "zhang3";
insertOne.setString(1, username1);
insertOne.setString(2, "123456");
insertOne.executeUpdate();
conn.commit();
selectOne.setString(1, username1);
ResultSet rs = selectOne.executeQuery();
rs.first();
assertThat(rs.getInt(1), is(1));
//插入失敗
String username2 = "li4";
insertOne.setString(1, username2);
insertOne.setString(2, "123456");
insertOne.executeUpdate();
conn.rollback();
selectOne.setString(1, username2);
rs = selectOne.executeQuery();
rs.first();
assertThat(rs.getInt(1), is(0));
參考資料
http://alvinalexander.com/java/jdbc-connection-string-mysql-postgresql-sqlserver