摘要:和很像,嚴(yán)格來說,應(yīng)該是模仿了是設(shè)計。程序中使用該類的主要功能是獲取對象,該類包含如下方法該方法獲得對應(yīng)數(shù)據(jù)庫的連接代表數(shù)據(jù)庫連接對象,每個代表一個物理連接會話。當(dāng)執(zhí)行查詢時,返回查詢到的結(jié)果集。程序可以通過操作該對象來取出查詢結(jié)果。
JDBC基礎(chǔ)
JDBC的全稱是Java Database Connectivity,即Java數(shù)據(jù)庫連接,它是一種可以執(zhí)行SQL語句的Java API。程序可通過JDBC API連接到關(guān)系數(shù)據(jù)庫,并使用結(jié)構(gòu)化查詢語言(SQL,數(shù)據(jù)庫標(biāo)準(zhǔn)的查詢語言)來完成對數(shù)據(jù)庫的查詢、更新
與其他數(shù)據(jù)庫編程環(huán)境相比,JDBC為數(shù)據(jù)庫開發(fā)提供了標(biāo)準(zhǔn)的API,使用JDBC開發(fā)的數(shù)據(jù)庫應(yīng)用可以跨平臺運行,而且還可以跨數(shù)據(jù)庫(如果全部使用標(biāo)準(zhǔn)的SQL語句)。也就是說如果使用JDBC開發(fā)一個數(shù)據(jù)庫應(yīng)用,則該應(yīng)用既可以在Windows操作系統(tǒng)上運行,又可以在Unix等其他操作系統(tǒng)上運行,既可以使用MySQL數(shù)據(jù)庫,又可以使用Oracle等其他的數(shù)據(jù)庫,應(yīng)用程序不需要做任何的修改
JDBC簡介Java語言的各種跨平臺特性,都采用相似的結(jié)構(gòu)。因為他們都需要讓相同的程序在不同的平臺上運行,所以需要中間的轉(zhuǎn)換程序(為了實現(xiàn)Java程序的跨平臺,Java為不同的操作系統(tǒng)提供了不同的Java虛擬機)。同樣,為了JDBC程序可以跨平臺,也需要不同的數(shù)據(jù)庫廠商提供相應(yīng)的驅(qū)動程序
Sun提供的JDBC可以完成以下三個基本操作:
建立與數(shù)據(jù)庫的鏈接
執(zhí)行SQL語句
獲得SQL語句的執(zhí)行結(jié)果
JDBC驅(qū)動程序數(shù)據(jù)庫驅(qū)動程序是JDBC程序和數(shù)據(jù)庫之間的轉(zhuǎn)換層,數(shù)據(jù)庫驅(qū)動程序負責(zé)將JDBC調(diào)用映射成特定的數(shù)據(jù)庫調(diào)用
ODB,Open Database Connectivity,即開放數(shù)據(jù)庫鏈接。ODBC和JDBC很像,嚴(yán)格來說,應(yīng)該是JDBC模仿了ODBC是設(shè)計。ODBC也允許應(yīng)用程序通過一種通用的API訪問不同的數(shù)據(jù)庫管理系統(tǒng),從而使得基于ODBC的應(yīng)用程序可以在不同的數(shù)據(jù)庫之間切換。同樣,ODBC也需要各數(shù)據(jù)庫廠商提供相應(yīng)的驅(qū)動程序,而ODBC負責(zé)管理這些驅(qū)動程序
JDBC驅(qū)動通常有如下4種類型
JDBC + ODBC橋的方式
直接將JDBC API隱射成數(shù)據(jù)庫特定的客戶端API。這種驅(qū)動包含特定數(shù)據(jù)庫的本地代碼,用于訪問特定數(shù)據(jù)庫的客戶端
支持三層結(jié)構(gòu)的JDBC訪問方式,主要用于Applet階段,通過Applet訪問數(shù)據(jù)庫
純java的,直接與數(shù)據(jù)庫實例交互,。這種驅(qū)動是智能型的,它知道數(shù)據(jù)庫使用的底層協(xié)議,是目前最流行的JDBC驅(qū)動
通常建議選擇第4種JDBC驅(qū)動,這種驅(qū)動避開了本地代碼,減少了應(yīng)用開發(fā)的復(fù)雜性,也減少了產(chǎn)生沖突和出錯的可能。如果對性能有嚴(yán)格的要求,則可以考慮使用第2種JDBC驅(qū)動,但使用這種驅(qū)動,則勢必增加編碼和維護的困難
JDBC比ODBC多了如下幾個優(yōu)勢
ODBC更復(fù)雜,ODBC中有幾個命令需要配置很多復(fù)雜的選項,而JDBC則采用簡單、直觀的方式來管理數(shù)據(jù)庫連接
JDBC比ODBC安全性更高,更易部署
JDBC的經(jīng)典用法 JDBC 4.2常用接口和類簡介JAVA8關(guān)于JDBC4.2的新增功能:
DriverManager:用于管理JDBC驅(qū)動的服務(wù)類。程序中使用該類的主要功能是獲取Connection對象,該類包含如下方法
public static synchronized Connection getConnection(String url, String user, String password) throws SQLException:該方法獲得url對應(yīng)數(shù)據(jù)庫的連接
ConnectionConnection:代表數(shù)據(jù)庫連接對象,每個Connection代表一個物理連接會話。要想訪問數(shù)據(jù)庫,必須先得到數(shù)據(jù)庫連接。該接口的常用方法如下:
Statement createStatement() throws SQLException:該方法返回一個Statement對象
PreparedStatement prepareStatement(String sql) throws SQLException:該方法返回預(yù)編譯的Statement對象,即將SQL語句提交到數(shù)據(jù)庫進行預(yù)編譯
CallableStatement prepareCall(String sql) throws SQLException:該方法返回CallableStatement對象,該對象用于調(diào)用存儲過程
上面三個方法都返回用于執(zhí)行SQL語句的Statement對象,PreparedStatement、CallableStatement是Statement的子類,只有獲得了Statement之后才可以執(zhí)行SQL語句
除此之外,Connection還有如下幾個用于控制事務(wù)的方法:
Savepoint setSavepoint() throws SQLException:創(chuàng)建一個保存點
Savepoint setSavepoint(String name):以指定名字來創(chuàng)建一個保存點
void setTransactionIsolation(int level):設(shè)置事務(wù)的隔離級別
void rollback():回滾事務(wù)
void rollback(Savepoint savepoint):將事務(wù)回滾到指定的保存點
void setAutoCommit(boolean autoCommit):關(guān)閉自動提交,打開事務(wù)
void commit() throws SQLException:提交事務(wù)
Java7位Connection新增了setSchema(String schema)、getSchema()兩個方法,這兩個方法用于控制該Connection訪問的數(shù)據(jù)庫Schema。還為Connection新增了setNetworkTimeout(Executor executor, int milliseconds)、getNetworkTimeout()兩個方法來控制數(shù)據(jù)庫連接的超時行為
StatementStatement:用于執(zhí)行SQL語句的工具接口。該對象既可以執(zhí)行DDL、DCL語句,也可以用于執(zhí)行DML語句,還可以用于執(zhí)行SQL查詢。當(dāng)執(zhí)行SQL查詢時,返回查詢到的結(jié)果集。它的常用方法如下:
ResultSet executeQuery(String sql) throws SQLException:該方法用于執(zhí)行查詢語句,并返回查詢結(jié)果對應(yīng)ResultSet對象。該方法只能用于執(zhí)行查詢語句
int executeUpdate(String sql) throws SQLException:該方法用于執(zhí)行DML語句,并返回受影響的行數(shù);該方法也可用于執(zhí)行DDL語句,執(zhí)行DDL語句將返回0
boolean execute(String sql) throws SQLException:該方法可以執(zhí)行任何SQL語句。如果執(zhí)行后第一個結(jié)果為ResultSet對象,則返回true;如果執(zhí)行后第一個結(jié)果為受影響的行數(shù)或沒有任何結(jié)果,則返回false
Java7為Statement新增了closeOnCompletion()方法,如果Statement執(zhí)行了此方法,則當(dāng)所有依賴于該Statement的ResultSet關(guān)閉時,該Statement會自動關(guān)閉。Java7還為Statement提供了一個isCloseOnCompletion()方法,該方法用于判斷該Statement是否打開了“closeOnCompletion”
PreparedStatementPreparedStatement:預(yù)編譯的Statement對象,PreparedStatement是Statement的子接口,它允許數(shù)據(jù)庫預(yù)編譯SQL語句(這些SQL語句通常帶有參數(shù)),以后每次只改變sql命令的參數(shù),避免數(shù)據(jù)庫每次都需要編譯SQL語句,無需再傳入SQL語句,因此性能更好。使用PreparedStatement執(zhí)行SQL語句時,無須再傳入SQL語句,只要為預(yù)編譯的SQL語句傳入?yún)?shù)值即可
PreparedStatement同樣有executeQuery()、executeUpdate()和execute()方法,只是這三個方法無須接收SQL字符串,因為PreparedStatement對象已預(yù)編譯了SQL命令,只要為這些方法傳入?yún)?shù)即可。所以它比Statement多了如下方法:
void setXxx(int parameterIndex, Xxx value):該方法根據(jù)傳入?yún)?shù)值的類型不同,需要使用不同的方法。傳入的值根據(jù)索引傳給SQL語句中指定位置的參數(shù)
ResultSetResultSet:結(jié)果集對象。該對象包含訪問查詢結(jié)果的方法,ResultSet可以通過列索引或列名獲得列數(shù)據(jù)。它包含了如下常用方法來移動記錄指針
void close():釋放ResultSet對象
boolean absolute(int row):將結(jié)果集的記錄指針移動到第row行,如果row是負數(shù),則移動到倒數(shù)第row行,如果移動后的記錄指針指向一條有效記錄,則該方法返回true
void beforeFisrt():將ResultSet的記錄指針定位到首行之前,這是ResultSet結(jié)果集記錄指針的初始狀態(tài)——記錄指針的起始位置位于第一行之前。
boolean first():將ResultSet的記錄指針定位到首行。如果移動后的記錄指針指向一條有效記錄,則該方法返回true
boolean previous():將ResultSet的記錄指針定位到上一行,如果移動后的記錄指針指向一條有效記錄,則該方法返回true
boolean next():將結(jié)果集的記錄指針定位到下一行,如果移動后的記錄指針指向一條有效的記錄,則該方法返回true
boolean last():將結(jié)果集的記錄指針定位到最后一行,如果移動后的記錄指針指向一條有效的記錄,則該方法返回true
void afterLast():將ResultSet的記錄指針定位到最后一行之后
當(dāng)把記錄指針移動到指定行之后,ResultSet可通過getXxx(int columnIndex)或getXxx(String columnLabel)方法來獲取當(dāng)前行、指定列的值,前者根據(jù)列索引獲取值,后者根據(jù)列名獲取值
JDBC編程步驟 1 加載數(shù)據(jù)庫驅(qū)動通常使用Class類的forName()靜態(tài)方法來加載驅(qū)動
// 加載驅(qū)動,driverClass就是數(shù)據(jù)庫驅(qū)動類所對應(yīng)的字符串 Class.forName(driverClass); // 加載MySQL的驅(qū)動 Class.forName("com.mysql.jdbc.Driver"); // 加載Oracle的驅(qū)動 Class.forName("oracle.jabc.driver.OracleDriver");2 通過DriverManager獲取數(shù)據(jù)庫的鏈接
// 獲取數(shù)據(jù)庫連接 DriverManager.getConnection(String url, Stirng user, String pass)
當(dāng)使用DriverManager來獲取鏈接,通常需要傳入三個參數(shù):數(shù)據(jù)庫URL、登錄數(shù)據(jù)庫的用戶名和密碼
數(shù)據(jù)庫URL通常遵循如下寫法:jdbc是固定的;subprotocol指定連接到特定數(shù)據(jù)庫的驅(qū)動;other和stuff也是不固定的
jdbc:subprotocol:other stuff3 通過Connection對象創(chuàng)建Statement(或者PreparedStatement)對象
createStatement():創(chuàng)建基本的Statement對象
prepareStatement(String sql):根據(jù)傳入的SQL語句創(chuàng)建預(yù)編譯的Statement對象
prepareCall(String sql):根據(jù)傳入的SQL語句創(chuàng)建CallableStatement對象
4 使用Statement執(zhí)行SQL語句execute():可以執(zhí)行任何SQL語句,但比較麻煩
executeUpdate():主要用于執(zhí)行DML和DDL語句。執(zhí)行DML返回受影響的SQL語句行數(shù),執(zhí)行DDL返回0
executeQuery():只能執(zhí)行查詢語句,執(zhí)行后返回代表查詢結(jié)果的ResultSet對象
5 操作結(jié)果集如果執(zhí)行的SQL語句是查詢語句,則執(zhí)行結(jié)果將返回一個ResultSet對象,該對象里保存了SQL語句查詢的結(jié)果。程序可以通過操作該ResultSet對象來取出查詢結(jié)果。ResultSet對象主要提供了如下兩類方法
next()、previous()、first()、last()、beforeFrist()、afterLast()、absolute()等移動指針的方法
getXxx()方法獲取記錄指針指向行,特定列的值。既可使用列名作為參數(shù)可讀性更好、使用索引作為參數(shù)性能更好
6 回收數(shù)據(jù)庫資源包括關(guān)閉ResultSet、Statement和Connection等資源
import java.sql.*; public class ConnMySql { public static void main(String[] args) throws Exception { // 1.加載驅(qū)動,使用反射的知識,現(xiàn)在記住這么寫。 Class.forName("com.mysql.jdbc.Driver"); try( // 2.使用DriverManager獲取數(shù)據(jù)庫連接, // 其中返回的Connection就代表了Java程序和數(shù)據(jù)庫的連接 // 不同數(shù)據(jù)庫的URL寫法需要查驅(qū)動文檔知道,用戶名、密碼由DBA分配 Connection conn = DriverManager.getConnection( "jdbc:mysql://127.0.0.1:3306/select_test" , "root" , "32147"); // 3.使用Connection來創(chuàng)建一個Statment對象 Statement stmt = conn.createStatement(); // 4.執(zhí)行SQL語句 /* Statement有三種執(zhí)行sql語句的方法: 1 execute 可執(zhí)行任何SQL語句。- 返回一個boolean值, 如果執(zhí)行后第一個結(jié)果是ResultSet,則返回true,否則返回false 2 executeQuery 執(zhí)行Select語句 - 返回查詢到的結(jié)果集 3 executeUpdate 用于執(zhí)行DML語句。- 返回一個整數(shù), 代表被SQL語句影響的記錄條數(shù) */ ResultSet rs = stmt.executeQuery("select s.* , teacher_name" + " from student_table s , teacher_table t" + " where t.teacher_id = s.java_teacher")) { // ResultSet有系列的getXxx(列索引 | 列名),用于獲取記錄指針 // 指向行、特定列的值,不斷地使用next()將記錄指針下移一行, // 如果移動之后記錄指針依然指向有效行,則next()方法返回true。 while(rs.next()) { System.out.println(rs.getInt(1) + " " + rs.getString(2) + " " + rs.getString(3) + " " + rs.getString(4)); } } } }執(zhí)行SQL語句的方式 使用Java8新增的executeLargeUpdate方法執(zhí)行DDL和DML語句
以下程序示范了使用executeUpdate()方法(MySQL驅(qū)動暫不支持executeLargeUpdate()方法)創(chuàng)建數(shù)據(jù)表。該示例并沒有直接把數(shù)據(jù)庫連接信息寫在程序里,而是使用一個mysql.ini文件(properties文件)來保存數(shù)據(jù)庫連接信息,這是比較成熟的做法——當(dāng)需要把應(yīng)用程序從開發(fā)環(huán)境移植到生產(chǎn)環(huán)境時,無須修改源代碼,只需修改mysql.ini配置文件即可
import java.util.*; import java.io.*; import java.sql.*; public class ExecuteDDL { private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public void createTable(String sql)throws Exception { // 加載驅(qū)動 Class.forName(driver); try( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); // 使用Connection來創(chuàng)建一個Statment對象 Statement stmt = conn.createStatement()) { // 執(zhí)行DDL,創(chuàng)建數(shù)據(jù)表 stmt.executeUpdate(sql); } } public static void main(String[] args) throws Exception { ExecuteDDL ed = new ExecuteDDL(); ed.initParam("mysql.ini"); ed.createTable("create table jdbc_test " + "( jdbc_id int auto_increment primary key, " + "jdbc_name varchar(255), " + "jdbc_desc text);"); System.out.println("-----建表成功-----"); } }
下面程序執(zhí)行一條insert語句,這條insert語句會向剛剛建立的jdbc_test數(shù)據(jù)表中插入幾條記錄。因為使用了帶子查詢的insert語句,所以可以一次插入多條語句
import java.util.*; import java.io.*; import java.sql.*; public class ExecuteDML { private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public int insertData(String sql)throws Exception { // 加載驅(qū)動 Class.forName(driver); try( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); // 使用Connection來創(chuàng)建一個Statment對象 Statement stmt = conn.createStatement()) { // 執(zhí)行DML,返回受影響的記錄條數(shù) return stmt.executeUpdate(sql); } } public static void main(String[] args)throws Exception { ExecuteDML ed = new ExecuteDML(); ed.initParam("mysql.ini"); int result = ed.insertData("insert into jdbc_test(jdbc_name,jdbc_desc)" + "select s.student_name , t.teacher_name " + "from student_table s , teacher_table t " + "where s.java_teacher = t.teacher_id;"); System.out.println("--系統(tǒng)中共有" + result + "條記錄受影響--"); } }使用execute方法執(zhí)行SQL語句
Statement的execute()方法幾乎可以執(zhí)行任何SQL語句,但它執(zhí)行SQL語句時比較麻煩,通常沒有必要使用execute()方法來執(zhí)行SQL語句,使用executeQuery()或executeUpdate()方法更簡單。但如果不清楚SQL語句的類型,則只能使用execute()方法來執(zhí)行該SQL語句
getResult():獲取該Statement執(zhí)行查詢語句所返回的ResultSet對象
getUpdateCount():獲取該Statement()執(zhí)行DML語句所影響的記錄行數(shù)
import java.util.*; import java.io.*; import java.sql.*; public class ExecuteSQL { private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public void executeSql(String sql)throws Exception { // 加載驅(qū)動 Class.forName(driver); try( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); // 使用Connection來創(chuàng)建一個Statement對象 Statement stmt = conn.createStatement() ) { // 執(zhí)行SQL,返回boolean值表示是否包含ResultSet boolean hasResultSet = stmt.execute(sql); // 如果執(zhí)行后有ResultSet結(jié)果集 if (hasResultSet) { try( // 獲取結(jié)果集 ResultSet rs = stmt.getResultSet() ) { // ResultSetMetaData是用于分析結(jié)果集的元數(shù)據(jù)接口 ResultSetMetaData rsmd = rs.getMetaData(); int columnCount = rsmd.getColumnCount(); // 迭代輸出ResultSet對象 while (rs.next()) { // 依次輸出每列的值 for (int i = 0 ; i < columnCount ; i++ ) { System.out.print(rs.getString(i + 1) + " "); } System.out.print(" "); } } } 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("------執(zhí)行刪除表的DDL語句-----"); es.executeSql("drop table if exists my_test"); System.out.println("------執(zhí)行建表的DDL語句-----"); es.executeSql("create table my_test" + "(test_id int auto_increment primary key, " + "test_name varchar(255))"); System.out.println("------執(zhí)行插入數(shù)據(jù)的DML語句-----"); es.executeSql("insert into my_test(test_name) " + "select student_name from student_table"); System.out.println("------執(zhí)行查詢數(shù)據(jù)的查詢語句-----"); es.executeSql("select * from my_test"); } }使用PreparedStatement執(zhí)行SQL語句
創(chuàng)建PreparedStatement對象使用Connection的preparedStatement()方法,該方法需要傳入一個SQL字符串,該字符串可以包含符參數(shù)
// 創(chuàng)建一個PreparedStatement對象 pstmt = conn.preparedStatement("insert into student_table values(null,?,1)");
PreparedStatement也提供了execute()、executeUpdate()、executeQuery()三個方法來執(zhí)行SQL語句,不過這三個方法無須參數(shù),因為PreparedStatement提供了一系列的setXxx(int index, Xxx value)方法來傳入?yún)?shù)值
如果程序很清楚PreparedStatement預(yù)編譯SQL語句中各參數(shù)的類型,則使用相應(yīng)的setXxx()方法來傳入?yún)?shù)即可;如果程序不清楚編譯SQL語句中各參數(shù)的類型,則可以使用setObject()方法來傳入?yún)?shù),由PreparedStatement來負責(zé)類型轉(zhuǎn)換
下面程序示范使用Statement和PreparedStatement分別插入100條記錄的對比。使用Statement需要傳入100條SQL語句,但使用PreparedStatement則只需傳入1條預(yù)編譯的SQL語句,然后100次為該PreparedStatement的參數(shù)設(shè)值即可
import java.util.*; import java.io.*; import java.sql.*; public class PreparedStatementTest { private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); // 加載驅(qū)動 Class.forName(driver); } public void insertUseStatement()throws Exception { long start = System.currentTimeMillis(); try( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); // 使用Connection來創(chuàng)建一個Statment對象 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( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); // 使用Connection來創(chuàng)建一個PreparedStatement對象 PreparedStatement pstmt = conn.prepareStatement( "insert into student_table values(null,?,1)")) { // 100次為PreparedStatement的參數(shù)設(shè)值,就可以插入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(); } }
SQL注入是一個較常見的Cracker入侵方式,它利用SQL語句的漏洞來入侵。以下程序以一個簡單的登錄窗口為例來介紹這種SQL注入的結(jié)果。下面登錄窗口包含兩個文本框,一個用于輸入用戶名,一個用于輸入密碼,系統(tǒng)根據(jù)用戶輸入與jdbc_test表里的記錄進行匹配,如果找到相應(yīng)記錄則提示登陸成功
public class LoginFrame { private final String PROP_FILE = "mysql.ini"; private String driver; // url是數(shù)據(jù)庫的服務(wù)地址 private String url; private String user; private String pass; // 登錄界面的GUI組件 private JFrame jf = new JFrame("登錄"); private JTextField userField = new JTextField(20); private JTextField passField = new JTextField(20); private JButton loginButton = new JButton("登錄"); public void init()throws Exception { Properties connProp = new Properties(); connProp.load(new FileInputStream(PROP_FILE)); driver = connProp.getProperty("driver"); url = connProp.getProperty("url"); user = connProp.getProperty("user"); pass = connProp.getProperty("pass"); // 加載驅(qū)動 Class.forName(driver); // 為登錄按鈕添加事件監(jiān)聽器 loginButton.addActionListener(e -> { // 登錄成功則顯示“登錄成功” if (validate(userField.getText(), passField.getText())) { JOptionPane.showMessageDialog(jf, "登錄成功"); } // 否則顯示“登錄失敗” else { JOptionPane.showMessageDialog(jf, "登錄失敗"); } }); jf.add(userField , BorderLayout.NORTH); jf.add(passField); jf.add(loginButton , BorderLayout.SOUTH); jf.pack(); jf.setVisible(true); } private boolean validate(String userName, String userPass) { // 執(zhí)行查詢的SQL語句 String sql = "select * from jdbc_test " + "where jdbc_name="" + userName + "" and jdbc_desc="" + userPass + """; System.out.println(sql); try( Connection conn = DriverManager.getConnection(url , user ,pass); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { // 如果查詢的ResultSet里有超過一條的記錄,則登錄成功 if (rs.next()) { return true; } } catch(Exception e) { e.printStackTrace(); } return false; } public static void main(String[] args) throws Exception { new LoginFrame().init(); } }
如果用戶正常輸入其用戶名、密碼,輸入正確時可以正常登陸,輸入錯誤將提示輸入失敗。但如果這個用戶是一個Cracker,可以輸入"or true or",也會顯示登陸成功。運行的后臺可以看到如下SQL語句
# 利用SQL注入后生成的SQL語句 select * from jdbc_test where jdbc_name = "" or true or "" and jdbc_desc = ""
如果換成使用PreparedStatement來執(zhí)行驗證,而不是直接使用Statement
private boolean validate(String userName, String userPass) { try( Connection conn = DriverManager.getConnection(url , user ,pass); PreparedStatement pstmt = conn.prepareStatement( "select * from jdbc_test where jdbc_name=? and jdbc_desc=?")) { pstmt.setString(1, userName); pstmt.setString(2, userPass); try( ResultSet rs = pstmt.executeQuery()) { //如果查詢的ResultSet里有超過一條的記錄,則登錄成功 if (rs.next()) { return true; } } } catch(Exception e) { e.printStackTrace(); } return false; }
PreparedStatement預(yù)編譯SQL語句,性能更好
PreparedStatement無須“拼接”SQL字符串,編程更簡單
使用PreparedStatement可防止SQL注入,安全性更好
使用PreparedStatement執(zhí)行帶占位符參數(shù)的SQL語句時,SQL語句中的占位符參數(shù)只能代替普通值,不要使用占位符參數(shù)代替表名、列名等數(shù)據(jù)庫對象,更不要用占位符參數(shù)來代替SQL語句中的insert、select等關(guān)鍵字
使用CallableStatement調(diào)用存儲過程MySQL數(shù)據(jù)庫中創(chuàng)建一個簡單的存儲過程的SQL語句
delimiter // create procedure add_pro(a int, b int ,out sum int) begin set sum = a + b; end; //
上面的SQL語句將MySQL的語句結(jié)束符改為雙斜線(//),這樣就可以在創(chuàng)建存儲過程中使用分號作為分隔符(默認使用分號作為語句結(jié)束符)程序創(chuàng)建了名為add_pro的存儲過程,該存儲過程包含三個參數(shù):a、b是傳入?yún)?shù),而sum使用out修飾,是傳出參數(shù)
調(diào)用存儲過程使用CallableStatement,通過Connection的prepareCall()方法來創(chuàng)建CallableStatement對象,創(chuàng)建該對象時需要傳入調(diào)用存儲過程的SQL語句。調(diào)用存儲過程的SQL語句總是這種格式:{call 過程名(?,?,?...)},其中的問號作為存儲過程參數(shù)的占位符
// 使用Connection來創(chuàng)建一個CallableStatement對象 cstmt = conn.prepareCall("{call add_pro(?,?,?)}");
存儲過程的參數(shù)既有傳入?yún)?shù),也有傳出參數(shù),所謂傳入?yún)?shù)就是Java程序必須為這些參數(shù)傳入值,可以通過CallableStatement的setXxx()方法為傳入?yún)?shù)設(shè)置值;所謂傳出參數(shù)就是Java程序可以通過該參數(shù)獲取存儲過程里的值,CallableStatement需要調(diào)用registerOutParameter()方法來注冊該參數(shù)
// 注冊CallableStatement的第三個參數(shù)是int類型 cstmt.registerOutParameter(3, Types.INTEGER);
之后調(diào)用CallableStatement的execute()方法來執(zhí)行存儲過程,執(zhí)行結(jié)束后通過CallableStatement對象的getXxx(int index)方法來獲取指定傳出參數(shù)的值
import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.*; import java.io.*; import java.sql.*; public class CallableStatementTest { private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public void callProcedure()throws Exception { // 加載驅(qū)動 Class.forName(driver); try( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); // 使用Connection來創(chuàng)建一個CallableStatment對象 CallableStatement cstmt = conn.prepareCall( "{call add_pro(?,?,?)}")) { cstmt.setInt(1, 4); cstmt.setInt(2, 5); // 注冊CallableStatement的第三個參數(shù)是int類型 cstmt.registerOutParameter(3, Types.INTEGER); // 執(zhí)行存儲過程 cstmt.execute(); // 獲取,并輸出存儲過程傳出參數(shù)的值。 System.out.println("執(zhí)行結(jié)果是: " + cstmt.getInt(3)); } } public static void main(String[] args) throws Exception { CallableStatementTest ct = new CallableStatementTest(); ct.initParam("mysql.ini"); ct.callProcedure(); } }管理結(jié)果集
JDBC使用ResultSet來封裝執(zhí)行查詢得到的查詢結(jié)果,然后通過移動ResultSet的記錄指針來取出結(jié)果集的內(nèi)容。除此之外,JDBC還允許通過ResultSet來更新記錄,并提供了ResultSetMetaData來獲得ResultSet對象的相關(guān)信息
可滾動、可更新的結(jié)果集可滾動的結(jié)果集:可以使用absolute()、previous()、afterLast()等方法只有移動指針記錄的ResultSet
以默認形式打開的ResultSet是不可更新的,如果希望創(chuàng)建可更新的ResultSet,則必須在Connection在創(chuàng)建Statement或PreparedStatement時,傳入額外的參數(shù):
resultSetType:控制ResultSet的類型,該參數(shù)可以取如下三個值
ResultSet.TYPE_FORWARD_ONLY:該常量控制記錄指針只能向前移動
ResultSet.TYPE_SCROLL_INSENSITIVE:該常量控制記錄指針自由移動(可滾動結(jié)果集),但底層的數(shù)據(jù)改變不影響結(jié)果集ResultSet的內(nèi)容
ResultSet.TYPE_SCROLL_SENSITIVE:該常量控制記錄指針自由移動,但底層數(shù)據(jù)的影響會改變結(jié)果集ResultSet的內(nèi)容
resultSetConcurrency:控制ResultSet的并發(fā)類型,該參數(shù)可以接收如下兩個值
ResultSet.CONCUR_READ_ONLY:該常量表示ResultSet是只讀并發(fā)模式(默認)
ResultSet.CONCUR_UPDATABLE:該常量表示ResultSet是更新并發(fā)模式
// 使用Connection創(chuàng)建一個PreparedStatement對象 // 傳入控制結(jié)果集可滾動、可更新的參數(shù) pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
可更新的結(jié)果集還需要滿足如下兩個條件:
所有數(shù)據(jù)都應(yīng)該來自一個表
選出的數(shù)據(jù)集必須包含主列鍵
通過該PreparedStatement創(chuàng)建的ResultSet就是可滾動的、可更新的,程序可調(diào)用的updateXxx(int columnIndex, Xxx value)方法來修改記錄指針?biāo)赣涗?、特定列的值,最后調(diào)用ResultSet的updateRow()方法來提交修改
Java8為ResultSet添加了updateObject(String columnLabel, Object x, SQLType targetSqlType)和updateObject(int columnIndex, Object x, SQLType targetSqlType)兩個默認方法,這兩個方法可以直接用Object來修改記錄指針?biāo)赣涗洝⑻囟械闹?,其中SQLType用于指定該數(shù)據(jù)列的類型
import java.util.*; import java.io.*; import java.sql.*; public class ResultSetTest { private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public void query(String sql)throws Exception { // 加載驅(qū)動 Class.forName(driver); try( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); // 使用Connection來創(chuàng)建一個PreparedStatement對象 // 傳入控制結(jié)果集可滾動,可更新的參數(shù)。 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) + " " + rs.getString(2) + " " + rs.getString(3)); // 修改記錄指針?biāo)杏涗?、?列的值 rs.updateString(2 , "學(xué)生名" + 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"); } }處理Blob類型數(shù)據(jù)
Blob——Binary long Object——二進制長對象,Blob列通常用于存儲大文件,典型的Blob內(nèi)容是一張圖片或者一個聲音文件,由于他們的特殊性,必須使用特殊的方式來存儲。使用Blob列可以把照片聲音等文件的二進制數(shù)據(jù)保存在數(shù)據(jù)庫里,并可以從數(shù)據(jù)庫里恢復(fù)指定文件
如果需要將圖片插入數(shù)據(jù)庫,顯然不能通過普通的SQL語句來完成,因為有一個關(guān)鍵的問題,Blob常量無法表示,所以將Blob數(shù)據(jù)插入數(shù)據(jù)庫需要使用PreparedStatement。該對象有一個方法:setBinaryStream(int parameterIndex, InputStream x)該方法可以為指定參數(shù)傳入二進制流,從而可以實現(xiàn)將Blob數(shù)據(jù)保存到數(shù)據(jù)庫的功能
當(dāng)需要從ResultSet里取出Blob數(shù)據(jù)時,可以調(diào)用ResultSet的getBlob(int columnIndex)方法,該方法將返回一個Blob對象Blob對象提供了getBinaryStream()方法獲取該獲取該Blob數(shù)據(jù)的輸入流,也可以使用Blob對象的getBytes()方法直接取出該Blob對象封裝的二進制數(shù)據(jù)
為了把圖片放入數(shù)據(jù)庫,本程序先使用如下SQL語句來建立一個數(shù)據(jù)表:
create table img_table { img_id int auto_increment primary key, img_name varchar(255), # 創(chuàng)建一個mediumblob類型的數(shù)據(jù)列,用于保存圖片數(shù)據(jù) ima_data mediumblob };
img_data列使用mediumblob類型,而不是blob類型。因為MySQL數(shù)據(jù)庫里的blob類型最多只能存儲64kb的內(nèi)容,所以使用mediumblob類型,該類型可以存儲16M內(nèi)容
// ---------將指定圖片放入數(shù)據(jù)庫--------- public void upload(String fileName) { // 截取文件名 String imageName = fileName.substring(fileName.lastIndexOf("") + 1, fileName.lastIndexOf(".")); File f = new File(fileName); try( InputStream is = new FileInputStream(f) ) { // 設(shè)置圖片名參數(shù) insert.setString(1, imageName); // 設(shè)置二進制流參數(shù) insert.setBinaryStream(2, is, (int)f.length()); int affect = insert.executeUpdate(); if (affect == 1) { // 重新更新ListModel,將會讓JList顯示最新的圖片列表 fillListModel(); } } catch (Exception e) { e.printStackTrace(); } } // ---------根據(jù)圖片ID來顯示圖片---------- public void showImage(int id)throws SQLException { // 設(shè)置參數(shù) query.setInt(1, id); try( // 執(zhí)行查詢 ResultSet rs = query.executeQuery() ) { if (rs.next()) { // 取出Blob列 Blob imgBlob = rs.getBlob(1); // 取出Blob列里的數(shù)據(jù) ImageIcon icon=new ImageIcon(imgBlob.getBytes(1L, (int)imgBlob.length())); imageLabel.setIcon(icon); } } }使用resultsetmetaData分析結(jié)果集
描述ResultSet信息的數(shù)據(jù)——ResultSetMetaData
MetaData即元數(shù)據(jù),即描述其它數(shù)據(jù)的數(shù)據(jù),因此ResultSetMetaData封裝了描述ResultSet對象的數(shù)據(jù)
ResultSet的getMetaData()方法返回該ResultSet對應(yīng)的ResultSetMetaData對象,就可通過ResultSetMetaData提供的大量方法返回ResultSet的描述信息
int getColumnCount():返回該ResultSet的列數(shù)量
String getColumnName(int column):返回指定索引的列名
int getColumnType(int column):返回指定索引的列類型
Java7的RowSet1.1RowSet接口繼承了ResultSet接口,RowSet接口下包含JdbcRowSet、CachedRowSet、FilteredRowSet、JoinRowSet和WebRowSet常用子接口。除了JdbcRowSet需要保持與數(shù)據(jù)庫的連接之外,其余4個子接口都是離線的RowSet,無須保持與數(shù)據(jù)庫的連接
RowSet默認是一個可滾動,可更新,可序列化的結(jié)果集,而且它作為JavaBeans,可以方便地在網(wǎng)絡(luò)間傳輸,用于兩端的數(shù)據(jù)同步。對于離線RowSet而言,程序在創(chuàng)建RowSet時已把數(shù)據(jù)從底層數(shù)據(jù)庫讀取到了內(nèi)存,因此可以充分利用計算機的內(nèi)存,從而降低數(shù)據(jù)庫服務(wù)器的負載,提供程序性能
RowSet規(guī)范的接口類圖
RowSet接口中定義的常用方法:
setUrl(String url):設(shè)置該RowSet要訪問的數(shù)據(jù)庫的URL
setUsername(String name):設(shè)置該RowSet要訪問的數(shù)據(jù)庫的用戶名
setPassword(String password):設(shè)置該RowSet要訪問的數(shù)據(jù)庫的密碼
setCommand(String sql):設(shè)置使用該sql語句的查詢結(jié)果來裝填該RowSet
execute():執(zhí)行查詢
populate(ResultSet rs):讓該RowSet直接包裝給定的ResultSet對象
Java7新增了RowSetProvider類和RowSetFactory接口,其中RowSetProvider負載創(chuàng)建RowSetFactory,而RowSetFactory則提供了如下方法來創(chuàng)建RowSet實例:
CachedRowSet createCachedRowSet():創(chuàng)建一個默認的CachedRowSet
FilteredRowSet createFilteredRowSet():創(chuàng)建一個默認的FilteredRowSet
JoinRowSet createJoinRowSet():創(chuàng)建一個默認的JoinRowSet
WebRowSet createWebRowSet():創(chuàng)建一個默認的WebRowSet
JdbcRowSet createJdbcRowSet():創(chuàng)建一個默認的JdbcRowSet
提供使用RowSetFactory,就可以把應(yīng)用程序與RowSet實現(xiàn)類分離開,避免直接使用JdbcRow SetImpl等非公開的API,也更有利于后期的升級、擴展
import java.util.*; import java.io.*; import java.sql.*; import javax.sql.rowset.*; public class RowSetFactoryTest { private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public void update(String sql)throws Exception { // 加載驅(qū)動 Class.forName(driver); // 使用RowSetProvider創(chuàng)建RowSetFactory RowSetFactory factory = RowSetProvider.newFactory(); try( // 使用RowSetFactory創(chuàng)建默認的JdbcRowSet實例 JdbcRowSet jdbcRs = factory.createJdbcRowSet() ) { // 設(shè)置必要的連接信息 jdbcRs.setUrl(url); jdbcRs.setUsername(user); jdbcRs.setPassword(pass); // 設(shè)置SQL查詢語句 jdbcRs.setCommand(sql); // 執(zhí)行查詢 jdbcRs.execute(); jdbcRs.afterLast(); // 向前滾動結(jié)果集 while (jdbcRs.previous()) { System.out.println(jdbcRs.getString(1) + " " + jdbcRs.getString(2) + " " + jdbcRs.getString(3)); if (jdbcRs.getInt("student_id") == 3) { // 修改指定記錄行 jdbcRs.updateString("student_name", "源博雅"); jdbcRs.updateRow(); } } } } public static void main(String[] args)throws Exception { RowSetFactoryTest jt = new RowSetFactoryTest(); jt.initParam("mysql.ini"); jt.update("select * from student_table"); } }離線RowSet
離線RowSet會直接將底層數(shù)據(jù)讀入內(nèi)存中,封裝成RowSet對象,而RowSet對象則完全可以當(dāng)成Java Bean來使用。因此不僅安全,而且編程簡單。CachedRowSet是所有離線RowSet的父接口
如下程序①處調(diào)用了RowSet的populate(ResultSet rs)方法來包裝給的的ResultSet,接著關(guān)閉了ResultSet、Statement、Connection等數(shù)據(jù)庫資源。如果程序直接返回ResultSet,那么這個Result無法使用——因為底層的Connection已經(jīng)關(guān)閉;但程序返回的是CachedRowSet,一個離線RowSet,因此程序依然可以讀取、修改RowSet中的記錄
為了將程序?qū)﹄x線RowSet所做的修改同步到底層數(shù)據(jù)庫,程序在調(diào)用RowSet的acceptChanges()方法時必須傳入Connection
public class CachedRowSetTest { private static String driver; private static String url; private static String user; private static String pass; 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"); pass = props.getProperty("pass"); } public CachedRowSet query(String sql) throws Exception { // 加載驅(qū)動 Class.forName(driver); // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url, user, pass); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql); // 使用RowSetProvider創(chuàng)建RowSetFactory RowSetFactory factory = RowSetProvider.newFactory(); // 創(chuàng)建默認的CachedRowSet實例 CachedRowSet cachedRs = factory.createCachedRowSet(); // 使用ResultSet裝填RowSet cachedRs.populate(rs); // ① // 關(guān)閉資源 rs.close(); stmt.close(); conn.close(); return cachedRs; } public static void main(String[] args)throws Exception { CachedRowSetTest ct = new CachedRowSetTest(); ct.initParam("mysql.ini"); CachedRowSet rs = ct.query("select * from student_table"); rs.afterLast(); // 向前滾動結(jié)果集 while (rs.previous()) { System.out.println(rs.getString(1) + " " + rs.getString(2) + " " + rs.getString(3)); if (rs.getInt("student_id") == 3) { // 修改指定記錄行 rs.updateString("student_name", "安倍晴明"); rs.updateRow(); } } // 重新獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url, user, pass); conn.setAutoCommit(false); // 把對RowSet所做的修改同步到底層數(shù)據(jù)庫 rs.acceptChanges(conn); } }離線RowSet的查詢分頁
CachedRowSet的分頁功能:一次只裝載ResultSet里的某幾條記錄,這樣就可以避免CachedRowSet占用內(nèi)存過大的問題
CachedRowSet提供了如下方法來控制分頁:
populate(ResultSet rs, int startRow):使用給定的Result裝填RowSet,從ResultSet的第startRow條記錄可是裝填
setPageSize(int pageSize):設(shè)置CachedRowSet每次返回記錄條數(shù)
previousPage():在底層ResultSet可用情況下,讓CachedRowSet讀取上一頁記錄
nextPage():在底層ResultSet可用情況下,讓CachedRowSet讀取下一頁記錄
public class CachedRowSetPage
{
private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public CachedRowSet query(String sql, int pageSize, int page) throws Exception { // 加載驅(qū)動 Class.forName(driver); try( // 獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url , user , pass); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql) ) { // 使用RowSetProvider創(chuàng)建RowSetFactory RowSetFactory factory = RowSetProvider.newFactory(); // 創(chuàng)建默認的CachedRowSet實例 CachedRowSet cachedRs = factory.createCachedRowSet(); // 設(shè)置每頁顯示pageSize條記錄 cachedRs.setPageSize(pageSize); // 使用ResultSet裝填RowSet,設(shè)置從第幾條記錄開始 cachedRs.populate(rs, (page - 1) * pageSize + 1); return cachedRs; } } public static void main(String[] args)throws Exception { CachedRowSetPage cp = new CachedRowSetPage(); cp.initParam("mysql.ini"); CachedRowSet rs = cp.query("select * from student_table", 3, 2); // ① // 向后滾動結(jié)果集 while (rs.next()) { System.out.println(rs.getString(1) + " " + rs.getString(2) + " " + rs.getString(3)); } }
}
事務(wù)處理 事務(wù)的概念和MySQL事務(wù)支持事務(wù)是由一步或幾步數(shù)據(jù)庫操作序列組成的邏輯執(zhí)行單元,這系列操作要么全部執(zhí)行,要么全部放棄執(zhí)行。
事務(wù)具有四個特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持續(xù)性(Durability)。這四個特性也簡稱ACID性
原子性:事務(wù)是應(yīng)用中最小的執(zhí)行單位,就如原子是自然界最小顆粒,具有不可再分的特征一樣。事務(wù)是應(yīng)用中不可再分的最小邏輯執(zhí)行體
一致性:事務(wù)執(zhí)行的結(jié)果,必須使數(shù)據(jù)庫從一個一致性狀態(tài),變到另一個一致性狀態(tài)。當(dāng)數(shù)據(jù)庫中只包含事務(wù)成功提交的結(jié)果時,數(shù)據(jù)庫處于一致性狀態(tài)。一致性是通過原子性來保證的
隔離性:各個事務(wù)的執(zhí)行互不干擾,任意一個事務(wù)的內(nèi)部操作對其他并發(fā)的事務(wù),都是隔離的。也就是說:并發(fā)執(zhí)行的事務(wù)之間不能看到對方的中間狀態(tài),并發(fā)執(zhí)行的事務(wù)之間不能相互影響
持續(xù)性:持續(xù)性也稱為持久性,指事務(wù)一旦提交,對數(shù)據(jù)所做的任何改變,都要記錄到永久存儲器中,通常是保存進物理數(shù)據(jù)庫
數(shù)據(jù)庫的事務(wù)有下列語句組成:
一組DML(Data Manipulate Language,即數(shù)據(jù)操作語言),經(jīng)過這組DML修改后數(shù)據(jù)將保持較好的一致性
一個DDL(Data Definition Language,即數(shù)據(jù)定義語言)語句
一個DCL(Data control Language,即數(shù)據(jù)控制語言)語句
DDL和DCL語句最多只能有一個,因為DDL和DCL語句都會導(dǎo)致事務(wù)立即提交
當(dāng)事務(wù)所包含的全部數(shù)據(jù)庫操作都成功執(zhí)行后,應(yīng)該提交(commit)事務(wù),使這些修改永久生效。事務(wù)提交有兩種方式:顯式提交和自動提交
顯式提交:使用commit
自動提交:執(zhí)行DDL或DCL,或者程序正常退出
當(dāng)事務(wù)所包含的任意一個數(shù)據(jù)庫操作執(zhí)行失敗后,應(yīng)該回滾(rollback)事務(wù),使該事務(wù)中所做的修改全部失效。事務(wù)回滾的方式有兩種:顯式回滾和自動回滾
顯式回滾:使用rollback關(guān)鍵字
隱式回滾:系統(tǒng)錯誤或者強行退出
MySQL默認關(guān)閉事務(wù)(即打開自動提交事務(wù)),在默認情況下,在MySQL控制臺輸入一條DML語句,該語句會立刻保存到數(shù)據(jù)庫中。可以使用下面的語句來開啟事務(wù)(即關(guān)閉自動提交事務(wù)):
// 關(guān)閉自動提交,即開啟事務(wù) set autocommit = 0; // 開啟自動提交,即關(guān)閉事務(wù) set autocommit = 1;
調(diào)用 set autocommit = 0; 命令后,該命令行窗口里的所有DML語句都不會立即生效,上一個事務(wù)結(jié)束后第一條DML語句將開始一個新的事務(wù),而后續(xù)執(zhí)行的所有SQL語句都處于該事務(wù)中。除非使用commit提交事務(wù)、或正常退出、或運行DDL語句或DCL語句導(dǎo)致事務(wù)隱式提交。也可以使用rollback回滾來結(jié)束事務(wù),使用rollback結(jié)束事務(wù)將會使此事務(wù)中的DML語句所做的修改全部失效
一個MySQL命令行窗口代表一個Session,在該窗口里設(shè)置set autocommit = 0; 相當(dāng)于關(guān)閉了該連接Session的自動提交,對其他連接不會有任何影響
如果不想使得整個Session都打開事務(wù),可以使用start transaction或begin這兩個命令,它們都表示臨時性地開始一次事務(wù)。處于start transaction或begin后的DML語句不會立即生效,除非使用commit顯式提交事務(wù),或者使用DDL語句或DCL語句隱式提交事務(wù)
如下SQL將不會對數(shù)據(jù)庫有任何影響
# 臨時開始事務(wù) begin; # 向player_table表插入3條數(shù)據(jù) insert into player_table values(null, "Westbrook", 1); insert into player_table values(null, "Harden", 2); insert into player_table values(null, "Durant", 3); # 查詢player_table表的記錄 select * from player_table; # ① # 回滾事務(wù) rollback; # 再次查詢 select * from player_table; # ②
通過使用savepoint設(shè)置事務(wù)的中間點可以讓事務(wù)回滾到指定中間點,而不是回滾全部事務(wù)。普通的提交、回滾都會結(jié)束當(dāng)前事務(wù),但回滾到指定中間點因為依然處于事務(wù)之中,所以不會結(jié)束當(dāng)前事務(wù)
savepoint a; # 回滾到指定中間點 rollback to a;JDBC的事務(wù)支持
JDBC連接的事務(wù)支持由Connection提供,Connection默認打開自動提交,即關(guān)閉事務(wù),在這種情況下,每條SQL語句一旦執(zhí)行,便會立即提交到數(shù)據(jù)庫,永久生效,無法對其進行回滾操作
可以調(diào)用Connection的setAutoCommit()方法來關(guān)閉自動提交,開啟事務(wù)
//關(guān)閉自動提交,開啟事務(wù) conn.setAutoCommit(false);
一旦事務(wù)開始之后,程序可以像平常一樣創(chuàng)建Statement對象,創(chuàng)建了Statement對象之后,可以執(zhí)行任意多條DML語句,這些SQL語句雖然被執(zhí)行了,但這些SQL語句所作的修改不會生效,因為事務(wù)還沒有結(jié)束。如果所有SQL語句執(zhí)行成功,程序可以調(diào)用Connection的commit方法來提交事務(wù)
//提交事務(wù) conn.commit();
如果任意一條SQL語句執(zhí)行失敗,應(yīng)該用Connection的rollback來回滾事務(wù)
//回滾事務(wù) conn.rollback();
當(dāng)Connection遇到一個未處理的SQLException異常時,系統(tǒng)將會非正常退出,事務(wù)也會自動回滾。但如果程序捕獲了該異常,則需要在異常處理塊中顯式地回滾事務(wù)
Connection設(shè)置中間點的方法:
Savepoint setSavepoint():在當(dāng)前事務(wù)中創(chuàng)建一個未命名的中間點,并返回代表該中間點的Savepoint對象
Savepoint setSavepoint(String name):在當(dāng)前事務(wù)中創(chuàng)建一個具有指定名稱的中間點,并返回代表該中間點的Savepoint對象
通常來說,設(shè)置中間點時沒有太大的必要指定名稱,因為Connection回滾到指定中間點時,并不是根據(jù)名字回滾的,而是根據(jù)中間點對象回滾的。Connection提供了rollback(Savepoint savepoint)方法來回滾到指定中間點
Java8增強的批量更新批量更新必須得到底層數(shù)據(jù)庫的支持,可通過調(diào)用DatabaseMetaData的supportsBatchUpdates()方法來查看底層數(shù)據(jù)庫是否支持批量更新
批量更新需要先創(chuàng)建一個Statement對象,然后利用該對象的addBatch()方法將多條SQL語句同時收集起來,最后調(diào)用Statement對象的executeBatch()(或executeLargeBatch())方法同時執(zhí)行這些SQL語句
批量更新代碼:
Statement stmt = conn.createStatement(); //使用Statement同時收集多個SQL語句 stmt.addBatch(sql1); stmt.addBatch(sql2); stmt.addBatch(sql3); ... //同時執(zhí)行所有的SQL語句 stmt.executeBatch();
為了讓批量操作可以正確地處理錯誤,必須把批量執(zhí)行的操作視為單個事務(wù),如果批量更新在執(zhí)行過程中失敗,則讓事務(wù)回滾到批量操作開始之前的狀態(tài)。程序應(yīng)該在開始批量操作之前先關(guān)閉自動提交,然后開始收集更新語句,當(dāng)批量操作結(jié)束之后,提交事務(wù),并恢復(fù)之前的自動提交模式
//保存當(dāng)前的自動的提交模式 Boolean autoCommit = conn.getAutoCommit(); //關(guān)閉自動提交 conn.setAutoCommit(false); Statement stmt = conn.createStatement(); //使用Statement同時收集多條SQL語句 stmt.addBatch(sql1); stmt.addBatch(sql2); stmt.addBatch(sql3); ... //同時提交所有的SQL語句 stmt.executeBatch(); //提交修改 conn.commit(); //恢復(fù)原有的自動提交模式 conn.setAutoCommit(autoCommit);分析數(shù)據(jù)庫信息 使用DatabaseMetaData分析數(shù)據(jù)庫信息
JDBC提供了DatabaseMetaData來封裝數(shù)據(jù)庫連接對應(yīng)數(shù)據(jù)庫的信息,通過Connection提供的getMetaData()方法就可以獲取數(shù)據(jù)庫對應(yīng)的DatabaseMetaData對象
DatabaseMetaData接口通常由驅(qū)動程序供應(yīng)商提供實現(xiàn),其目的是讓用戶了解底層數(shù)據(jù)庫的相關(guān)信息。使用該接口的目的是發(fā)現(xiàn)如何處理底層數(shù)據(jù)庫,尤其是對于試圖與多個數(shù)據(jù)庫一起使用的應(yīng)用程序
許多DatabaseMetaData方法以ResultSet對象的形式返回查詢信息,然后使用ResultSet的常規(guī)方法(如getString()和getInt())即可從這些ResultSet對象中獲取數(shù)據(jù)。如果查詢的信息不可用,則將返回一個空ResultSet對象
DatabaseMetaData的很多方法都需要傳入一個xxxPattern模式字符串,這里的xxxPattern不是正則表達式,而是SQL里的模式字符串,即用百分號(%)代表任意多個字符,使用下劃線(_)代表一個字符。在通常情況下,如果把該模式字符串的參數(shù)值設(shè)置為null,即表明該參數(shù)不作為過濾條件
import java.sql.*; import java.io.*; import java.util.*; public class DatabaseMetaDataTest{ private String driver; private String url; private String user; private String pass; 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"); pass = props.getProperty("pass"); } public void info() throws Exception{ //加載驅(qū)動 Class.forName(driver); try( //獲取數(shù)據(jù)庫連接 Connection conn = DriverManager.getConnection(url, user, pass); ){ //獲取DatabaseMetaData對象 DatabaseMetaData dbmd = conn.getMetaData(); //獲取MySQL支持的所有表類型 ResultSet rs = dbmd.getTableTypes(); System.out.println("---MySQL支持的表類型信息---"); printResultSet(rs); //獲取當(dāng)前數(shù)據(jù)庫的全部數(shù)據(jù)表 rs = dbmd.getTables(null, null, "%", new String[]{"TABLE"}); System.out.println("---當(dāng)前數(shù)據(jù)庫里的數(shù)據(jù)表信息---"); printResultSet(rs); //獲取student_table表的主鍵 rs = dbmd.getPrimaryKeys(null, null, "student_table"); System.out.println("---student_table表的主鍵信息---"); printResultSet(rs); //獲取當(dāng)前數(shù)據(jù)庫的全部存儲過程 rs = dbmd.getProcedures(null, null, "%"); System.out.println("---當(dāng)前數(shù)據(jù)庫里的存儲過程信息---"); printResultSet(rs); //獲取teacher_table表和student_table表之間的外鍵約束 rs = dbmd.getCrossReference(null, null, "teacher_table", null, null, "student_table"); System.out.println("---teacher_table表和student_table表之間的外鍵約束---"); printResultSet(rs); //獲取student_table表的全部數(shù)據(jù)列 rs = dbmd.getColumns(null, null, "student_table", "%"); System.out.println("---student_table表的全部數(shù)據(jù)列---"); printResultSet(rs); } } public void printResultSet(ResultSet rs) throws SQLException{ ResultSetMetaData rsmd = rs.getMetaData(); //打印ResultSet的所有列標(biāo)題 for(int i = 0; i < rsmd.getColumnCount(); i++){ System.out.print(rsmd.getColumnName(i + 1) + " "); } System.out.print(" "); //打印ResultSet的全部數(shù)據(jù) while(rs.next()){ for(int i = 0; i < rsmd.getColumnCount(); i ++){ System.out.print(rs.getString(i + 1) + " "); } System.out.print(" "); } rs.close(); } public static void main(String args[]) throws Exception{ DatabaseMetaDataTest dmdt = new DatabaseMetaDataTest(); dmdt.initParam("sql.ini"); dmdt.info(); } }使用系統(tǒng)表分析數(shù)據(jù)庫信息
如已確定應(yīng)用程序所使用的數(shù)據(jù)庫系統(tǒng),則可以通過數(shù)據(jù)庫的系統(tǒng)表來分析數(shù)據(jù)庫信息。系統(tǒng)表又稱為數(shù)據(jù)字典,數(shù)據(jù)字典的數(shù)據(jù)通常由數(shù)據(jù)庫系統(tǒng)負責(zé)維護,用戶通常只能查詢數(shù)據(jù)字典,而不能修改數(shù)據(jù)字典的內(nèi)容
MySQL數(shù)據(jù)庫使用information_schema數(shù)據(jù)庫來保存系統(tǒng)表,在該數(shù)據(jù)庫里包含了大量系統(tǒng)表,常用系統(tǒng)表的簡單介紹如下:
tables:存放數(shù)據(jù)庫里所有數(shù)據(jù)表的信息
schemata:存放數(shù)據(jù)庫里所有數(shù)據(jù)庫(與MySQL的Schema對應(yīng))的信息
views:存放數(shù)據(jù)庫里所有視圖的信息
columns:存放數(shù)據(jù)庫里所有列的信息
triggers:存放數(shù)據(jù)庫里所有觸發(fā)器的信息
routines:存放數(shù)據(jù)庫里所有存儲過程和函數(shù)的信息
key_column_usage:存放數(shù)據(jù)庫里所有具有約束的鍵信息
table_constraints:存放數(shù)據(jù)庫里全部約束的表信息
statistics:存放數(shù)據(jù)庫里全部索引的信息
select * from schemata; select * from tables where table_schema = "select_test"; select * from columns where table_name = "student_table";選擇合適的分析方式
通常而言,如果使用DatabaseMetaData來分析數(shù)據(jù)庫信息,則具有更好的跨數(shù)據(jù)庫特性,應(yīng)用程序可以做到數(shù)據(jù)庫無關(guān);但可能無法準(zhǔn)確獲取數(shù)據(jù)庫的更多細節(jié)
使用數(shù)據(jù)庫系統(tǒng)表來分析數(shù)據(jù)庫系統(tǒng)信息會更加準(zhǔn)確,但使用系統(tǒng)表也有壞處——這種方式與底層數(shù)據(jù)庫耦合嚴(yán)重,采用這種方式將會導(dǎo)致程序只能運行在特定的數(shù)據(jù)庫之上
通常來說,如果需要獲得數(shù)據(jù)庫信息,包括該數(shù)據(jù)庫驅(qū)動提供了哪些功能,則應(yīng)該利用DatabaseMetaData來了解該數(shù)據(jù)庫支持哪些功能。完全可能出現(xiàn)這樣一種情況:對于底層數(shù)據(jù)庫支持的功能,但數(shù)據(jù)庫驅(qū)動沒有提供該功能,程序還是不能使用該功能。使用DatabaseMetaData則不會出現(xiàn)這種問題
如果需要純粹地分析數(shù)據(jù)庫的靜態(tài)對象,例如分析數(shù)據(jù)庫系統(tǒng)里包含多少數(shù)據(jù)庫、數(shù)據(jù)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/66469.html
摘要:我們對語句做適當(dāng)改變,就完成了注入,因為普通的不會對做任何處理,該例中單引號后的生效,拉出了所有數(shù)據(jù)。查詢資料后,發(fā)現(xiàn)還要開啟一個參數(shù),讓端緩存,緩存是級別的。結(jié)論是個好東西。 背景 最近因為工作調(diào)整的關(guān)系,都在和數(shù)據(jù)庫打交道,增加了許多和JDBC親密接觸的機會,其實我們用的是Mybatis啦。知其然,知其所以然,是我們工程師童鞋們應(yīng)該追求的事情,能夠幫助你更好的理解這個技術(shù),面對問題...
摘要:不管是還是,表之間的連接查詢,被映射為實體類之間的關(guān)聯(lián)關(guān)系,這樣,如果兩個實體類之間沒有實現(xiàn)關(guān)聯(lián)關(guān)系,你就不能把兩個實體或者表起來查詢。 因為項目需要選擇數(shù)據(jù)持久化框架,看了一下主要幾個流行的和不流行的框架,對于復(fù)雜業(yè)務(wù)系統(tǒng),最終的結(jié)論是,JOOQ是總體上最好的,可惜不是完全免費,最終選擇JDBC Template。 Hibernate和Mybatis是使用最多的兩個主流框架,而JOO...
摘要:學(xué)編程真的不是一件容易的事不管你多喜歡或是多會編程,在學(xué)習(xí)和解決問題上總會碰到障礙。熟練掌握核心內(nèi)容,特別是和多線程初步具備面向?qū)ο笤O(shè)計和編程的能力掌握基本的優(yōu)化策略。 學(xué)Java編程真的不是一件容易的事,不管你多喜歡或是多會Java編程,在學(xué)習(xí)和解決問題上總會碰到障礙。工作的時間越久就越能明白這個道理。不過這倒是一個讓人進步的機會,因為你要一直不斷的學(xué)習(xí)才能很好的解決你面前的難題...
摘要:同時也有一些兒高級的處理,比如批處理更新事務(wù)隔離和可滾動結(jié)果集等。連接對象表示通信上下文,即,與數(shù)據(jù)庫中的所有的通信是通過此唯一的連接對象。因為是針對類的關(guān)系而言,所以一個對象對應(yīng)多個類的實例化。返回表示查詢返回表示其它操作。 JDBC是什么? JDBC是一個Java API,用中文可以通俗的解釋為,使用Java語言訪問訪問數(shù)據(jù)庫的一套接口集合。這是調(diào)用者(程序員)和實行者(數(shù)據(jù)庫廠商...
摘要:是訪問數(shù)據(jù)庫的標(biāo)準(zhǔn)規(guī)范提供了一種基準(zhǔn)據(jù)此可以構(gòu)建更高級的工具和接口,使數(shù)據(jù)庫開發(fā)人員能夠編寫數(shù)據(jù)庫應(yīng)用程序。在將此值發(fā)送到數(shù)據(jù)庫時,驅(qū)動程序?qū)⑺D(zhuǎn)換成一個類型值。 1.JDBC概念和數(shù)據(jù)庫驅(qū)動程序 A: JDBC概述 JDBC(Java Data Base Connectivity,java數(shù)據(jù)庫連接)是一種用于執(zhí)行SQL語句的Java API,可以為多種關(guān)系數(shù)據(jù)庫提供統(tǒng)一訪問,...
閱讀 1089·2021-11-19 09:40
閱讀 2227·2021-11-15 18:00
閱讀 1278·2021-10-18 13:34
閱讀 2258·2021-09-02 15:40
閱讀 1543·2019-08-30 14:01
閱讀 1122·2019-08-30 11:11
閱讀 2489·2019-08-29 15:26
閱讀 735·2019-08-29 14:15