JDBC是訪問關系型數據庫的java API.JDBC給程序員提供訪問和操縱眾多關系型數據庫的一個統一接口。使用JDBC API,程序員可以用java以一種用戶友好的接口執行SQL語句、獲取結果以及顯示數據,並且操縱數據。JDBC API還可用於分布式、異構環境中的多種數據源之間實現交互。
JDBC API是一組規范(java接口和類的集合)用於編寫和訪問操作關系數據庫的Java程序。JDBC驅動程序起一個接口的作用,它使JDBC與具體數據庫之間的通信靈活方便(實現了API的規范)。它由具體的數據庫供應商提供。下圖顯示了java程序、JDBC API和JDBC驅動程序和關系數據庫之間的關系。
使用JDBC開發數據庫應用程序
准備mysql結構數據(學員選課系統)
CREATE DATABASE avatar; -- 切換數據庫 USE avatar; /* 學員選課系統 1、課程實體 課程編號(主鍵) int 自增長 課程名稱 varchar(20) 學分 int 課程簡介 varchar(50) 2、學員信息 ssn char(9) primary key, stu_name varchar(20) not null, stu_gender enum('MALE','FEMALE') default 'MALE', born_date date, address varchar(60), phone varchar(15) 3、成績實體 ssn char(9), course_id int, score float(5,2), sc_date date, foreign key(ssn) references student(ssn), foreign key(course_id) references course(course_id), primary key(ssn,course_id) */ CREATE TABLE course( course_id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(20) NOT NULL, credit INT, course_intro VARCHAR(50) ); CREATE TABLE student( ssn CHAR(9) PRIMARY KEY, stu_name VARCHAR(20) NOT NULL, stu_gender ENUM('MALE','FEMALE') DEFAULT 'MALE', born_date DATE, address VARCHAR(60), phone VARCHAR(15) ); CREATE TABLE score( ssn CHAR(9), course_id INT, score FLOAT(5,2), sc_date DATE, FOREIGN KEY(ssn) REFERENCES student(ssn), FOREIGN KEY(course_id) REFERENCES course(course_id), PRIMARY KEY(ssn,course_id) );
使用java開發任何數據庫應用程序都需要4個主要接口:Driver、Connection、Statement和ResultSet。這些接口定義了使用SQL訪問數據庫的一般架構。JDBC API定義了這些接口。JDBC驅動程序開發商為這些接口提供實現。程序猿使用這些接口。JDBC應用程序使用Driver接口加載一個合適的驅動程序,使用Connection接口連接到數據庫,使用Statement接口創建和執行SQL語句,如果語句返回結果,使用ResultSet接口處理結果。
JDBC接口和類是開發java數據庫程序的構建模塊。開發jdbc程序的典型步驟為:
1、加載驅動程序
在連接到數據庫之前,需要加載一個合適的驅動程序
Class.forName("com.mysql.cj.jdbc.Driver");
驅動程序是一個實現接口java.sql.Driver的具體類。java6支持驅動程序的自動加載,因此不需要顯示的加載它們,但是並不是所有的驅動程序都有這個特性。為安全起見,應該顯式加載驅動程序。
2、建立連接
為了連接到一個數據庫,需要使用DriverManager類中的靜態方法getConnection(url,username,password).
Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.254.188:3306/avatar", "peppa", "peppa");
其中,url是數據庫在Internet上的唯一標識符,username為用戶名,password為密碼。
數據庫 | url模式 |
Mysql | jdbc:mysql://hostname:port/dbname |
Oracle | jdbc:oracle:thin:@hostname:port:oracleDBSID |
3、創建語句
如果把一個Connection對象想象成一條連接程序和數據庫的纜道,那么Statement對象可以看作一輛纜車,它為數據庫傳輸SQL語句用於執行,並把運行結果返回給程序。一旦創建了Connection對象,就可以創建執行SQL語句。
Statement stat = conn.createStatement();
4、執行語句
- execute(sql):可以執行任意類型sql語句,一般比較復雜,很少使用
- executeUpdate(sql):執行DML語句(insert、update、delete),返回的是影響行數
- executeQuery(sql):執行DQL語句(select),返回查詢結果----ResultSet
5、處理結果ResultSet
結果集ResultSet維護SQL語句的查詢結果,該結果的當前行可以獲得。當前行的初始位置是null。可以使用next方法移動到下一行,可以使用各種getter方法從當前行獲取值。
6、回收數據庫(釋放)資源
關閉連接並釋放與連接有關的資源,可以使用try-with-resource語法。
package edu.uestc.avatar.sql; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class SimpleJdbc { public static void addCourse() throws ClassNotFoundException, SQLException { //1、加載驅動程序 Class.forName("com.mysql.cj.jdbc.Driver"); //2、建立連接 Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.254.188:3306/avatar", "peppa", "peppa"); //3、創建語句 Statement stat = conn.createStatement(); //4、發送並執行sql,返回直接結果 String sql = "insert into course(course_name,credit,course_intro) values ('Linux',10,'Linux必知必會')"; int ret = stat.executeUpdate(sql); //5、處理結果 if(ret >= 1) System.out.println("課程添加成功"); //6、回收數據庫(釋放)資源 conn.close();//bug:應該確保資源的釋放 } public static void main(String[] args) throws ClassNotFoundException, SQLException { addCourse(); } }
SQL注入問題
CREATE TABLE `user`( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(12) NOT NULL UNIQUE, `password` VARCHAR(36) NOT NULL ); INSERT INTO `user` VALUES(DEFAULT,'peppa','peppa'), (DEFAULT,'suzy','suzy'),(DEFAULT,'emily','emily');
public static void find(String username,String password) throws Exception{ //java6支持驅動程序的自動加載,因此不需要顯示的加載它們,但是並不是所有的驅動程序都有這個特性。為安全起見,應該顯式加載驅動程序 Class.forName("com.mysql.cj.jdbc.Driver"); //2、獲取連接對象 Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.254.188:3306/avatar", "peppa", "peppa"); //3、通過Connection創建語句對象 Statement stat = conn.createStatement(); String sql = "select id,username,password from user where username='" + username + "' and password='" + password + "'"; ResultSet rs = stat.executeQuery(sql); while(rs.next()) { System.out.println("user id:" + rs.getInt("id")); System.out.println("username:" + rs.getString("username")); System.out.println("password:" + rs.getString("password")); System.out.println("============================================="); } }
這個程序有一個安全漏洞。如果在password字段中輸入1' or true or '1就會得到結果,這是因為查詢字符串變成了:
SELECT id,username,PASSWORD FROM USER WHERE username='candy' AND PASSWORD='1' OR TRUE OR '1'
可以使用PreparedStatement接口來避免這個問題。
PreparedStatement
一旦建立了一個到特定數據庫的連接,就可以用這個連接把程序的SQL語句發送到數據庫。Statement接口用於執行不含參數的靜態SQL語句。PreparedStatement接口繼承自Statement接口,用於執行含有或不含參數的預編譯SQL語句。由於SQL語句是預編譯的,所以重復執行它們時效率較高。
public static void login(String username, String password) { String sql = "select id,username, password from user where username=? and password=?"; try (Connection conn = DriverManager.getConnection("jdbc:mysql://192.168.254.188:3306/avatar","peppa","peppa")){ Class.forName("com.mysql.cj.jdbc.Driver"); //創建預編譯語句對象, PreparedStatement ptst = conn.prepareStatement(sql); //處理預編譯語句的參數 ptst.setString(1, username); ptst.setString(2, password); //執行sql語句 ResultSet rs = ptst.executeQuery(); if(rs.next()) { System.out.printf("登錄成功,用戶名為:%s;密碼為:%s", rs.getString("username"), rs.getString("password")); } } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } }
這個查詢語句有兩個問號用作參數的占位符表示用戶名和密碼的值。PreparedStatement提供了設置參數的方法。總體來看,PreparedStatement比Statement多了三個好處:
- PreparedStatement預編譯SQL語句,性能更好
- PreparedStatement無須拼接SQL語句,編程更簡單
- PreparedStatement可以防止SQL注入,安全性更好
規范和封裝JDBC代碼
目前存在的問題:
1、每執行一個JDBC方法都要注冊驅動,驅動應該只注冊一次且在系統啟動時注冊
2、數據庫連接屬性是硬編碼在代碼中,應該可配置完成解耦
3、像公共的獲取連接及釋放資源(可使用try-with-resource語法)應該抽取為公共的方法,避免在方法中鍵入相關屬性
解決方案:提供一個JDBC的助手類
package edu.uestc.avatar.commons; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; /** * jdbc助手(工具類) * */ public class JdbcHelper { //數據庫驅動類 private static String driverClassName; //數據庫唯一標識符 private static String url; //數據庫用戶賬戶名 private static String username; //賬戶密碼 private static String password; //private static JdbcHelper instance = new JdbcHelper(); private static JdbcHelper instance; /** * 完成JDBC相關初始化,只初始化一次 */ static { try { //初始化類變量信息,讀取類路徑下的jdbc.properties文件 Properties prop = new Properties(); //通過類加載器讀取位於classPath下的文件,將其轉為InputStream InputStream in = JdbcHelper.class.getClassLoader().getResourceAsStream("jdbc.properties"); prop.load(in); driverClassName = prop.getProperty("driverClassName"); url = prop.getProperty("url"); username = prop.getProperty("username"); password = prop.getProperty("password"); //加載數據庫驅動 Class.forName(driverClassName); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } } /** * 單例模式: * 1、構造方法私有化,外部不能創建其實例 * 2、內部創建其唯一的實例對外提供 * 飢漢模式:開始就創建其實例 * 懶漢模式:開始不創建,在系統第一次獲取該實例進行創建 */ private JdbcHelper() { } /** * 獲取數據庫連接對象 * @return Connection * @throws SQLException */ public Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } /** * 釋放jdbc相關資源 * @param rs 結果集 * @param stat 語句對象 * @param conn 連接對象 */ public void free(ResultSet rs, Statement stat, Connection conn) { try { if(rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } finally { try { if(stat != null) stat.close(); } catch (SQLException e) { e.printStackTrace(); } finally { try { if(conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } /** * 提供一個公共的靜態方法,對外返回該類的唯一實例 * @return 該類的唯一實例 */ // public static JdbcHelper getInstance() { // return instance; // } // public synchronized static JdbcHelper getInstance() { // if(instance == null) { // instance = new JdbcHelper(); // } // return instance; // } public static JdbcHelper getInstance() { if(instance == null) { synchronized (JdbcHelper.class) { if(instance == null) //雙重檢查 instance = new JdbcHelper(); } } return instance; } }
CallableStatement
可以使用CallableStatement執行SQL存儲過程。存儲過程可能會有IN、OUT或IN OUT參數。調用過程時,參數IN接收傳遞給存儲過程的值。在過程結束后,參數OUT返回一個值,但是當調用過程時,它不包含任何值。當過程被調用時,IN OUT參數包含傳遞給過程的值,在它完成后返回一個值。在Mysql數據庫中創建一個存儲過程
delimiter // create procedure compute_average(in p_ssn char(9),out p_score float(5,2)) begin select avg(score) into p_score from score where ssn = p_ssn; end; //
可以使用Connection接口中的prepareCall()方法來創建CallableStatement對象
var call = conn.prepareCall("{call compute_average(?,?)}");
{call compute_average(?,?)}指的是SQL轉義語法,它通知驅動程序其中的代碼應該被不同處理。驅動程序解析轉義語法,並且將它翻譯成數據庫可以理解的代碼。
compute_average為存儲過程名稱。
CallableStatement繼承自PreparedStatement。此外,CallableStatement接口提供了注冊OUT參數的方法以及從OUT參數獲取值的方法。
public class CallableStatementDemo { public static void main(String[] args) { try(var conn = JdbcHelper.getInstance().getConnection()){ /* * call compute_average(?,?)是SQL轉義語法 */ var call = conn.prepareCall("{call compute_average(?,?)}"); call.setString(1, "201810001"); //輸出參數注冊 call.registerOutParameter(2, Types.FLOAT); //執行存儲過程 call.execute(); System.out.println(call.getFloat(2)); }catch (SQLException e) { e.printStackTrace(); } } }
管理結果集
JDBC使用ResultSet來封裝執行查詢得到的查詢結果。然后通過移動ResultSet的記錄指針來取出結果集的內容,除此之外,JDBC還允許通過ResultSet來更新記錄。
可滾動可更新的結果集。
package edu.uestc.monster; import java.sql.ResultSet; import java.sql.SQLException; import edu.uestc.monster.commons.JdbcHelper; /** * ResultSet來更新記錄 * prepareStatement(String sql, int resultSetType,int resultSetConcurrency) * resultSetType: * ResultSet.TYPE_FORWARD_ONLY:結果集不能滾動(默認值) * ResultSet.TYPE_SCROLL_INSENSITIVE:結果集可以滾動,但是對數據庫變化不敏感 * ResultSet.TYPE_SCROLL_SENSITIVE:結果集可以滾動,對數據庫變化敏感 * resultSetConcurrency * ResultSet.CONCUR_READ_ONLY:只讀,結果集不能更新(默認值) * ResultSet.CONCUR_UPDATABLE:結果集可以用於更新數據庫 * */ public class ScrollAndUpdateTest { public static void main(String[] args) { var sql = "select cou_id,cou_name,credit,intro from course"; var helper = JdbcHelper.getInstance(); try (var conn = helper.getConnection(); var ptst = conn.prepareStatement(sql,ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE)){ var rs = ptst.executeQuery(); rs.afterLast();//將游標移動到結果集的末尾 while(rs.previous()) { System.out.println("課程編號:" + rs.getInt("cou_id")); System.out.println("課程名稱:" + rs.getString("cou_name")); System.out.println("課程學分:" + rs.getInt("credit")); System.out.println("課程簡介:" + rs.getObject("intro")); System.out.println("--------------------------------------"); } System.out.println("========================="); rs.absolute(3);//滾動到第三條記錄 System.out.println("課程編號:" + rs.getInt("cou_id")); System.out.println("課程名稱:" + rs.getString("cou_name")); System.out.println("課程學分:" + rs.getInt("credit")); System.out.println("課程簡介:" + rs.getObject("intro")); rs.absolute(2); var credit = rs.getInt("credit"); rs.updateInt(3, credit + 3); //可更新記錄結果集 rs.updateRow();//將更新的記錄提交到數據庫 } catch (SQLException e) { e.printStackTrace(); } } }
處理大數據類型數據
利用Clob處理大的文本數據及利用Blob處理長二進制數據
package edu.uestc.avatar.sql; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.sql.SQLException; import edu.uestc.avatar.commons.JdbcHelper; public class LargerDataDemo { /** * 添加文本文件 * @param ssn 學號 * @param file 文本文件===>學生簡歷 */ private static void updateStudentResume(String ssn, File file) { String sql = "update student set resume=? where ssn=?"; try(var conn = JdbcHelper.getInstance().getConnection(); var reader = new FileReader(file)){ var ptst = conn.prepareStatement(sql); ptst.setClob(1, reader, file.length());//設置文本內容,將文本轉為reader進行傳輸 ptst.setString(2, ssn); int ret = ptst.executeUpdate(); if(ret == 1) System.out.println(ssn + "學員個人簡歷更新成功"); }catch(SQLException | IOException e) { e.printStackTrace(); } } public static void main(String[] args) { //updateStudentResume("201810001",new File("D:\\test\\clock.html")); //readStudentResume("201810001"); //updateStudentPhoto("201810001", new File("C:/Users/Adan/Pictures/sufei.jpg")); readStudentPhoto("201810001"); } private static File readStudentResume(String ssn) { var file = new File(ssn + ".txt"); String sql = "select resume from student where ssn = ?"; try(var conn = JdbcHelper.getInstance().getConnection(); var out = new BufferedWriter(new FileWriter(file))){ var ptst = conn.prepareStatement(sql); ptst.setString(1, ssn); var rs = ptst.executeQuery(); if(rs.next()) { var clob = rs.getClob("resume"); var in = clob.getCharacterStream(); char[] buff = new char[1024]; var len = -1; while((len = in.read(buff)) != -1) out.write(buff, 0, len); in.close(); } }catch(SQLException | IOException e) { e.printStackTrace(); } return file; } private static void updateStudentPhoto(String ssn,File image) { String sql = "update student set photo=? where ssn=?"; try(var conn = JdbcHelper.getInstance().getConnection(); var input = new BufferedInputStream(new FileInputStream(image))){ var ptst = conn.prepareStatement(sql); ptst.setBlob(1, input);//blob對應的二進制IO流 ptst.setString(2, ssn); int ret = ptst.executeUpdate(); if(ret == 1) System.out.println(ssn + "學員頭像更新成功"); }catch(SQLException | IOException e) { e.printStackTrace(); } } private static File readStudentPhoto(String ssn) { var file = new File(ssn + ".jpg"); String sql = "select photo from student where ssn=?"; try(var conn = JdbcHelper.getInstance().getConnection(); var output = new BufferedOutputStream(new FileOutputStream(file))){ var ptst = conn.prepareStatement(sql); ptst.setString(1, ssn); var rs = ptst.executeQuery(); if(rs.next()) { var blob = rs.getBlob("photo"); byte[] buff = new byte[1024]; var input = blob.getBinaryStream(); int len = -1; while((len = input.read(buff)) != -1) { output.write(buff, 0, len); } } }catch(SQLException | IOException e) { e.printStackTrace(); } return file; } }
獲取元數據
利用數據庫元數據可以動態獲取數據庫相關信息
1、數據庫元數據
public class DatabaseMetaDemo { public static void main(String[] args) { try(var conn = JdbcHelper.getInstance().getConnection()){ var dbMeta = conn.getMetaData(); System.out.println("url:" + dbMeta.getURL()); System.out.println("database product name:" + dbMeta.getDatabaseProductName()); System.out.println("dababase major version:" + dbMeta.getDatabaseMajorVersion()); System.out.println("dababase minor version:" + dbMeta.getDatabaseMinorVersion()); System.out.println("jdbc driver name:" + dbMeta.getDriverName()); System.out.println("jdbc driver version:" + dbMeta.getDriverVersion()); System.out.println("max number of connections:" + dbMeta.getMaxConnections()); System.out.println("=======================結果集數據庫表================="); var rsTables = dbMeta.getTables(null, null, null, new String[] {"TABLE"}); System.out.println("user tables:"); while(rsTables.next()) System.out.println(rsTables.getString("TABLE_NAME")); }catch (SQLException e) { e.printStackTrace(); } } }
2、結果集元數據
public class ResultSetMetaDataDemo { public static void main(String[] args) { try(var conn = JdbcHelper.getInstance().getConnection()){ var sql = "select course_id id,course_name name,credit,course_intro intro from course"; var ptst = conn.prepareStatement(sql); var rs = ptst.executeQuery(); //獲取結果集的元數據 var meta = rs.getMetaData(); for(int i = 1; i <= meta.getColumnCount(); i++) //列名:getColumnName() System.out.print(meta.getColumnLabel(i) + "\t"); System.out.println(); while(rs.next()) { for(int i = 1; i <= meta.getColumnCount(); i++) System.out.print(rs.getObject(i) + "\t"); System.out.println(); } }catch (SQLException e) { e.printStackTrace(); } } }
RowSet
RowSet接口繼承自ResultSet接口,RowSet接口下包含JdbcRowSet、CachedRowSet、FilteredRowSet、JoinRowSet和WebRowSet常用子接口。除了JdbcRowSet需要保持與數據的連接外,其余4個子接口都是離線的RowSet,無須保持與數據庫的連接。
與ResultSet相比,RowSet默認時可滾動、可更新、可序列化的結果集,而且作為javaBean使用,因此能方便地在網絡上傳輸,用於同步兩端的數據。對於離線RowSet而言,程序創建RowSet時已經把數據從數據庫讀取到了內存,因此可以充分利用計算機的內存,從而降低數據庫服務器的負載,提高程序性能。
通過RowSetFactory把應用程序和RowSet實現類分離開,下例演示可滾動、可更新的特性
離線RowSet
在使用ResultSet的時代,程序查詢得到ResultSet之后必須立即讀取或處理它對應的記錄,否則一旦Connection關閉,再取通過ResultSet讀取記錄就會引發異常。在這種模式下,JDBC編程十分痛苦,假設應用程序分為數據訪問層和視圖顯式層,當應用程序在數據訪問層查詢得到ResultSet后,對ResultSet的處理有如下兩種常見的方式:
- 使用迭代訪問ResultSet里的記錄,並將這些記錄轉成JavaBean。再將多個JavaBean封裝到一個List集合(參考另一篇博文:策略模式)。處理完成后就可以關閉Connection等資源,然后再將JavaBean集合傳到視圖顯示層。
- 直接將ResultSet傳到視圖顯示層——這要求當視圖顯示層顯示數據時,底層Connection必須一直處於打開狀態,否則ResultSet無法讀取數據。
第一種方式比較安全,但編程要求較高,對各種類型轉換處理也比較繁瑣;第二種方式則需要Connection一直處於打開狀態,這樣不僅不安全,而且對程序性能也有很大影響。通過使用離線RowSet可以十分"優雅"地處理上面的問題,離線RowSet會直接將底層數據讀入內存中,封裝成RowSet對象,而RowSet對象可以完全當成JavaBean來使用。因此不僅安全而且編程十分簡單。CachedRowSet是所有離線RowSet的父接口,因此以CachedRowSet為例作為介紹:
/** * DQL語句的頂層邏輯 * 使用離線結果集 * RowSet是ResultSet的子接口,RowSet默認是可滾動,可更新的 * JdbcRowSet * CachedRowSet * FilteredRowSet * JoinRowSet * WebRowSet * 除了JdbcRowSet需要與數據庫保持連接之外,其余的都是離線RowSet * @param sql 要執行的查詢語句 * @param params 可變參數 * @return 離線結果集 */ public RowSet executeQuery(String sql, Object...params){ Connection conn = null; PreparedStatement ptst = null; ResultSet rs = null; try{ conn = helper.getConnection(); ptst = conn.prepareStatement(sql); //對sql語句中的?占位參數進行設值 for (var i = 0; params != null && i < params.length; i++) ptst.setObject(i + 1, params[i]); rs = ptst.executeQuery(); //使用RowSetProvider創建RowSetFactory RowSetFactory factory = RowSetProvider.newFactory(); //創建默認的CachedRowSet實例 CachedRowSet crs = factory.createCachedRowSet(); //使用ResultSet填充RowSet crs.populate(rs); return crs; }catch (SQLException e){ LOGGER.debug(e.getMessage(),e); }finally { helper.free(rs,ptst,conn); } return null; }
離線RowSet的查詢分頁
由於CachedRowSet會將數據記錄直接裝載到內存中,因此如果SQL查詢返回的記錄過大,CachedRowSet就會占用大量的內存,在某些極端的情況下,甚至會導致內存溢出。為了解決該問題,CachedRowSet提供了分頁功能。所謂分頁就是一次裝載ResultSet中的某幾條記錄,這樣就可以避免CachedRowSet占用內存過大的問題。CachedRowSet通過如下方法控制分頁:
populate(ResultSet rs, int startRow):從給定的rs的第startRow條記錄開始填充
setPageSize(int pageSize):設置CachedRowSet每次返回多少條記錄
previousPage():在底層ResultSet可用的情況下,讓CachedRowSet讀取上一頁的記錄
nextPage():在底層ResultSet可用的情況下,讓CachedRowSet讀取下一頁的記錄
/** * 封裝分頁DQL頂層邏輯 * @param sql sql語句 * @param offset 數據起始偏移量 * @param pageSize 加載數據量 * @param params 查詢參數 * @return 離線RowSet */ public RowSet executeQuery(String sql, int offset,int pageSize,Object...params){ try( var conn = helper.getConnection(); var ptst = conn.prepareStatement(sql)){ //對sql語句中的?占位參數進行設值 for (var i = 0; params != null && i < params.length; i++) ptst.setObject(i + 1, params[i]); var rs = ptst.executeQuery(); //使用RowSetProvider創建RowSetFactory RowSetFactory factory = RowSetProvider.newFactory(); //創建默認的CachedRowSet實例 CachedRowSet crs = factory.createCachedRowSet(); //設置每頁顯示pageSize記錄數 crs.setPageSize(pageSize); //使用ResultSet填充RowSet,從第幾條記錄開始樁頭 crs.populate(rs,offset); return crs; }catch (SQLException e){ LOGGER.debug(e.getMessage(),e); } return null; }
事務處理
事務:代表一個業務邊界(業務邏輯的多條語句組成),這系列操作要么全部執行,要么全部放棄執行。是保證數據完整的重要手段。
事務具備4個特性:
- 原子性(Atomicity):事務是一個整體的工作單元,事務對數據庫所做的操作要么全部執行,要么全部取消。如果某條語句執行失敗,則所有語句全部回滾。
- 一致性(Consistency):事務在完成時,必須使所有的數據都保持一致狀態。在相關數據庫中,所有規則都必須應用於事務的修改,以保持所有數據的完整性。如果事務成功,則所有數據將變為一個新的狀態;如果事務失敗,則所有數據將處於開始之前的狀態。
- 隔離性(Isolation):事務與事務之間是相互隔離的
由事務所作的修改必須與其他事務所作的修改隔離。事務查看數據時數據所處的狀態,要么是另一並發事務修改它之前的狀態,要么是另一事務修改它之后的狀態,事務不會查看中間狀態的數據。
- 持久性(Durability):事務提交后,對數據庫數據的影響是持久性(永久的)
JDBC連接也提供了事務的支持。默認情況下,新連接是自動提交模式,並且每條sql語句都作為一個單獨的事務執行和提交。可以用setAutoCommit(false)方法取消自動提交,此時,調用commit()或rollback()之前的所有語句都被組織成一個事務。
public class TransactionalDemo { public static void main(String[] args) { Connection conn = null; Savepoint sp = null; try { conn = JdbcHelper.getInstance().getConnection(); /* * 開啟事務就是將默認的自動提交關閉 */ conn.setAutoCommit(false); String sql = "insert into course(course_name) values ('Linux')"; var ptst = conn.prepareStatement(sql); ptst.executeUpdate(); //可以在程序中設置多個事務保存點,可以將事務回滾到指定的保存點 sp = conn.setSavepoint(); sql = "delete from student where ssn = '201810001'"; ptst = conn.prepareStatement(sql); ptst.executeUpdate(); //違反約束,會發生異常 sql = "insert into score(ssn,course_id,score) values('9527',10,78)"; ptst = conn.prepareStatement(sql); ptst.executeUpdate(); //如果沒有異常,提交事務 conn.commit(); }catch (SQLException e) { try { System.out.println("回滾事務"); if(conn != null) { //conn.rollback();//全部回滾 //回滾到指定的保存點 conn.rollback(sp); //保存點前的操作進行提交 conn.commit(); } e.printStackTrace(); } catch (SQLException e1) { e1.printStackTrace(); } } finally { JdbcHelper.getInstance().free(null, null, conn); } } }
批量更新
多條SQL語句被作為一批操作被同時收集,並同時提交
package edu.uestc.canary.jdbc; import java.sql.Connection; import java.sql.SQLException; public class BatchUpdatesDemo { public static void main(String[] args) { Connection conn = null; try{ conn = JdbcHelper.getInstance().getConnection(); /* * 開啟事務就是將默認的自動提交關閉 */ conn.setAutoCommit(false); var sql = "insert into user(username,password) values(?,?)"; var ptst = conn.prepareStatement(sql); for(var i = 1; i <= 8650; i++) { ptst.setString(1, "batch_" + i); ptst.setString(2, "pass_" + i); ptst.addBatch(); //收集sql語句 if(i % 1000 == 0) ptst.executeLargeBatch();//一次提交所收集到的sql語句 } //提交剩余的sql語句 ptst.executeLargeBatch(); //沒有異常,執行成功,提交事務 conn.commit(); }catch(SQLException e) { //有異常,回滾事務 try { System.out.println("發生異常,事務回滾:" + e.getMessage()); conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } finally { //最終確保連接釋放 JdbcHelper.getInstance().free(conn); } } }
使用連接池管理連接
數據庫連接的建立及關閉是極耗費系統資源的操作,在多層結構的應用環境中,這種資源的耗費對系統性能影響尤為明顯。通過DriverMananger獲取數據庫的連接均對應一個物理數據庫的連接,每次操作都打開一個物理連接,使用完后理解關閉連接。頻繁的打開、關閉連接將造成系統性能低下。
數據庫連接池的解決方案:當應用程序啟動時,系統主動建立足夠的數據庫連接,並將這些連接放入連接池。每次應用程序請求數據庫連接時,無須重新打開連接,而是從連接池中取出已有的連接使用,使用完后歸還給連接池。為了解決數據庫連接的頻繁請求、釋放,JDBC2.0規范引入了數據庫連接池技術。使用javax.sql.DataSource來表示,DataSource只是一個規范,該規范通常由商用服務器及開源組織提供實現。
package edu.uestc.avatar.commons; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import javax.sql.DataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; /** * jdbc工具類 * 采用數據源(jdbc擴展規范)來獲取連接 * 傳統方式:DriverManager.getConnection(url,username,password);使用完畢后進行釋放 * 創建數據庫連接是非常消耗系統資源,頻繁創建及釋放連接,對系統性能影響很大 * 連接池參數 * 初始化連接數 * 最大連接數 * 最大等待時間 * 最大空閑數 * 最小空閑數 * * 選用產品: * dbcp2、c3p0、druid、boneCP、hikari ....... * */ @SuppressWarnings("static-access") public class JdbcUtil { //數據源 private static DataSource dataSource; private static JdbcUtil instance; static { try {//初始化數據源 var prop = new Properties(); prop.load(JdbcUtil.class.getClassLoader().getResourceAsStream("jdbc.properties")); //通過工廠創建數據源,只要properties中的property的名稱為DruidDataSourceFactory里的屬性名相同,會自動將對應的屬性值設置給對應的屬性 var factory = new DruidDataSourceFactory(); dataSource = factory.createDataSource(prop); } catch (Exception e) { e.printStackTrace(); } } private JdbcUtil() { } /** * 通過數據源獲取連接對象 * @return 數據庫連接對象 * @throws SQLException */ public Connection getConnetion() throws SQLException { return dataSource.getConnection(); } /** * 釋放jdbc相關資源 * @param rs 結果集 * @param stat 語句對象 * @param conn 連接對象 */ public void free(ResultSet rs, Statement stat, Connection conn) { try { if(rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } finally { try { if(stat != null) stat.close(); } catch (SQLException e) { e.printStackTrace(); } finally { try { if(conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } public static JdbcUtil getInstance() { if(instance == null) { synchronized (JdbcUtil.class) { if(instance == null) instance = new JdbcUtil(); } } return instance; } }
DAO模式
DAO(Data Access Object)顧名思義是一個為數據庫或其他持久化機制提供了抽象接口的對象,在不暴露底層持久化方案實現細節的前提下提供了各種數據訪問操作。
在實際的開發中,應該將所有對數據源的訪問操作進行抽象化后封裝在一個公共API中。
用程序設計語言來說,就是建立一個接口,接口中定義了此應用程序中將會用到的所有事務方法。在這個應用程序中,當需要和數據源進行交互的時候則使用這個接口,並且編寫一個單獨的類來實現這個接口,在邏輯上該類對應一個特定的數據存儲。
DAO模式實際上包含了兩個模式,一是Data Accessor(數據訪問器),二是Data Object(數據對象),前者要解決如何訪問數據的問題,而后者要解決的是如何用對象封裝數據。
package edu.uestc.avatar.domain; import java.time.LocalDate; /** * 學生實體類----pojo * 用於Student對象封裝數據 * */ public class Student { private String ssn; private String name; //后期使用枚舉 private String gender = "MALE"; private String address; private String phone; private LocalDate bornDate; //setter and getter @Override public String toString() { return "Student [ssn=" + ssn + ", name=" + name + ", gender=" + gender + ", address=" + address + ", phone=" + phone + ", bornDate=" + bornDate + "]"; } }
針對Student的是Data Accessor(數據訪問器)
package edu.uestc.avatar.dao; import java.util.List; import edu.uestc.avatar.domain.Student; public interface StudentDao { /** * 保存學生信息 * @param student {@link Student} */ void save(Student student); /** * 根據學號刪除學生信息 * @param ssn 學號 */ void removeBySsn(String ssn); /** * 根據學號修改學生信息 * @param student 修改后的學員信息,學號不允許被修改 */ void modify(Student student); /** * 根據學號加載學員信息 * @param ssn 學號 * @return 學員信息,沒有找到,返回null */ Student findBySsn(String ssn); /** * 加載學生總數 * @return 學生總數 */ long count(); /** * 分頁加載學生信息 * limit offset, size * @param offset 起始偏移量,基於0 * @param size 加載大小 * @return 學生列表 */ List<Student> paging(int offset, int size); }
單獨的類來實現這個數據訪問接口,后期使用模板方法模式及策略模式改寫
package edu.uestc.avatar.dao.impl; import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import edu.uestc.avatar.commons.JdbcUtil; import edu.uestc.avatar.dao.StudentDao; import edu.uestc.avatar.domain.Student; /** * StudentDao實現類 * */ public class StudentDaoImpl implements StudentDao { private JdbcUtil util = JdbcUtil.getInstance(); @Override public void save(Student student) { Connection conn = null; try { conn = util.getConnetion(); var sql = "insert into student(ssn,stu_name,stu_gender,born_date,address,phone)" + " values (?,?,?,?,?,?)"; var ptst = conn.prepareStatement(sql); //設置預編譯參數 ptst.setString(1, student.getSsn()); ptst.setString(2, student.getName()); ptst.setString(3, student.getGender()); ptst.setObject(4, student.getBornDate()); ptst.setString(5, student.getAddress()); ptst.setString(6, student.getPhone()); ptst.executeUpdate(); }catch(SQLException e) { e.printStackTrace(); } finally { util.free(null, null, conn); } } @Override public void removeBySsn(String ssn) { Connection conn = null; try { conn = util.getConnetion(); var sql = "delete from student where ssn=?"; var ptst = conn.prepareStatement(sql); ptst.setString(1, ssn); ptst.executeUpdate(); }catch(SQLException e) { e.printStackTrace(); } finally { util.free(null, null, conn); } } @Override public void modify(Student student) { Connection conn = null; try { conn = util.getConnetion(); var sql = "update student set stu_name=?,stu_gender=?,born_date=?," + "address=?,phone=? where ssn=?"; var ptst = conn.prepareStatement(sql); ptst.setString(1, student.getName()); ptst.setString(2, student.getGender()); ptst.setObject(3, student.getBornDate()); ptst.setString(4, student.getAddress()); ptst.setString(5, student.getPhone()); ptst.setString(6, student.getSsn()); ptst.executeUpdate(); }catch(SQLException e) { e.printStackTrace(); } finally { util.free(null, null, conn); } } @Override public Student findBySsn(String ssn) { Student student = null; Connection conn = null; try { conn = util.getConnetion(); var sql = "select ssn,stu_name,stu_gender,born_date,address,phone from student where ssn=?"; var ptst = conn.prepareStatement(sql); ptst.setString(1, ssn); var rs = ptst.executeQuery(); if(rs.next()) { student = new Student(); student.setSsn(rs.getString("ssn")); student.setGender(rs.getString("stu_gender")); student.setAddress(rs.getString("address")); student.setName(rs.getString("stu_name")); student.setPhone(rs.getString("phone")); if(rs.getDate("born_date") != null) student.setBornDate(rs.getDate("born_date").toLocalDate());//java.sql.Date } }catch(SQLException e) { e.printStackTrace(); } finally { util.free(null, null, conn); } return student; } @Override public long count() { Connection conn = null; try { var sql = "select count(ssn) from student"; conn = util.getConnetion(); var ptst = conn.prepareStatement(sql); var rs = ptst.executeQuery(); if(rs.next()) { return rs.getLong(1); } }catch(SQLException e) { e.printStackTrace(); } finally { util.free(null, null, conn); } return 0; } @Override public List<Student> paging(int offset, int size) { List<Student> students = new ArrayList<Student>(); Connection conn = null; try { var sql = "select ssn,stu_name,stu_gender,born_date,address,phone from student limit ?,?"; conn = util.getConnetion(); var ptst = conn.prepareStatement(sql); ptst.setInt(1, offset); ptst.setInt(2, size); //執行查詢,處理結果集 var rs = ptst.executeQuery(); while(rs.next()) { Student student = new Student(); student.setSsn(rs.getString("ssn")); student.setGender(rs.getString("stu_gender")); student.setAddress(rs.getString("address")); student.setName(rs.getString("stu_name")); student.setPhone(rs.getString("phone")); if(rs.getDate("born_date") != null) student.setBornDate(rs.getDate("born_date").toLocalDate());//java.sql.Date students.add(student); } }catch(SQLException e) { e.printStackTrace(); } finally { util.free(null, null, conn); } return students; } }