在JDBC教程中,咱們學習瞭如何使用JDBC API進行數據庫鏈接和執行SQL查詢。此外,還研究了不一樣類型的驅動程序,以及如何編寫鬆散耦合的JDBC程序,幫助咱們輕鬆地切換數據庫服務器。html
本教程旨在詳細介紹JDBC事務管理,以及如何使用JDBC SavePoint進行回滾操做。java
默認狀況下,當咱們建立一個數據庫鏈接時,會運行在自動提交模式(Auto-commit)下。這意味着,任什麼時候候咱們執行一條SQL完成以後,事務都會自動提交。因此咱們執行的每一條SQL都是一個事務,而且若是正在運行DML或者DDL語句,這些改變會在每一條SQL語句結束的時存入數據庫。有時候咱們想讓一組SQL語句成爲事務的一部分,那樣咱們就能夠在全部語句運行成功的時候提交,而且若是出現任何異常,這些語句做爲事務的一部分,咱們能夠選擇將其所有回滾。mysql
讓咱們經過一個簡單的示例理解一下,這裏使用JDBC的事務管理來支持數據的完整性。假設咱們有一個名爲UserDB的數據庫,員工的信息分別存儲在兩張表中。好比我正在使用MySQL數據庫,可是一樣能夠在Oracle和PostgreSQL等其餘的關係型數據庫上運行。sql
數據庫表中存儲員工信息和地址明細。兩張表的DDL腳本以下:shell
1
2
3
4
5
6
7
8
9
10
11
12
13
|
CREATE
TABLE
`Employee` (
`empId`
int
(11) unsigned
NOT
NULL
,
`
name
`
varchar
(20)
DEFAULT
NULL
,
PRIMARY
KEY
(`empId`)
) ENGINE=InnoDB
DEFAULT
CHARSET=utf8;
CREATE
TABLE
`Address` (
`empId`
int
(11) unsigned
NOT
NULL
,
`address`
varchar
(20)
DEFAULT
NULL
,
`city`
varchar
(5)
DEFAULT
NULL
,
`country`
varchar
(20)
DEFAULT
NULL
,
PRIMARY
KEY
(`empId`)
) ENGINE=InnoDB
DEFAULT
CHARSET=utf8;
|
最終的工程以下圖,咱們將逐個查看這些類:數據庫
如圖所示,在工程的build path中有一個 MySQL JDBC 的jar包,這樣就能夠鏈接到MySQL數據庫。服務器
DBConnection.java學習
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package
com.journaldev.jdbc.transaction;
import
java.sql.Connection;
import
java.sql.DriverManager;
import
java.sql.SQLException;
public
class
DBConnection {
public
final
static
String DB_DRIVER_CLASS =
"com.mysql.jdbc.Driver"
;
public
final
static
String DB_URL =
"jdbc:mysql://localhost:3306/UserDB"
;
public
final
static
String DB_USERNAME =
"pankaj"
;
public
final
static
String DB_PASSWORD =
"pankaj123"
;
public
static
Connection getConnection()
throws
ClassNotFoundException,
SQLException {
Connection con =
null
;
// load the Driver Class
Class.forName(DB_DRIVER_CLASS);
// create the connection now
con = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
System.out.println(
"DB Connection created successfully"
);
return
con;
}
}
|
在DBConnection類中,建立MySQL數據庫鏈接供其餘類使用。ui
EmployeeJDBCInsertExample.javaspa
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
|
package
com.journaldev.jdbc.transaction;
import
java.sql.Connection;
import
java.sql.PreparedStatement;
import
java.sql.SQLException;
public
class
EmployeeJDBCInsertExample {
public
static
final
String INSERT_EMPLOYEE_QUERY =
"insert into Employee (empId, name) values (?,?)"
;
public
static
final
String INSERT_ADDRESS_QUERY =
"insert into Address (empId, address, city, country) values (?,?,?,?)"
;
public
static
void
main(String[] args) {
Connection con =
null
;
try
{
con = DBConnection.getConnection();
insertEmployeeData(con,
1
,
"Pankaj"
);
insertAddressData(con,
1
,
"Albany Dr"
,
"San Jose"
,
"USA"
);
}
catch
(SQLException | ClassNotFoundException e) {
e.printStackTrace();
}
finally
{
try
{
if
(con !=
null
)
con.close();
}
catch
(SQLException e) {
e.printStackTrace();
}
}
}
public
static
void
insertAddressData(Connection con,
int
id,
String address,
String city,
String country)
throws
SQLException {
PreparedStatement stmt = con.prepareStatement(INSERT_ADDRESS_QUERY);
stmt.setInt(
1
, id);
stmt.setString(
2
, address);
stmt.setString(
3
, city);
stmt.setString(
4
, country);
stmt.executeUpdate();
System.out.println(
"Address Data inserted successfully for ID="
+ id);
stmt.close();
}
public
static
void
insertEmployeeData(Connection con,
int
id, String name)
throws
SQLException {
PreparedStatement stmt = con.prepareStatement(INSERT_EMPLOYEE_QUERY);
stmt.setInt(
1
, id);
stmt.setString(
2
, name);
stmt.executeUpdate();
System.out.println(
"Employee Data inserted successfully for ID="
+ id);
stmt.close();
}
}
|
這是一個簡單的JDBC程序,向前面建立的Employee表和Address表中插入用戶提供的數據。當咱們將運行這個程序時,將獲得如下輸出:
1
2
3
4
5
6
7
8
9
10
11
12
|
Employee Data inserted successfully
for
ID=1
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long
for
column
'city'
at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.main(EmployeeJDBCInsertExample.java:23)
|
從結果能夠看到,在咱們試圖往Address表中插入數據時,因爲輸入的值超過了字段的大小,所以拋出了SQLException異常。
若是瀏覽Employee和Address表的內容,你會發現Employee表有數據,Address表卻沒有。這是一個嚴重的問題,由於只有部分數據正確地被插入。而且若是咱們再次運行這個程序,它會再次試圖向Employee表插入數據,而且引起下面的異常:
1
2
3
4
5
6
7
8
9
10
11
12
|
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry
'1'
for
key
'PRIMARY'
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:931)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2941)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertEmployeeData(EmployeeJDBCInsertExample.java:57)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.main(EmployeeJDBCInsertExample.java:21)
|
因此,咱們沒有辦法把Employee對應的Address數據保存到Address表中。這個程序形成了數據完整性的問題,這也是爲何須要用事務管理來確保兩張表都得以成功插入,而且若是發生任何異常所有回滾。
JDBC API提供了setAutoCommit()方法,經過它咱們能夠禁用自動提交數據庫鏈接。自動提交應該被禁用,由於只有這樣事務纔不會自動提交,除非調用了鏈接的commit()方法。數據庫服務器使用表鎖來實現事務管理,而且它是一種緊張的資源。所以,在操做完成後應該儘快提交事務。讓咱們編寫另一個程序,這裏我將使用JDBC事務管理特性來保證數據的完整性不被破壞。
EmployeeJDBCTransactionExample.java
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
|
package
com.journaldev.jdbc.transaction;
import
java.sql.Connection;
import
java.sql.SQLException;
public
class
EmployeeJDBCTransactionExample {
public
static
void
main(String[] args) {
Connection con =
null
;
try
{
con = DBConnection.getConnection();
// set auto commit to false
con.setAutoCommit(
false
);
EmployeeJDBCInsertExample.insertEmployeeData(con,
1
,
"Pankaj"
);
EmployeeJDBCInsertExample.insertAddressData(con,
1
,
"Albany Dr"
,
"San Jose"
,
"USA"
);
// now commit transaction
con.commit();
}
catch
(SQLException e) {
e.printStackTrace();
try
{
con.rollback();
System.out.println(
"JDBC Transaction rolled back successfully"
);
}
catch
(SQLException e1) {
System.out.println(
"SQLException in rollback"
+ e.getMessage());
}
}
catch
(ClassNotFoundException e) {
e.printStackTrace();
}
finally
{
try
{
if
(con !=
null
)
con.close();
}
catch
(SQLException e) {
e.printStackTrace();
}
}
}
}
|
在運行程序以前,請確保你清楚地瞭解以前插入的數據。當你運行這個程序時,將獲得下面的輸出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
DB Connection created successfully
Employee Data inserted successfully
for
ID=1
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long
for
column
'city'
at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCInsertExample.insertAddressData(EmployeeJDBCInsertExample.java:45)
at com.journaldev.jdbc.transaction.EmployeeJDBCTransactionExample.main(EmployeeJDBCTransactionExample.java:19)
JDBC Transaction rolled back successfully
|
這段輸出和前面的程序很像,可是若是你查看數據庫表,就會發現數據沒有被插入Employee表。如今咱們能夠修改城市(city)的值,這樣它就能夠符合字段要求,從新運行程序就可以把數據插到兩張表中。注意:只有當兩個插入操做都執行成功時,鏈接纔會提交。若是其中任何一個拋出異常,整個事務會回滾。
有時候一個事務多是一組複雜的語句,所以可能想要回滾到事務中某個特殊的點。JDBC Savepoint幫咱們在事務中建立檢查點(checkpoint),這樣就能夠回滾到指定點。當事務提交或者整個事務回滾後,爲事務產生的任何保存點都會自動釋放並變爲無效。把事務回滾到一個保存點,會使其餘全部保存點自動釋放並變爲無效。
假設咱們有一張日誌表Logs,用來記錄員工信息保存成功的日誌。可是由於它只用於日誌記錄,當插入日誌表有任何異常時,咱們不但願回滾整個事務。咱們來看一下如何用JDBC Savepoint來實現。
1
2
3
4
5
|
CREATE
TABLE
`Logs` (
`id`
int
(3) unsigned
NOT
NULL
AUTO_INCREMENT,
`message`
varchar
(10)
DEFAULT
NULL
,
PRIMARY
KEY
(`id`)
) ENGINE=InnoDB
DEFAULT
CHARSET=utf8;
|
EmployeeJDBCSavePointExample.java
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
68
69
70
71
72
73
74
75
76
77
|
package
com.journaldev.jdbc.transaction;
import
java.sql.Connection;
import
java.sql.PreparedStatement;
import
java.sql.SQLException;
import
java.sql.Savepoint;
public
class
EmployeeJDBCSavePointExample {
public
static
final
String INSERT_LOGS_QUERY =
"insert into Logs (message) values (?)"
;
public
static
void
main(String[] args) {
Connection con =
null
;
Savepoint savepoint =
null
;
try
{
con = DBConnection.getConnection();
// set auto commit to false
con.setAutoCommit(
false
);
EmployeeJDBCInsertExample.insertEmployeeData(con,
2
,
"Pankaj"
);
EmployeeJDBCInsertExample.insertAddressData(con,
2
,
"Albany Dr"
,
"SFO"
,
"USA"
);
// if code reached here, means main work is done successfully
savepoint = con.setSavepoint(
"EmployeeSavePoint"
);
insertLogData(con,
2
);
// now commit transaction
con.commit();
}
catch
(SQLException e) {
e.printStackTrace();
try
{
if
(savepoint ==
null
) {
// SQLException occurred in saving into Employee or Address
// tables
con.rollback();
System.out.println(
"JDBC Transaction rolled back successfully"
);
}
else
{
// exception occurred in inserting into Logs table
// we can ignore it by rollback to the savepoint
con.rollback(savepoint);
// lets commit now
con.commit();
}
}
catch
(SQLException e1) {
System.out.println(
"SQLException in rollback"
+ e.getMessage());
}
}
catch
(ClassNotFoundException e) {
e.printStackTrace();
}
finally
{
try
{
if
(con !=
null
)
con.close();
}
catch
(SQLException e) {
e.printStackTrace();
}
}
}
private
static
void
insertLogData(Connection con,
int
i)
throws
SQLException {
PreparedStatement stmt = con.prepareStatement(INSERT_LOGS_QUERY);
// message is very long, will throw SQLException
stmt.setString(
1
,
"Employee information saved successfully for ID"
+ i);
stmt.executeUpdate();
System.out.println(
"Logs Data inserted successfully for ID="
+ i);
stmt.close();
}
}
|
這段程序很是容易理解。在數據成功插入Employee表和Address表後,建立了一個Savepoint。若是拋出SQLException,而Savepoint爲空,意味着在執行插入Employee或者Address表時發生了異常,因此須要回滾整個事務。
若是Savepoint不爲空,意味着SQLException由插入日誌表Logs操做引起,因此只回滾事務到保存點,而後提交。
運行上面的程序,能夠看到下面的輸出信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
DB Connection created successfully
Employee Data inserted successfully
for
ID=2
Address Data inserted successfully
for
ID=2
com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long
for
column
'message'
at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at com.journaldev.jdbc.transaction.EmployeeJDBCSavePointExample.insertLogData(EmployeeJDBCSavePointExample.java:73)
at com.journaldev.jdbc.transaction.EmployeeJDBCSavePointExample.main(EmployeeJDBCSavePointExample.java:30)
|
若是查看數據庫表,能夠看到數據成功地插入到了Employee表和Address表。須要注意的是,咱們有更簡單的實現方式。當數據成功插入Employee表和Address表時提交事務,使用另外一個事務管理插入日誌的操做。這只是爲了展現Java程序中JDBC Savepoint的用法。
原文地址