标簽: Java與存儲
JDBC(Java Database Connectivity)代表Java程式設計語言與資料庫連接配接的标準API,然而JDBC隻是接口,JDBC驅動才是真正的接口實作,沒有驅動無法完成資料庫連接配接. 每個資料庫廠商都有自己的驅動,用來連接配接自己公司的資料庫(如Oricle, MySQL, DB2, MS SQLServer).
下面我們以MySQL為例,JDBC程式設計大緻步驟如下:
注意: 需要在pom.xml中添加如下MySQL驅動:
注: <code>ResultSet</code>參數<code>columnIndex</code>索引從1開始,而不是0!
JDBC規定: 驅動類在被加載時,需要<code>主動</code>把自己注冊到<code>DriverManger</code>中:
com.mysql.jdbc.Driver
代碼顯示:隻要去加載<code>com.mysql.jdbc.Driver</code>類那麼就會執行<code>static</code>塊, 進而把<code>com.mysql.jdbc.Driver</code>注冊到<code>DriverManager</code>中.
<code>java.sql.DriverManager</code>是用于管理JDBC驅動的服務類,其主要功能是擷取<code>Connection</code>對象:
1. <code>static Connection getConnection(String url, Properties info)</code>
2. <code>static Connection getConnection(String url, String user, String password)</code>
另: 還可以在擷取<code>Connection</code>的URL中設定參數,如: jdbc:mysql://host:port/database?useUnicode=true&characterEncoding=UTF8 <code>useUnicode=true&characterEncoding=UTF8</code>指定連接配接資料庫的過程中使用Unicode字元集/UTF-8編碼;
<code>java.sql.Connection</code>代表資料庫連接配接,每個<code>Connection</code>代表一個實體連接配接會話, 該接口提供如下建立<code>Statement</code>的方法, 隻有擷取<code>Statement</code>之後才可執行SQL語句:
方法
描述
<code>Statement createStatement()</code>
Creates a Statement object for sending SQL statements to the database.
<code>PreparedStatement prepareStatement(String sql)</code>
Creates a PreparedStatement object for sending parameterized SQL statements to the database.
<code>CallableStatement prepareCall(String sql)</code>
Creates a CallableStatement object for calling database stored procedures.
其中<code>Connection</code>還提供了如下控制事務/儲存點的方法:
<code>Savepoint setSavepoint(String name)</code>
Creates a savepoint with the given name in the current transaction and returns the new Savepoint object that represents it.
<code>void setTransactionIsolation(int level)</code>
Attempts to change the transaction isolation level(事務隔離級别) for this Connection object to the one given.
<code>void setAutoCommit(boolean autoCommit)</code>
Sets this connection’s auto-commit mode to the given state.
<code>void rollback()</code>
Undoes all changes made in the current transaction and releases any database locks currently held by this Connection object.
<code>void rollback(Savepoint savepoint)</code>
Undoes all changes made after the given Savepoint object was set.
<code>void commit()</code>
Makes all changes made since the previous commit/rollback permanent and releases any database locks currently held by this Connection object.
由于擷取<code>Connection</code>的步驟單一,每次可能隻是加載的參數不同,是以我們可以将擷取<code>Connection</code>的操作封裝成一個方法,并使其從配置檔案中加載配置:
配置檔案形式
ConnectionManger
SQLUtil
前面通過<code>DriverManger</code>獲得<code>Connection</code>, 一個<code>Connection</code>對應一個實際的實體連接配接,每次操作都需要打開實體連接配接, 使用完後立即關閉;這樣頻繁的打開/關閉連接配接會造成不必要的資料庫系統性能消耗.
資料庫連接配接池提供的解決方案是:當應用啟動時,主動建立足夠的資料庫連接配接,并将這些連接配接組織成連接配接池,每次請求連接配接時,無須重新打開連接配接,而是從池中取出已有連接配接,使用完後并不實際關閉連接配接,而是歸還給池.
JDBC資料庫連接配接池使用<code>javax.sql.DataSource</code>表示, <code>DataSource</code>隻是一個接口, 其實作通常由伺服器提供商(如WebLogic, WebShere)或開源組織(如DBCP,C3P0和HikariCP)提供.
資料庫連接配接池的常用參數如下:
資料庫初始連接配接數;
連接配接池最大連接配接數;
連接配接池最小連接配接數;
連接配接池每次增加的容量;
Tomcat預設使用的是DBCP連接配接池,但相比之下,C3P0則比DBCP更勝一籌(Hibernate推薦使用C3P0),C3P0不僅可以自動清理不再使用的<code>Connection</code>, 還可以自動清理<code>Statement</code>/<code>ResultSet</code>, 使用C3P0需要在pom.xml中添加如下依賴:
C3P0還可以使用配置檔案來初始化連接配接池(配置檔案可以是properties/XML, 在此僅介紹XML),C3P0配置檔案名必須為c3p0-config.xml,其放在類路徑下:
這樣, 我們在建立<code>ComboPooledDataSource</code>時就預設加載配置檔案中的配置, 無須手動配置:
C3P0配置檔案可以配置多個連接配接資訊, 并為每個配置命名, 這樣可以友善的通過配置名稱來切換配置資訊:
HikariCP用方法擷取<code>Connection</code>的方法與C3P0大同小異:
附:
2. properties檔案形式如下:
<code>java.sql.Statement</code>可用于執行DDL/DML/DCL語句:
<code>boolean execute(String sql)</code>
Executes the given SQL statement, which may return multiple results.
<code>ResultSet executeQuery(String sql)</code>
Executes the given SQL statement, which returns a single ResultSet object.
<code>int executeUpdate(String sql)</code>
Executes the given SQL statement, which may be an INSERT, UPDATE, or DELETE statement or an SQL statement that returns nothing, such as an SQL DDL statement.
<code>int[] executeBatch()</code>
Submits a batch of commands to the database for execution and if all commands execute successfully, returns an array of update counts.
Java 1.7還新增了<code>closeOnCompletion()</code>方法,當所有依賴于目前<code>Statement</code>的<code>ResultSet</code>關閉時,該<code>Statement</code>自動關閉.
<code>Statement</code>使用<code>executeUpdate</code>方法執行DDL/DML(不包含<code>select</code>)語句:執行DDL該方法傳回0; 執行DML傳回受影響的記錄數.
DDL
DML
<code>execute</code>方法幾乎可以執行任何SQL語句,但較為繁瑣(除非不清楚SQL語句類型,否則不要使用<code>execute</code>方法).該方法傳回值為<code>boolean</code>,代表執行該SQL語句是否傳回<code>ResultSet</code>,然後<code>Statement</code>提供了如下方法來擷取SQL執行的結果:
<code>ResultSet getResultSet()</code>
Retrieves the current result as a ResultSet object.
<code>int getUpdateCount()</code>
Retrieves the current result as an update count; if the result is a ResultSet object or there are no more results, -1 is returned.
client
<code>PreparedStatement</code>是<code>Statement</code>的子接口, 它可以預編譯SQL語句,編譯後的SQL模闆被存儲在<code>PreparedStatement</code>對象中,每次使用時首先為SQL模闆設值,然後執行該語句(是以使用<code>PreparedStatement</code>效率更高).
建立<code>PreparedStatement</code>需要使用<code>Connection</code>的<code>prepareStatement(String sql)</code>方法,該方法需要傳入SQL模闆,可以包含占位符參數:
<code>PreparedStatement</code>也提供了<code>excute</code>等方法來執行SQL語句, 隻是這些方法無須傳入參數, 因為SQL語句已經存儲在<code>PreparedStatement</code>對象中.
由于執行SQL前需要為SQL模闆傳入參數值,<code>PreparedStatement</code>提供了一系列的<code>setXxx(int parameterIndex, X x)</code>方法;另外,如果不清楚SQL模闆各參數的類型,可以使用<code>setObject(int parameterIndex, Object x)</code>方法傳入參數, 由<code>PreparedStatement</code>來負責類型轉換.
注意: SQL語句的占位符參數隻能代替普通值, 不能代替表名/列名等資料庫對象, 更不能代替INSERT/SELECT等關鍵字.
注: 1. 預設使用<code>PreparedStatement</code>是沒有開啟預編譯功能的,需要在URL中給出<code>useServerPrepStmts=true</code>參數來開啟此功能; 2. 當使用不同的<code>PreparedStatement</code>對象來執行相同SQL語句時,還是會出現編譯兩次的現象,這是因為驅動沒有緩存編譯後的函數key,導緻二次編譯.如果希望緩存編譯後的函數key,那麼就要設定<code>cachePrepStmts=true</code>參數. 3. 另外, 還可以設定預編譯緩存的大小:cachePrepStmts=true&prepStmtCacheSize=50&prepStmtCacheSqlLimit=300` jdbc:mysql://host:port/database?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=50&prepStmtCacheSqlLimit=300
在資料庫中建立一個簡單的存儲過程<code>add_pro</code>:
<code>delimiter //</code>會将SQL語句的結束符改為<code>//</code>, 這樣就可以在建立存儲過程時使用<code>;</code>作為分隔符. MySQL預設使用<code>;</code>作為SQL結束符.
調用存儲過程需要使用<code>CallableStatement</code>,可以通過<code>Connection</code>的<code>prepareCall()</code>方法來建立,建立時需要傳入調用存儲過程的SQL語句,形式為:
存儲過程的參數既有入參,也有回參; 入參可通過<code>setXxx(int parameterIndex/String parameterName, X x)</code>方法傳入;回參可以通過調用<code>registerOutParameter(int parameterIndex, int sqlType)</code>來注冊, 經過上面步驟, 就可以調用<code>execute()</code>方法來調用該存儲過程, 執行結束, 則可通過<code>getXxx(int parameterIndex/String parameterName)</code>方法來擷取指定回參的值:
JDBC使用<code>ResultSet</code>封裝查詢結果,然後通過<code>ResultSet</code>的記錄指針來讀取/更新記錄.并提供了<code>ResultSetMetaDate</code>來獲得<code>ResultSet</code>對象的中繼資料資訊.
<code>java.sql.ResultSet</code>是結果集對象,可以通過列索引/列名來讀/寫資料, 它提供了如下常用方法來移動記錄指針:
<code>boolean next()</code>
Moves the cursor froward one row from its current position.
<code>boolean previous()</code>
Moves the cursor to the previous row in this ResultSet object.
<code>boolean first()</code>
Moves the cursor to the first row in this ResultSet object.
<code>boolean last()</code>
Moves the cursor to the last row in this ResultSet object.
<code>void beforeFirst()</code>
Moves the cursor to the front of this ResultSet object, just before the first row.
<code>void afterLast()</code>
Moves the cursor to the end of this ResultSet object, just after the last row.
<code>boolean absolute(int row)</code>
Moves the cursor to the given row number in this ResultSet object.
<code>boolean relative(int rows)</code>
Moves the cursor a relative number of rows, either positive or negative.
當把記錄指針定位到指定行後, <code>ResultSet</code>可通過<code>getXxx(int columnIndex/String columnLabel)</code>方法來獲得指定類型值.或使用<code><T> T getObject(int columnIndex/String columnLabel, Class<T> type)</code>來擷取任意類型值.
以預設方式打開的<code>ResultSet</code>是不可更新的,獲得可更新的<code>ResultSet</code>,需要在建立<code>Statement</code>/<code>PreparedStatement</code>時傳入如下兩個參數:
resultSetType: 控制<code>ResultSet</code>可移動方向
<code>ResultSet.TYPE_FORWARD_ONLY</code>
The constant indicating the type for a ResultSet object whose cursor may move only forward.
<code>ResultSet.TYPE_SCROLL_INSENSITIVE</code>
The constant indicating the type for a ResultSet object that is scrollable but generally not sensitive to changes to the data that underlies the ResultSet.
<code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
The constant indicating the type for a ResultSet object that is scrollable and generally sensitive to changes to the data that underlies the ResultSet.
resultSetConcurrency: 控制<code>ResultSet</code>的讀/寫并發類型
<code>ResultSet.CONCUR_READ_ONLY</code>
The constant indicating the concurrency mode for a ResultSet object that may NOT be updated.
<code>ResultSet.CONCUR_UPDATABLE</code>
The constant indicating the concurrency mode for a ResultSet object that may be updated.
另外可更新的結果集還必須滿足如下條件: 1) 所有資料都來自一個表; 2)選出的資料集必須包含主鍵列;
這樣, 擷取的<code>ResultSet</code>就是可更新/可滾動的, 程式可通過調用<code>ResultSet</code>的<code>updateXxx(int columnIndex/String columnLabel, X x)</code>方法來修改記錄指針所指向的值, 最後調用<code>updateRow()</code>來送出修改.
SQLClient
<code>ResultSet</code>提供了<code>getMetaData()</code>方法來擷取<code>ResultSetMetaData</code>以分析關于<code>ResultSet</code>的描述資訊(前面我們已經使用<code>ResultSetMetaData</code>來擷取結果集的列數以及列名):
ResultSetMetaData方法
<code>int getColumnCount()</code>
Returns the number of columns in this ResultSet object.
<code>String getColumnLabel(int column)</code>
Gets the designated column’s suggested title for use in printouts and displays.
<code>String getColumnName(int column)</code>
Get the designated column’s name.
<code>int getColumnType(int column)</code>
Retrieves the designated column’s SQL type.
<code>String getColumnTypeName(int column)</code>
Retrieves the designated column’s database-specific type name.
<code>boolean isAutoIncrement(int column)</code>
Indicates whether the designated column is automatically numbered.
analyseResult
注: 因為擷取<code>ResultSetMetaData</code>隻能通過<code>ResultSet</code>的<code>getMetaData()</code>方法,是以使用<code>ResultSetMetaData</code>就需要一定的運作時開銷;是以如果在編碼過程中已經知道列數/列名/類型等資訊, 就沒有再用<code>ResultSetMetaData</code>了.
<code>javax.sql.RowSet</code>繼承自<code>ResultSet</code>, <code>RowSet</code>的子接口有<code>CachedRowSet</code>, <code>FilteredRowSet</code>, <code>JdbcRowSet</code>, <code>JoinRowSet</code>, <code>WebRowSet</code>, 其中隻有<code>JdbcRowSet</code>需要保持與資料庫的連接配接, 其他都是離線<code>RowSet</code>.
與<code>ResultSet</code>相比, <code>RowSet</code>預設就是可滾動/可更新/可序列化的結果集,是以可以作為JavaBean使用(比如在網絡上傳輸,用于同步兩端資料).
而對于離線<code>RowSet</code>, 程式在建立<code>RowSet</code>時已把資料從底層資料庫讀取到了記憶體,是以可以充分發揮記憶體的優勢,降低資料庫Server的負載,提高性能.
<code>RowSet</code>接口提供了如下常用方法:
<code>void setUrl(String url)</code>
Sets the URL this RowSet object will use when it uses the DriverManager to create a connection.
<code>void setUsername(String name)</code>
Sets the username property for this RowSet object to the given String.
<code>void setPassword(String password)</code>
Sets the database password for this RowSet object to the given String.
<code>void setCommand(String cmd)</code>
Sets this RowSet object’s command property to the given SQL query.
<code>void setXxx(String parameterName/int parameterIndex, X x)</code>
<code>void execute()</code>
Fills this RowSet object with data.
Java 1.7為<code>RowSet</code>提供了<code>RowSetProvider</code>與<code>RowSetFactory</code>工具, <code>RowSetProvider</code>負載建立<code>RowSetFactory</code>, <code>RowSetFactory</code>提供如下方法建立<code>RowSet</code>執行個體:
<code>CachedRowSet createCachedRowSet()</code>
Creates a new instance of a CachedRowSet.
<code>FilteredRowSet createFilteredRowSet()</code>
Creates a new instance of a FilteredRowSet.
<code>JdbcRowSet createJdbcRowSet()</code>
Creates a new instance of a JdbcRowSet.
<code>JoinRowSet createJoinRowSet()</code>
Creates a new instance of a JoinRowSet.
<code>WebRowSet createWebRowSet()</code>
Creates a new instance of a WebRowSet.
JdbcRowSetClient
可将初始化<code>RowSet</code>操作封裝成一個方法:
前面查詢得到<code>ResultSet</code>後必須立即處理,否則一旦<code>Connection</code>連接配接關閉,再去讀/寫<code>ResultSet</code>就會引發異常.而離線<code>RowSet</code>會直接将資料讀入記憶體,封裝成<code>RowSet</code>對象,<code>CachedRowSet</code>是所有離線<code>RowSet</code>的父接口,提供了如下實用方法:
<code>void populate(ResultSet data)</code>
Populates this CachedRowSet object with data from the given ResultSet object.
<code>void acceptChanges()</code>
Propagates row update, insert and delete changes made to this CachedRowSet object to the underlying data source.
<code>void acceptChanges(Connection con)</code>
Propagates all row update, insert and delete changes to the data source backing this CachedRowSet object using the specified Connection object to establish a connection to the data source.
CachedRowSetClient
由于<code>CachedRowSet</code>會将底層資料庫資料直接裝載到記憶體,是以如果SQL查詢傳回資料過大,可能會導緻記憶體溢出.為了解決這個問題,<code>CachedRowSet</code>提供了分頁功能: 一次隻裝載<code>ResultSet</code>的部分記錄,這樣可以避免<code>CachedRowSet</code>占用記憶體過大.
<code>void populate(ResultSet rs, int startRow)</code>
<code>void setPageSize(int size)</code>
Sets the CachedRowSet object’s page-size.
<code>boolean nextPage()</code>
Increments the current page of the CachedRowSet.
<code>boolean previousPage()</code>
Decrements the current page of the CachedRowSet.
事務是由一步/幾步資料庫操作序列組成的邏輯執行單元, 這些操作要麼全部執行, 要麼全部不執行.
原子性(A: Atomicity): 事務是不可再分的最小邏輯執行體;
一緻性(C: Consistency): 事務執行的結果, 必須使資料庫從一個一緻性狀态, 變為另一個一緻性狀态.
隔離性(I: Isolation): 各個事務的執行互不幹擾, 任意一個事務的内部操作對其他并發事務都是隔離的(并發執行的事務之間不能看到對方的中間狀态,不能互相影響)
持續性(D: Durability): 持續性也稱持久性(Persistence), 指事務一旦送出, 對資料所做的任何改變都要記錄到永久存儲器(通常指實體資料庫).
當事務所包含的全部操作都成功執行後送出事務,使操作永久生效,事務送出有兩種方式:
1). 顯式送出: 使用<code>commit</code>;
2). 自動送出: 執行DDL/DCL語句或程式正常退出;
當事務所包含的任意一個操作執行失敗後應該復原事務, 使該事務中所做的修改全部失效, 事務復原也有兩種方式:
1). 顯式復原: 使用<code>rollback</code>;
2). 自動復原: 系統錯誤或強行退出.
注意: 同一事務中所有的操作,都必須使用同一個<code>Connection</code>.
JDBC對事務的支援由<code>Connection</code>提供, <code>Connection</code>預設打開自動送出,即關閉事務,SQL語句一旦執行, 便會立即送出資料庫,永久生效,無法對其進行復原操作,是以需要關閉自動送出功能.
首先建立一張表用于測試
插入兩條測試資料
No Transaction
By Transaction
注意: 當<code>Connection</code>遇到一個未處理的<code>SQLException</code>時, 程式将會非正常退出,事務也會自動復原;但如果程式捕獲了該異常, 則需要在異常處理塊中顯式地復原事務.
在相同資料環境下,使用相同輸入,執行相同操作,根據不同的隔離級别,會導緻不同的結果.不同的事務隔離級别能夠解決的資料并發問題的能力是不同的, 由弱到強分為以下四級:
隔離級别
釋義
<code>read uncommitted</code>
讀未送出資料
不符合原子性,稱為”髒讀”, 在實際業務中不用.
<code>read commited</code>
讀已送出資料(Oracle)
事務執行中,讀不到另一個事務正在進行的操作,但可以讀到另一個事務結束的操作.
<code>repeatable read</code>
可重複讀(MySQL)
在事務執行中,所有資訊都來自事務開始的那一瞬間的資訊,不受其他已送出事務的影響.
<code>serializeable</code>
串行化
所有的事務按順序執行,也就沒有了沖突的可能.隔離級别最高,但事務互相等待時間太長,性能最差,少用.
MySQL設定事務隔離級别:
<code>set session transaction isolation level [read uncommitted | read committed | repeatable read |serializable]</code>
檢視目前事務隔離級别:
<code>select @@tx_isolation</code>
JDBC設定隔離級别
<code>connection.setTransactionIsolation(int level)</code>
level可為以下值:
1). <code>Connection.TRANSACTION_READ_UNCOMMITTED</code>
2). <code>Connection.TRANSACTION_READ_COMMITTED</code>
3). <code>Connection.TRANSACTION_REPEATABLE_READ</code>
4). <code>Connection.TRANSACTION_SERIALIZABLE</code>
附: 事務并發讀問題 1. 髒讀(<code>dirty read</code>):讀到另一個事務的未送出的資料,即讀取到了髒資料(<code>read commited</code>級别可解決). 2. 不可重複讀(<code>unrepeatable read</code>):對同一記錄的兩次讀取不一緻,因為另一事務對該記錄做了修改(<code>repeatable read</code>級别可解決) 3. 幻讀/虛讀(<code>phantom read</code>):對同一張表的兩次查詢不一緻,因為另一事務插入了一條記錄(<code>repeatable read</code>級别可解決) 不可重複讀和幻讀的差別: 不可重複讀是讀取到了另一事務的更新; 幻讀是讀取到了另一事務的插入(MySQL中無法測試到幻讀,效果與不可重複讀一緻);
多條SQL語句被當做同一批操作同時執行.
調用<code>Statement</code>對象的<code>addBatch(String sql)</code>方法将多條SQL語句收集起來, 然後調用<code>executeBatch()</code>同時執行.
為了讓批量操作可以正确進行, 必須把批處理視為單個事務, 如果在執行過程中失敗, 則讓事務復原到批處理開始前的狀态.
1). 對于批處理,也可以使用<code>PreparedStatement</code>,建議使用<code>Statement</code>,因為<code>PreparedStatement</code>的預編譯空間有限,當資料量過大時,可能會引起記憶體溢出. 2). MySQL預設也沒有打開批處理功能,需要在URL中設定<code>rewriteBatchedStatements=true</code>參數打開.
commons-dbutils是Apache Commons元件中的一員,提供了對JDBC的簡單封裝,以簡化JDBC程式設計;使用dbutils需要在pom.xml中添加如下依賴:
dbutils的常用類/接口如下:
<code>DbUtils</code>: 提供了一系列的實用靜态方法(如:<code>close()</code>);
<code>ResultSetHandler</code>: 提供對結果集<code>ResultSet</code>與JavaBean等的轉換;
<code>QueryRunner</code>:
<code>update()</code>(執行insert/update/delete)
<code>query()</code>(執行select)
<code>batch()</code>(批處理).
常用的update方法簽名如下:
第二種方式需要提供<code>Connection</code>, 這樣多次調用<code>update</code>可以共用一個<code>Connection</code>, 是以調用該方法可以支援事務;
QueryRunner常用的query方法簽名如下:
<code>query()</code>方法會通過sql語句和params參數查詢出<code>ResultSet</code>,然後通過<code>ResultSetHandler</code>将<code>ResultSet</code>轉換成對應的JavaBean傳回.
在上例中, 我們使用自定的<code>ResultSetHandler</code>将<code>ResultSet</code>轉換成JavaBean, 但實際上dbutils預設已經提供了很多定義良好的<code>Handler</code>實作:
<code>BeanHandler</code> : 單行處理器,将<code>ResultSet</code>轉換成JavaBean;
<code>BeanListHandler</code> : 多行處理器,将<code>ResultSet</code>轉換成<code>List<JavaBean></code>;
<code>MapHandler</code> : 單行處理器,将<code>ResultSet</code>轉換成<code>Map<String,Object></code>, 列名為鍵;
<code>MapListHandler</code> : 多行處理器,将<code>ResultSet</code>轉換成<code>List<Map<String,Object>></code>;
<code>ScalarHandler</code> : 單行單列處理器,将<code>ResultSet</code>轉換成<code>Object</code>(如儲存<code>SELECT COUNT(*) FROM t_ddl</code>).
<code>ColumnListHandler</code> : 多行單列處理器,将<code>ResultSet</code>轉換成<code>List<Object></code>(使用時需要指定某一列的名稱/編号,如<code>new ColumListHandler(“name”)</code>:表示把name列資料放到<code>List</code>中);
<code>QueryRunner</code>提供了批處理方法<code>int[] batch(String sql, Object[][] params)</code>(由于更新一行時需要<code>Object[] param</code>作為參數, 是以批處理需要指定<code>Object[][] params</code>,其中每個<code>Object[]</code>對應一條記錄):