JDBC
JDBC(Java Data Base Connectivity, java數(shù)據(jù)庫連接)是一種用于執(zhí)行SQL語句的javaAPI,可以為多種關(guān)系數(shù)據(jù)庫提供統(tǒng)一訪問,它由一組用Java語言編寫的類和接口組成。JDBC提供了一種基準(zhǔn),據(jù)此可以構(gòu)建更高級的工具和接口,使數(shù)據(jù)庫開發(fā)人員能夠編寫數(shù)據(jù)庫應(yīng)用程序。
- 加載驅(qū)動(dòng)
- 連接數(shù)據(jù)庫
- 使用語句操作數(shù)據(jù)庫
- 關(guān)閉數(shù)據(jù)庫連接,釋放資源
加載驅(qū)動(dòng)(以MySQL舉例)
Java連接MySQL需要驅(qū)動(dòng)包,最新下載地址為:http://dev.mysql.com/downloads/connector/j/,解壓后得到j(luò)ar庫文件,然后在對應(yīng)的項(xiàng)目中導(dǎo)入該庫文件。
MySQL8.0之前版本
驅(qū)動(dòng)名:com.mysql.jdbc.Driver
加載方式:Class.forName("com.mysql.jdbc.Driver")
MySQL8.0之后版本
驅(qū)動(dòng)名:com.mysql.cj.jdbc.Driver
加載方式:Class.forName("com.mysql.cj.jdbc.Driver")
連接關(guān)閉數(shù)據(jù)庫
- DriverManager:驅(qū)動(dòng)管理類,負(fù)責(zé)獲取數(shù)據(jù)庫的連接
- getConnection(String url, String user, String password):試圖建立給定數(shù)據(jù)庫的URL連接
- 數(shù)據(jù)庫連接地址格式:jdbc:mysql://IP:端口號(hào)/數(shù)據(jù)庫名稱
- Connection接口:與特定的數(shù)據(jù)庫連接(會(huì)話)
- createStatement():創(chuàng)建Statement執(zhí)行數(shù)據(jù)庫更刪改查等操作
- close():立即釋放此Connection對象的數(shù)據(jù)庫和JDBC資源,而不是等待他們被自動(dòng)釋放
舉個(gè)例子:
import java.sql.*;
public class MySQLDemo{
//MySQL8.0以下版本-JDBC數(shù)據(jù)庫名及數(shù)據(jù)庫URL,數(shù)據(jù)庫名稱為test_db
static final String JDBC_DRIVER = "com.mysql.Driver";
statis final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//MySQL8.0以上版本-JDBC數(shù)據(jù)庫名及數(shù)據(jù)庫URL,數(shù)據(jù)庫名稱為test_db
//static final String JDBC_DRIVER="com.mysql.cj.jdbc.Driver";
//static final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//數(shù)據(jù)庫的用戶名與密碼,根據(jù)你自己的數(shù)據(jù)庫進(jìn)行設(shè)置
static final String User ="root";
static final String Password="123456";
public static void main(String... args){
Connection conn = null;
try{
//注冊JDBC驅(qū)動(dòng)
Class.forName(JDBC_DRIVER);
System.out.println("注冊驅(qū)動(dòng)成功");
//連接數(shù)據(jù)庫并得到Connection會(huì)話
conn = DriverManager.getConnection(DB_URL, User, Password);
}catch(ClassNotFoundException e){
e.printStackTrace();
System.out.println("注冊驅(qū)動(dòng)失敗")
}finally{
//最后關(guān)閉Connection會(huì)話,釋放資源
if(conn !=null){
try{
conn.close();
}catch(SQLException e){
e.printStackTrace();
}
}
}
}
}
操作數(shù)據(jù)庫(Statement)
- 作用:用于執(zhí)行靜態(tài)SQL語句并返回它所生成結(jié)果的對象;
- int executeUpdate(String sql)執(zhí)行給定SQL語句,該語句可能為INSERT、UPDATE或DELETE語句,或者不返回任何內(nèi)容的SQL語句(如SQL DDL 語句)
- void close():立即釋放此Statement對象的數(shù)據(jù)庫和資源,而不是等待該對象自動(dòng)關(guān)閉
封裝DBUtil工具類
public class DBUtil{
//MySQL8.0以下版本-JDBC數(shù)據(jù)庫名及數(shù)據(jù)庫URL,數(shù)據(jù)庫名稱為test_db
static final String JDBC_DRIVER = "com.mysql.Driver";
statis final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//MySQL8.0以上版本-JDBC數(shù)據(jù)庫名及數(shù)據(jù)庫URL,數(shù)據(jù)庫名稱為test_db
//static final String JDBC_DRIVER="com.mysql.cj.jdbc.Driver";
//static final String DB_URL="jdbc:mysql://localhost:3306/test_db"
//數(shù)據(jù)庫的用戶名與密碼,根據(jù)你自己的數(shù)據(jù)庫進(jìn)行設(shè)置
static final String User ="root";
static final String Password="123456";
/**
*獲取數(shù)據(jù)庫連接
*/
public Connection getConnection() throws Exception{
//注冊JDBC驅(qū)動(dòng)
Class.forName(JDBC_DRIVER);
//連接數(shù)據(jù)庫并得到Connection會(huì)話
return DriverManager.getConnection(DB_URL, User, Password);
}
/**
*關(guān)閉連接
* @param conn
* @throws Exception
*/
public void close(Connection conn) throws Exception{
if(conn!=null){
conn.close();
}
}
/**
* 同時(shí)關(guān)閉Statement和Connection
*/
public void close(Statement stmt, Connection conn) throws Exception{
if(stmt !=null){
stmt.close();
}
if(conn !=null){
conn.close();
}
}
}
下面執(zhí)行連接數(shù)據(jù)庫均采用上述的DBUtil工具類。
創(chuàng)建測試數(shù)據(jù)
接下來我們在MySQL中創(chuàng)建test_db數(shù)據(jù)庫并創(chuàng)建websites數(shù)據(jù)表,表結(jié)構(gòu)如下:
CREATE TABLE `websites` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(20) NOT NULL DEFAULT '' COMMENT '站點(diǎn)名稱',
`url` varchar(255) NOT NULL DEFAULT '',
`alexa` int(11) NOT NULL DEFAULT '0' COMMENT 'Alexa 排名',
`country` char(10) NOT NULL DEFAULT '' COMMENT '國家',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
測試插入:
public class TestInsert{
public static void main(String... args) throws Exception{
DBUtil dbUtil = new DBUtil();
//SQL語句,這里的1也可以設(shè)置為null,因?yàn)闉樗O(shè)置了自增
String sql="INSERT INTO `websites` VALUES ('1', 'Google', 'https://www.google.cm/', '1', 'USA')";
Connection conn = dbUtil.getConnection();//獲取數(shù)據(jù)庫連接
Statement stmt = conn.createStatement();//獲取Statement
//進(jìn)行SQL語句進(jìn)行插入,其中返回結(jié)果表示數(shù)據(jù)庫操作的數(shù)量,這里插入一條數(shù)據(jù)返回的就是1,插入N條返回N。
int result = stmt.executeUpdate(sql);
System.out.println("操作的結(jié)果:"+ result+"數(shù)據(jù)");
stmt.close();關(guān)閉stmt
dbUtil.close(conn);//關(guān)閉Connection
}
}
這里我們將需要插入的數(shù)據(jù)直接封裝在SQL語句中,但是實(shí)際開發(fā)的時(shí)候是由外部傳入的,我們可以進(jìn)行修改為:
public class TestInsert{
public static int addWebsite(String name, String url, int alexa, String country) throws Exception{
DBUtil dbUtil = new DBUtil();
//SQL語句,這里的1也可以設(shè)置為null,因?yàn)闉樗O(shè)置了自增
String sql="INSERT INTO `websites` VALUES (null, '"+name+"', '"+website+"', '"+alexa+"', '"+country+"')";
Connection conn = dbUtil.getConnection();//獲取數(shù)據(jù)庫連接
Statement stmt = conn.createStatement();//獲取Statement
//進(jìn)行SQL語句進(jìn)行插入,其中返回結(jié)果表示數(shù)據(jù)庫操作的數(shù)量,這里插入一條數(shù)據(jù)返回的就是1,插入N條返回N。
int result = stmt.executeUpdate(sql);
System.out.println("操作的結(jié)果:"+ result+"數(shù)據(jù)");
stmt.close();關(guān)閉stmt
dbUtil.close(conn);//關(guān)閉Connection
return result;
}
public static void main(String... args) throws Exception{
int result=addWebsite("Google", "https://www.google.com/", 1, "USA");
if(result==1){
System.out.println("添加成功");
}else{
System.out.println("添加失敗");
}
}
}
但是傳入那么多參數(shù)并不符合面向?qū)ο蟮乃枷?,我們?yīng)該將網(wǎng)站信息封裝成Java類,以對象的形式進(jìn)行操作,具體如下:
class Website{
private int id;
private String name;
private String url;
private int alexa;
private String country;
//下面是對應(yīng)的構(gòu)造方法、Getter和Setter方法,請自行添加
}
添加完Website類之后,TestInsert添加AddWebsite2方法,操作如下:
public static int addWebsite2(Website website) throws Exception{
DBUtil dbUtil = new DBUtil();
//SQL語句,這里的1也可以設(shè)置為null,因?yàn)闉樗O(shè)置了自增
String sql="INSERT INTO 'websites' VALUES (null, '"+website.getName()+"', '"+website.getUrl()+"', '"+website.getAlexa()+"', '"+website.getCountry()+"')";
Connection conn = dbUtil.getConnection();//獲取數(shù)據(jù)庫連接
Statement stmt = conn.createStatement();//獲取Statement
//進(jìn)行SQL語句進(jìn)行插入,其中返回結(jié)果表示數(shù)據(jù)庫操作的數(shù)量,這里插入一條數(shù)據(jù)返回的就是1,插入N條返回N。
int result = stmt.executeUpdate(sql);
System.out.println("操作的結(jié)果:"+ result+"數(shù)據(jù)");
stmt.close();關(guān)閉stmt
dbUtil.close(conn);//關(guān)閉Connection
return result;
}
修改main方法如下:
public static void main(String... args) throws Exception{
Website website = new Website("Google", "http://www.google.com/", 1, "USA");
int result=addWebsite2(website);
System.out.println(result==1?"添加成功":"添加失敗");
}
其中更新、刪除操作進(jìn)本相同,不同的是數(shù)據(jù)庫語句,這里就不過多的舉例子!
PreparedStatement實(shí)現(xiàn)增刪改
PreparedStatement是Statement的子接口,屬于預(yù)處理操作。與Statement不同的是,PreparedStatement在操作時(shí),是先在數(shù)據(jù)表中準(zhǔn)備好一條SQL語句,但是此SQL語句的具體內(nèi)容暫時(shí)不設(shè)置,而是在之后進(jìn)行設(shè)置。
插入
我們?nèi)匀皇褂蒙厦娴臄?shù)據(jù)庫,使用PreparedStatement插入一個(gè)網(wǎng)站,具體如下:
DBUtil dbUtil = new DBUtil();
public static int addWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql = "INSERT INTO websites values(null, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.setString(1, website.getName());//從1開始
pstmt.setString(2, website.getUrl());
pstmt.setInt(3, website.getAlexa());
pstmt.setString(4, website.getCountry());
//所以SQL語句的四個(gè)問號(hào)分別對應(yīng)name、URL、alexa、country
int result = conn.executeUpdate();
dbUtil.close(pstmt, conn);
return result;
}
更新
現(xiàn)在數(shù)據(jù)庫中已經(jīng)存在了(id=1,name=Google,url="https://www.google.com/",alexa=1,country=USA)的數(shù)據(jù)了。如果我們要將更改這條數(shù)據(jù),用PreparedStatement接口應(yīng)該怎么做呢?
DBUtil dbUtil = new DBUtil();
public static int updateWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql ="UPDATE websites set name=?,url=?,alexa=?,country=? where id=?";
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.putString(1, website.getName());
pstmt.putString(2, website.getUrl());
pstmt.putInt(3, website.getAlexa());
pstmt.putString(4, website.getCountry());
pstmt.putInt(5, website.getId());
int result =pstmt.executeUpdate();
dbUtil.close(pstmt, conn);
return result;
}
刪除
假設(shè)我們要?jiǎng)h除id=1的數(shù)據(jù),代碼如下:
DBUtil dbUtil = new DBUtil();
//刪除指定id的websiste
public static int deleteWebsite(int id) throws Exception{
Connection conn = dbUtil.getConnection();
String deleteSql ="DELETE from websites where id=?";
PreparedStatement pstmt = conn.preparedStatement(deleteSql);
pstmt.putInt(1, id);
int result=pstmt.executeUpdate();
dbUtil.close(pstmt, conn);
return result;
}
ResultSet結(jié)果集
當(dāng)我們查詢數(shù)據(jù)庫時(shí),返回的是一個(gè)二維的結(jié)果集,我們需要使用ResultSet來遍歷結(jié)果,獲取每一行的數(shù)據(jù)。
ResultSet光標(biāo)最初位于第一行之前,第一次調(diào)用next()方法使第一行成為當(dāng)前行,第二次調(diào)用使第二行成為當(dāng)前行,以此類推。
- boolean next():將光標(biāo)從當(dāng)期位置向前移一行;
- String getString(int columnIndex):以Java編程語言中String的形式獲取ResultSet對象的當(dāng)前行中指定列的值;
- String getString(int columnLabel):以Java編程語言中String的形式獲取ResultSet對象的當(dāng)前行中指定列的值。
實(shí)戰(zhàn)
DBUtil dbUtil = new DBUtil();
public List<Website> queryAllWebsite()throws Exception{
Connection conn = dbUtil.getConnection();
String querySql = "SELECT * from websites";
PreparedStatement pstmt = conn.preparedStatement(querySql);
ResultSet rs = pstmt.executeQuery();//executeQuery執(zhí)行查詢,返回ResultSet結(jié)果集
List<Website> websites = new ArrayList();
while(rs.next()){
int id = rs.getInt(1);//獲取第一列的值:id
String name=rs.getString(2);//獲取第二列的值:name
String url=rs.getString(3);//獲取第三列的值:url
int alexa=rs.getInt(4);//獲取第四列的值:alexa
String country=rs.getString(5);//獲取第五列的值:country
Website website=new Website(id, name, url, alexa, country);
websites.add(website);
}
dbUtil.close(pstmt, conn);
return websites;
}
在while語句中我們也可以直接使用字段名稱進(jìn)行查詢,操作如下:
int id = rs.getInt("id");
String name=rs.getString("name");
String url=rs.getString("url");
int alexa=rs.getInt("alexa");
String country=rs.getString("country");
處理大數(shù)據(jù)對象
- CLOB:(character large object)可以存儲(chǔ)字符大數(shù)據(jù)對象,比如長篇小說;
- BLOB:(binary large object)可以存放二進(jìn)制大數(shù)據(jù)對象,比如圖片、電影、音樂;
CLOB實(shí)戰(zhàn)
因?yàn)镃LOB是存儲(chǔ)字符大數(shù)據(jù)對象,我們可以設(shè)置字段的類型為TEXT或者LONGTEXT
- 在websites表中增加
about字段,字段類型為longtext - 假設(shè)在電腦的C盤根目錄下新增一個(gè)helloworld.txt文本文件,內(nèi)容不限;
- 在Website類中增加類型為File的about字段。
實(shí)例代碼如下:
public class TestClob{
DBUtil dbUtil = new DBUtil();
//插入網(wǎng)站
public int addWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql = "INSERT INTO websites values(null, ?,?,?,?,?)";//多了about的?
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.putString(1, website.getName());
pstmt.putString(2, website.getUrl());
pstmt.putInt(3, website.getAlexa());
pstmt.putString(4, website.getCountry());
File about=website.getAbout();
InputStream in = new FileInputStream(about);
pstmt.setAsciiStream(5, in, in.length());//這里不是putString而是setAsciiStream
int result=pstmt.excuteUpdate();
dbUtil.close(pstmt, conn);
return result;
}
//讀取網(wǎng)站
public void queryWebsite(int id)throws Exception{
Connection conn = dbUtil.getConnection();
String sql="SELECT * from websites where id=?";
PreparedStatement pstmt = conn.preparesStatement(sql);
pstmt.putString(1, id);
ResultSet rs = pstmt.excuteQuery();
while(rs.next()){
int id = rs.getInt("id");//依次獲取name|country|alexa|url即可
Clob clob = rs.getClob("about");//先通過getClob獲取Clob對象
String about=clob.getSubString(1,(int)clob.length());//利用getSubString獲取內(nèi)容
//也可以通過getAsciiStream流的形式獲取內(nèi)容
System.out.println("id:"+id+"\tabout:" + about);
}
dbUtil.close(pstmt, conn);
}
public static void main(String... args) throws Exception{
Website website = new Website("Google","http://www.google.com/", 1, "USA");
File fAbout = new File("C:/helloworld.txt");
website.setAbout(fAbout);//設(shè)置文件
int result = addWebsite(website);
System.out.println(result==1?"添加成功":"添加失敗");
}
}
BLOB實(shí)戰(zhàn)
因?yàn)锽lob是存儲(chǔ)二級制文件,包括圖片、音樂等,數(shù)據(jù)庫中可以申明為BLOB、LONGBLOB類型。
- 在websites表中增加logo字段(網(wǎng)站logo),字段類型為BLOB;
- 假設(shè)在電腦的C盤根目錄下存放網(wǎng)站logo的圖片名為logo.jpg;
- 在Website類中增加類型為File的logo字段
實(shí)例代碼如下:
public class TestClob{
DBUtil dbUtil = new DBUtil();
//插入網(wǎng)站
public int addWebsite(Website website) throws Exception{
Connection conn = dbUtil.getConnection();
String sql = "INSERT INTO websites values(null, ?,?,?,?,?,?)";//多了一個(gè)logo的?
PreparedStatement pstmt = conn.preparedStatement(sql);
pstmt.putString(1, website.getName());
pstmt.putString(2, website.getUrl());
pstmt.putInt(3, website.getAlexa());
pstmt.putString(4, website.getCountry());
File about=website.getAbout();
InputStream in = new FileInputStream(about);
pstmt.setAsciiStream(5, in, in.length());//這里不是putString而是setAsciiStream
File logo = website.getLogo();
InputStream logoIn= new FileInputStream(logo);
pstmt.setBinaryStream(6, logoIn, logoIn.length());//這里是setBinaryStream
int result=pstmt.excuteUpdate();
dbUtil.close(pstmt, conn);
return result;
}
//讀取網(wǎng)站
public void queryWebsite(int id)throws Exception{
Connection conn = dbUtil.getConnection();
String sql="SELECT * from websites where id=?";
PreparedStatement pstmt = conn.preparesStatement(sql);
pstmt.putString(1, id);
ResultSet rs = pstmt.excuteQuery();
while(rs.next()){
int id = rs.getInt("id");//依次獲取name|country|alexa|url即可
//獲取about
Clob clob = rs.getClob("about");//先通過getClob獲取Clob對象
String about=clob.getSubString(1,(int)clob.length());//利用getSubString獲取內(nèi)容
//獲取網(wǎng)站logo
Blob bLogo = rs.getBlob("logo");
//定義文件輸出流,將文件保存到D盤根目錄
FileOutputStream out=new FileOutputStream(new File("D:/logo.jpg"));
out.write(bLogo.getBytes(1, ((int)bLogo).length()));
out.close();
System.out.println("id:"+id+"\tabout:" + about);
}
dbUtil.close(pstmt, conn);
}
public static void main(String... args) throws Exception{
Website website = new Website("Google","http://www.google.com/", 1, "USA");
File fAbout = new File("C:/helloworld.txt");
website.setAbout(fAbout);//設(shè)置文件
File fLogo = new File("C:/logo.jpg");
website.setLogo(fLogo);
int result = addWebsite(website);
System.out.println(result==1?"添加成功":"添加失敗");
}
}
CallableStatement調(diào)用存儲(chǔ)過程
CallableStatement主要是調(diào)用數(shù)據(jù)庫中的存儲(chǔ)過程,是PreparedStatement的子接口。使用CallableStatement可以接收存儲(chǔ)過程的返回值。
- void registerOutParameter(int parameterIndex, int sqlType):按順序?yàn)橹筽arameterIndex將OUT參數(shù)注冊為JDBC類型sqlType
首先添加存儲(chǔ)過程,根據(jù)website的id獲取website的名稱,操作如下:
//創(chuàng)建存儲(chǔ)過程
DELIMITER &&
CREATE PROCEDURE pro_getSiteNameById(IN siteId INT, OUT sn CHAR(20))
BEGIN
select name into sn from websites where id=siteId;
END
&&
DELIMITER ;
//測試存儲(chǔ)過程
CALL pro_getSiteNameById(1, @siteName);//執(zhí)行
SELECT @siteName;//輸出
CallableStatement調(diào)用存儲(chǔ)過程,代碼如下:
public class TestProcedure{
DBUtil dbUtil = new DBUtil();
public String getSiteNameById(int siteId) throws Exception{
Connection conn = dbUtil.getConnection();
String sql="{CALL pro_getSiteNameById(?,?)}";
CallableStatement cstmt = conn.prepareCall(sql);//prepareCall得到CallableStatement
cstmt.setInt(1, siteId);//設(shè)置第一個(gè)參數(shù)
cstmt.registerOutParameter(2, Types.CHAR);//注冊第二個(gè)參數(shù)類型
cstmt.execute();
String siteName = cstmt.getString("sn");
dbUtil.close(cstmt,conn);
return siteName;
}
}
使用元數(shù)據(jù)分析數(shù)據(jù)庫
- DatabaseMetaData:獲取數(shù)據(jù)庫基本信息,包括版本、名稱以及表信息等
- String getDatabaseProductName():數(shù)據(jù)庫產(chǎn)品的名稱
- int getDriverMajorVersion():獲取JDBC驅(qū)動(dòng)的主版本號(hào)
- int getDriverMinorVersion():獲取JDBC驅(qū)動(dòng)的次版本號(hào)
- String getDriverName:獲取驅(qū)動(dòng)名稱
- ResultSetMetaData:獲取ResultSet對象中列的基本信息
- int getColumnCount():返回此ResultSet對象中的列數(shù)
- String getColumnName(int column):獲取指定列的名稱
- int getColumnTypeName(int column):獲取指定列的SQL類型名稱
實(shí)例代碼如下:
public void aboutDatabaseMetaData() throws Exception{
DBUtil dbUtil = new DBUtil();
Connection conn = dbUtil.getConnection();
DatabaseMetaData metaData = conn.getMetaData();//獲取元數(shù)據(jù)
System.out.println("數(shù)據(jù)庫名稱:"+ metaData.getDatabaseProductName());
System.out.println("數(shù)據(jù)庫主版本號(hào):" + metaData.getDriverMajorVersion());
}
public void aboutResultMetaData() throws Exception{
DBUtil dbUtil = new DBUtil();
Connection conn = dbUtil.getConnection();
String sql ="select * from websites";
PreparedStatement pstmt = conn.preparedStatement();
ResultSet rs = pstmt.executeQuery(sql);
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
System.out.println("website 共有:" + columnCount+" 列");
for(int i=0; i<columnCount; i++)
{
System.out.println("第"+i+"列名稱為:" + metaData.getColumnName(i)
+"\t 列的類型為:" + metaData.getColumnTypeName(i));
}
}
JDBC處理事務(wù)
所謂事務(wù)就是所有的操作要么一起成功,要么一起失敗。事務(wù)本身具備:
- 原子性:是不可再分割的最小單元,相當(dāng)于一個(gè)個(gè)小的數(shù)據(jù)庫操作,這些操作必須同時(shí)成功,如果一個(gè)失敗則一切操作將全部失敗;
- 一致性:數(shù)據(jù)庫操作前后是一致的,保證數(shù)據(jù)有效性。如果事務(wù)正常操作則系統(tǒng)維持有效性,如果出現(xiàn)了錯(cuò)誤,則回到最原始狀態(tài),也要維持有效性;
- 隔離性:多個(gè)事務(wù)可以同時(shí)進(jìn)行且彼此之間無法訪問,只有當(dāng)事務(wù)完成最終操作時(shí),才可以看到結(jié)果;
- 持久性:事務(wù)完成后,它對系統(tǒng)的影響是持久的。該修改即使出現(xiàn)致命的系統(tǒng)故障也將一直保持。
創(chuàng)建賬戶表
首先創(chuàng)建賬戶表account,包含id、賬戶名稱和賬戶余額,代碼如下:
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(20) NOT NULL DEFAULT '' COMMENT '站點(diǎn)名稱',
`accountBalance` INTEGER NOT NULL DEFAULT '0' COMMENT '賬戶余額',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
然后插入兩個(gè)賬戶,分別為(張三,1000)和(李四,1000)。當(dāng)張三向李四轉(zhuǎn)賬500的時(shí)候,其實(shí)出發(fā)兩個(gè)數(shù)據(jù)庫操作:
- 張三賬戶的余額變成500
- 李四賬戶的月變成1500
必須保證上述兩個(gè)操作同時(shí)成功,才能認(rèn)為轉(zhuǎn)賬操作是成功,如果有一個(gè)失敗則必須回到轉(zhuǎn)賬操作之前的狀態(tài)(兩個(gè)人都是1000)。
不用事務(wù)(存在風(fēng)險(xiǎn))
public class Test{
DBUtil dbUtil = new DBUtil();
/**
* 賬戶轉(zhuǎn)出
*/
public void transferOut(Connection conn, String accountName, int money) throws Exception{
String sql = "UPDATE account set accountBalance=accountBalance-? where name=?";
PreparedStatement pstmt = con.preparedStatement(sql);
pstmt.setInt(1, 500);
pstmt.setString(2, accountName);
pstmt.executUpdate();
}
/**
* 賬戶轉(zhuǎn)入
*/
public void transferIn(Connection conn, String accountName, int money) throws Exception{
String sql = "UPDATE account set accountBalance=accountBalance+? where name=?";
PreparedStatement pstmt = con.preparedStatement(sql);
pstmt.setInt(1, 500);
pstmt.setString(2, accountName);
pstmt.executUpdate();
}
public static void main(String... args) throws Exception{
Connection conn = dbUtil.getConnection();
int money=500;
transferOut(conn, "張三", money);
transferIn(conn, "李四", money);
dbUtil.close(conn);
}
}
以上操作是存在風(fēng)險(xiǎn)的并不能保證兩個(gè)操作能夠同時(shí)成功。
采用事務(wù)操作
轉(zhuǎn)入和轉(zhuǎn)出操作與上面是一致的,主要是修改main方法,代碼如下:
public static void main(String... args){
Connection conn;
try{
conn = dbUtil.getConnection();
conn.setAutoCommit(false);//首先將自動(dòng)提交進(jìn)行關(guān)閉
System.out.println("張三開始向李四轉(zhuǎn)賬");
int money=500;
transferOut(conn, "張三", money);
transferIn(conn, "李四", money);
System.out.println("轉(zhuǎn)賬成功");
}catch(Exception e){
if(conn !=null){
try{
conn.rollback(); //當(dāng)操作失敗的時(shí)候回滾到之前的狀態(tài)
}catch(Exception e){
e.printStackTrace();
}
}
}finally{
if(conn !=null){
try{
conn.commit();//提交事務(wù)
conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
setAutoCommit、rollback、commit三個(gè)方法的結(jié)合實(shí)現(xiàn)了事務(wù)的操作!
保存點(diǎn)
假設(shè)存在這種情況,張三將錢轉(zhuǎn)給第三方,由第三方轉(zhuǎn)給李四,也就是張三負(fù)責(zé)轉(zhuǎn),至于李四是否收到并不關(guān)心。
那么我們可以使用保存點(diǎn)來實(shí)現(xiàn)這個(gè)功能,具體操作如下:
public static void main(String... args){
Connection conn;
SavePoint sp;
try{
conn = dbUtil.getConnection();
conn.setAutoCommit(false);//首先將自動(dòng)提交進(jìn)行關(guān)閉
System.out.println("張三開始向李四轉(zhuǎn)賬");
int money=500;
transferOut(conn, "張三", money);
sp = conn.setSavePoint();//設(shè)置一個(gè)保存點(diǎn)
transferIn(conn, "李四", money);
System.out.println("轉(zhuǎn)賬成功");
}catch(Exception e){
if(conn !=null){
try{
//當(dāng)操作失敗的時(shí)候回滾到sp這個(gè)保存點(diǎn)的位置
conn.rollback(sp);
}catch(Exception e){
e.printStackTrace();
}
}
}finally{
if(conn !=null){
try{
conn.commit();//提交事務(wù)
conn.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}