Java JDBC編程

JDBC基礎

JDBC的全稱是Java Database Connectivity,即Java數據庫鏈接,它是一種能夠執行SQL語句的Java API。程序可經過JDBC API鏈接到關係數據庫,並使用結構化查詢語言(SQL,數據庫標準的查詢語言)來完成對數據庫的查詢、更新java

與其餘數據庫編程環境相比,JDBC爲數據庫開發提供了標準的API,使用JDBC開發的數據庫應用能夠跨平臺運行,並且還能夠跨數據庫(若是所有使用標準的SQL語句)。也就是說若是使用JDBC開發一個數據庫應用,則該應用既能夠在Windows操做系統上運行,又能夠在Unix等其餘操做系統上運行,既可使用MySQL數據庫,又可使用Oracle等其餘的數據庫,應用程序不須要作任何的修改mysql

JDBC簡介

Java語言的各類跨平臺特性,都採用類似的結構。由於他們都須要讓相同的程序在不一樣的平臺上運行,因此須要中間的轉換程序(爲了實現Java程序的跨平臺,Java爲不一樣的操做系統提供了不一樣的Java虛擬機)。一樣,爲了JDBC程序能夠跨平臺,也須要不一樣的數據庫廠商提供相應的驅動程序正則表達式

clipboard.png

Sun提供的JDBC能夠完成如下三個基本操做:sql

  • 創建與數據庫的連接數據庫

  • 執行SQL語句編程

  • 得到SQL語句的執行結果設計模式

JDBC驅動程序

數據庫驅動程序是JDBC程序和數據庫之間的轉換層,數據庫驅動程序負責將JDBC調用映射成特定的數據庫調用緩存

clipboard.png

ODB,Open Database Connectivity,即開放數據庫連接。ODBC和JDBC很像,嚴格來講,應該是JDBC模仿了ODBC是設計。ODBC也容許應用程序經過一種通用的API訪問不一樣的數據庫管理系統,從而使得基於ODBC的應用程序能夠在不一樣的數據庫之間切換。一樣,ODBC也須要各數據庫廠商提供相應的驅動程序,而ODBC負責管理這些驅動程序安全

JDBC驅動一般有以下4種類型服務器

  • JDBC + ODBC橋的方式

  • 直接將JDBC API隱射成數據庫特定的客戶端API。這種驅動包含特定數據庫的本地代碼,用於訪問特定數據庫的客戶端

  • 支持三層結構的JDBC訪問方式,主要用於Applet階段,經過Applet訪問數據庫

  • 純java的,直接與數據庫實例交互,。這種驅動是智能型的,它知道數據庫使用的底層協議,是目前最流行的JDBC驅動

一般建議選擇第4種JDBC驅動,這種驅動避開了本地代碼,減小了應用開發的複雜性,也減小了產生衝突和出錯的可能。若是對性能有嚴格的要求,則能夠考慮使用第2種JDBC驅動,但使用這種驅動,則勢必增長編碼和維護的困難

JDBC比ODBC多了以下幾個優點

  • ODBC更復雜,ODBC中有幾個命令須要配置不少複雜的選項,而JDBC則採用簡單、直觀的方式來管理數據庫鏈接

  • JDBC比ODBC安全性更高,更易部署

JDBC的經典用法

JDBC 4.2經常使用接口和類簡介

JAVA8關於JDBC4.2的新增功能:
DriverManager:用於管理JDBC驅動的服務類。程序中使用該類的主要功能是獲取Connection對象,該類包含以下方法

  • public static synchronized Connection getConnection(String url, String user, String password) throws SQLException:該方法得到url對應數據庫的鏈接

Connection

Connection:表明數據庫鏈接對象,每一個Connection表明一個物理鏈接會話。要想訪問數據庫,必須先獲得數據庫鏈接。該接口的經常使用方法以下:

  • Statement createStatement() throws SQLException:該方法返回一個Statement對象

  • PreparedStatement prepareStatement(String sql) throws SQLException:該方法返回預編譯的Statement對象,即將SQL語句提交到數據庫進行預編譯

  • CallableStatement prepareCall(String sql) throws SQLException:該方法返回CallableStatement對象,該對象用於調用存儲過程

上面三個方法都返回用於執行SQL語句的Statement對象,PreparedStatement、CallableStatement是Statement的子類,只有得到了Statement以後才能夠執行SQL語句

除此以外,Connection還有以下幾個用於控制事務的方法:

  • Savepoint setSavepoint() throws SQLException:建立一個保存點

  • Savepoint setSavepoint(String name):以指定名字來建立一個保存點

  • void setTransactionIsolation(int level):設置事務的隔離級別

  • void rollback():回滾事務

  • void rollback(Savepoint savepoint):將事務回滾到指定的保存點

  • void setAutoCommit(boolean autoCommit):關閉自動提交,打開事務

  • void commit() throws SQLException:提交事務

Java7位Connection新增了setSchema(String schema)、getSchema()兩個方法,這兩個方法用於控制該Connection訪問的數據庫Schema。還爲Connection新增了setNetworkTimeout(Executor executor, int milliseconds)、getNetworkTimeout()兩個方法來控制數據庫鏈接的超時行爲

Statement

Statement:用於執行SQL語句的工具接口。該對象既能夠執行DDL、DCL語句,也能夠用於執行DML語句,還能夠用於執行SQL查詢。當執行SQL查詢時,返回查詢到的結果集。它的經常使用方法以下:

  • ResultSet executeQuery(String sql) throws SQLException:該方法用於執行查詢語句,並返回查詢結果對應ResultSet對象。該方法只能用於執行查詢語句

  • int executeUpdate(String sql) throws SQLException:該方法用於執行DML語句,並返回受影響的行數;該方法也可用於執行DDL語句,執行DDL語句將返回0

  • boolean execute(String sql) throws SQLException:該方法能夠執行任何SQL語句。若是執行後第一個結果爲ResultSet對象,則返回true;若是執行後第一個結果爲受影響的行數或沒有任何結果,則返回false

Java7爲Statement新增了closeOnCompletion()方法,若是Statement執行了此方法,則當全部依賴於該Statement的ResultSet關閉時,該Statement會自動關閉。Java7還爲Statement提供了一個isCloseOnCompletion()方法,該方法用於判斷該Statement是否打開了「closeOnCompletion」

PreparedStatement

PreparedStatement:預編譯的Statement對象,PreparedStatement是Statement的子接口,它容許數據庫預編譯SQL語句(這些SQL語句一般帶有參數),之後每次只改變sql命令的參數,避免數據庫每次都須要編譯SQL語句,無需再傳入SQL語句,所以性能更好。使用PreparedStatement執行SQL語句時,無須再傳入SQL語句,只要爲預編譯的SQL語句傳入參數值便可

PreparedStatement一樣有executeQuery()、executeUpdate()和execute()方法,只是這三個方法無須接收SQL字符串,由於PreparedStatement對象已預編譯了SQL命令,只要爲這些方法傳入參數便可。因此它比Statement多了以下方法:

  • void setXxx(int parameterIndex, Xxx value):該方法根據傳入參數值的類型不一樣,須要使用不一樣的方法。傳入的值根據索引傳給SQL語句中指定位置的參數

ResultSet

ResultSet:結果集對象。該對象包含訪問查詢結果的方法,ResultSet能夠經過列索引或列名得到列數據。它包含了以下經常使用方法來移動記錄指針

  • void close():釋放ResultSet對象

  • boolean absolute(int row):將結果集的記錄指針移動到第row行,若是row是負數,則移動到倒數第row行,若是移動後的記錄指針指向一條有效記錄,則該方法返回true

  • void beforeFisrt():將ResultSet的記錄指針定位到首行以前,這是ResultSet結果集記錄指針的初始狀態——記錄指針的起始位置位於第一行以前。

  • boolean first():將ResultSet的記錄指針定位到首行。若是移動後的記錄指針指向一條有效記錄,則該方法返回true

  • boolean previous():將ResultSet的記錄指針定位到上一行,若是移動後的記錄指針指向一條有效記錄,則該方法返回true

  • boolean next():將結果集的記錄指針定位到下一行,若是移動後的記錄指針指向一條有效的記錄,則該方法返回true

  • boolean last():將結果集的記錄指針定位到最後一行,若是移動後的記錄指針指向一條有效的記錄,則該方法返回true

  • void afterLast():將ResultSet的記錄指針定位到最後一行以後

當把記錄指針移動到指定行以後,ResultSet可經過getXxx(int columnIndex)或getXxx(String columnLabel)方法來獲取當前行、指定列的值,前者根據列索引獲取值,後者根據列名獲取值

JDBC編程步驟

1 加載數據庫驅動

一般使用Class類的forName()靜態方法來加載驅動

// 加載驅動,driverClass就是數據庫驅動類所對應的字符串
Class.forName(driverClass);
// 加載MySQL的驅動
Class.forName("com.mysql.jdbc.Driver");
// 加載Oracle的驅動
Class.forName("oracle.jabc.driver.OracleDriver");

2 經過DriverManager獲取數據庫的連接

// 獲取數據庫鏈接
DriverManager.getConnection(String url, Stirng user, String pass)

當使用DriverManager來獲取連接,一般須要傳入三個參數:數據庫URL、登陸數據庫的用戶名和密碼

數據庫URL一般遵循以下寫法:jdbc是固定的;subprotocol指定鏈接到特定數據庫的驅動;other和stuff也是不固定的

jdbc:subprotocol:other stuff

3 經過Connection對象建立Statement(或者PreparedStatement)對象

  • createStatement():建立基本的Statement對象

  • prepareStatement(String sql):根據傳入的SQL語句建立預編譯的Statement對象

  • prepareCall(String sql):根據傳入的SQL語句建立CallableStatement對象

4 使用Statement執行SQL語句

  • execute():能夠執行任何SQL語句,但比較麻煩

  • executeUpdate():主要用於執行DML和DDL語句。執行DML返回受影響的SQL語句行數,執行DDL返回0

  • executeQuery():只能執行查詢語句,執行後返回表明查詢結果的ResultSet對象

5 操做結果集

若是執行的SQL語句是查詢語句,則執行結果將返回一個ResultSet對象,該對象裏保存了SQL語句查詢的結果。程序能夠經過操做該ResultSet對象來取出查詢結果。ResultSet對象主要提供了以下兩類方法

  • next()、previous()、first()、last()、beforeFrist()、afterLast()、absolute()等移動指針的方法

  • getXxx()方法獲取記錄指針指向行,特定列的值。既可以使用列名做爲參數可讀性更好、使用索引做爲參數性能更好

6 回收數據庫資源

包括關閉ResultSet、Statement和Connection等資源

import java.sql.*;
public class ConnMySql
{
    public static void main(String[] args) throws Exception
    {
        // 1.加載驅動,使用反射的知識,如今記住這麼寫。
        Class.forName("com.mysql.jdbc.Driver");
        try(
            // 2.使用DriverManager獲取數據庫鏈接,
            // 其中返回的Connection就表明了Java程序和數據庫的鏈接
            // 不一樣數據庫的URL寫法須要查驅動文檔知道,用戶名、密碼由DBA分配
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/select_test"
                , "root" , "32147");
            // 3.使用Connection來建立一個Statment對象
            Statement stmt = conn.createStatement();
            // 4.執行SQL語句
            /*
            Statement有三種執行sql語句的方法:
            1 execute 可執行任何SQL語句。- 返回一個boolean值,
              若是執行後第一個結果是ResultSet,則返回true,不然返回false
            2 executeQuery 執行Select語句 - 返回查詢到的結果集
            3 executeUpdate 用於執行DML語句。- 返回一個整數,
              表明被SQL語句影響的記錄條數
            */
            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) + "\t"
                    + rs.getString(2) + "\t"
                    + rs.getString(3) + "\t"
                    + rs.getString(4));
            }
        }
    }
}

執行SQL語句的方式

使用Java8新增的executeLargeUpdate方法執行DDL和DML語句

如下程序示範了使用executeUpdate()方法(MySQL驅動暫不支持executeLargeUpdate()方法)建立數據表。該示例並無直接把數據庫鏈接信息寫在程序裏,而是使用一個mysql.ini文件(properties文件)來保存數據庫鏈接信息,這是比較成熟的作法——當須要把應用程序從開發環境移植到生產環境時,無須修改源代碼,只需修改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
    {
        // 加載驅動
        Class.forName(driver);
        try(
        // 獲取數據庫鏈接
        Connection conn = DriverManager.getConnection(url , user , pass);
        // 使用Connection來建立一個Statment對象
        Statement stmt = conn.createStatement())
        {
            // 執行DDL,建立數據表
            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("-----建表成功-----");
    }
}

下面程序執行一條insert語句,這條insert語句會向剛剛創建的jdbc_test數據表中插入幾條記錄。由於使用了帶子查詢的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
    {
        // 加載驅動
        Class.forName(driver);
        try(
            // 獲取數據庫鏈接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection來建立一個Statment對象
            Statement stmt = conn.createStatement())
        {
            // 執行DML,返回受影響的記錄條數
            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("--系統中共有" + result + "條記錄受影響--");
    }
}

使用execute方法執行SQL語句

Statement的execute()方法幾乎能夠執行任何SQL語句,但它執行SQL語句時比較麻煩,一般沒有必要使用execute()方法來執行SQL語句,使用executeQuery()或executeUpdate()方法更簡單。但若是不清楚SQL語句的類型,則只能使用execute()方法來執行該SQL語句

  • getResult():獲取該Statement執行查詢語句所返回的ResultSet對象

  • getUpdateCount():獲取該Statement()執行DML語句所影響的記錄行數

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
    {
        // 加載驅動
        Class.forName(driver);
        try(
            // 獲取數據庫鏈接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection來建立一個Statement對象
            Statement stmt = conn.createStatement()
            )
        {
            // 執行SQL,返回boolean值表示是否包含ResultSet
            boolean hasResultSet = stmt.execute(sql);
            // 若是執行後有ResultSet結果集
            if (hasResultSet)
            {
                try(
                    // 獲取結果集
                    ResultSet rs = stmt.getResultSet()
                    )
                {
                    // ResultSetMetaData是用於分析結果集的元數據接口
                    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) + "\t");
                        }
                        System.out.print("\n");
                    }
                }
            }
            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("------執行刪除表的DDL語句-----");
        es.executeSql("drop table if exists my_test");
        System.out.println("------執行建表的DDL語句-----");
        es.executeSql("create table my_test"
            + "(test_id int auto_increment primary key, "
            + "test_name varchar(255))");
        System.out.println("------執行插入數據的DML語句-----");
        es.executeSql("insert into my_test(test_name) "
            + "select student_name from student_table");
        System.out.println("------執行查詢數據的查詢語句-----");
        es.executeSql("select * from my_test");
    }
}

使用PreparedStatement執行SQL語句

建立PreparedStatement對象使用Connection的preparedStatement()方法,該方法須要傳入一個SQL字符串,該字符串能夠包含符參數

// 建立一個PreparedStatement對象
pstmt = conn.preparedStatement("insert into student_table values(null,?,1)");

PreparedStatement也提供了execute()、executeUpdate()、executeQuery()三個方法來執行SQL語句,不過這三個方法無須參數,由於PreparedStatement提供了一系列的setXxx(int index, Xxx value)方法來傳入參數值

若是程序很清楚PreparedStatement預編譯SQL語句中各參數的類型,則使用相應的setXxx()方法來傳入參數便可;若是程序不清楚編譯SQL語句中各參數的類型,則可使用setObject()方法來傳入參數,由PreparedStatement來負責類型轉換

下面程序示範使用Statement和PreparedStatement分別插入100條記錄的對比。使用Statement須要傳入100條SQL語句,但使用PreparedStatement則只需傳入1條預編譯的SQL語句,而後100次爲該PreparedStatement的參數設值便可

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");
        // 加載驅動
        Class.forName(driver);
    }
    public void insertUseStatement()throws Exception
    {
        long start = System.currentTimeMillis();
        try(
            // 獲取數據庫鏈接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection來建立一個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(
            // 獲取數據庫鏈接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection來建立一個PreparedStatement對象
            PreparedStatement pstmt = conn.prepareStatement(
                "insert into student_table values(null,?,1)"))

        {
            // 100次爲PreparedStatement的參數設值,就能夠插入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注入的結果。下面登陸窗口包含兩個文本框,一個用於輸入用戶名,一個用於輸入密碼,系統根據用戶輸入與jdbc_test表裏的記錄進行匹配,若是找到相應記錄則提示登錄成功

public class LoginFrame
{
    private final String PROP_FILE = "mysql.ini";
    private String driver;
    // url是數據庫的服務地址
    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");
        // 加載驅動
        Class.forName(driver);
        // 爲登陸按鈕添加事件監聽器
        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)
    {
        // 執行查詢的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來執行驗證,而不是直接使用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預編譯SQL語句,性能更好

  • PreparedStatement無須「拼接」SQL字符串,編程更簡單

  • 使用PreparedStatement可防止SQL注入,安全性更好

使用PreparedStatement執行帶佔位符參數的SQL語句時,SQL語句中的佔位符參數只能代替普通值,不要使用佔位符參數代替表名、列名等數據庫對象,更不要用佔位符參數來代替SQL語句中的insert、select等關鍵字

使用CallableStatement調用存儲過程

MySQL數據庫中建立一個簡單的存儲過程的SQL語句

delimiter //
create procedure add_pro(a int, b int ,out sum int)
begin
set sum = a + b;
end;
//

上面的SQL語句將MySQL的語句結束符改成雙斜線(//),這樣就能夠在建立存儲過程當中使用分號做爲分隔符(默認使用分號做爲語句結束符)程序建立了名爲add_pro的存儲過程,該存儲過程包含三個參數:a、b是傳入參數,而sum使用out修飾,是傳出參數

調用存儲過程使用CallableStatement,經過Connection的prepareCall()方法來建立CallableStatement對象,建立該對象時須要傳入調用存儲過程的SQL語句。調用存儲過程的SQL語句老是這種格式:{call 過程名(?,?,?...)},其中的問號做爲存儲過程參數的佔位符

// 使用Connection來建立一個CallableStatement對象
cstmt = conn.prepareCall("{call add_pro(?,?,?)}");

存儲過程的參數既有傳入參數,也有傳出參數,所謂傳入參數就是Java程序必須爲這些參數傳入值,能夠經過CallableStatement的setXxx()方法爲傳入參數設置值;所謂傳出參數就是Java程序能夠經過該參數獲取存儲過程裏的值,CallableStatement須要調用registerOutParameter()方法來註冊該參數

// 註冊CallableStatement的第三個參數是int類型
cstmt.registerOutParameter(3, Types.INTEGER);

以後調用CallableStatement的execute()方法來執行存儲過程,執行結束後經過CallableStatement對象的getXxx(int index)方法來獲取指定傳出參數的值

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
    {
        // 加載驅動
        Class.forName(driver);
        try(
            // 獲取數據庫鏈接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection來建立一個CallableStatment對象
            CallableStatement cstmt = conn.prepareCall(
                "{call add_pro(?,?,?)}"))
        {
            cstmt.setInt(1, 4);
            cstmt.setInt(2, 5);
            // 註冊CallableStatement的第三個參數是int類型
            cstmt.registerOutParameter(3, Types.INTEGER);
            // 執行存儲過程
            cstmt.execute();
            // 獲取,並輸出存儲過程傳出參數的值。
            System.out.println("執行結果是: " + cstmt.getInt(3));
        }
    }
    public static void main(String[] args) throws Exception
    {
        CallableStatementTest ct = new CallableStatementTest();
        ct.initParam("mysql.ini");
        ct.callProcedure();
    }
}

管理結果集

JDBC使用ResultSet來封裝執行查詢獲得的查詢結果,而後經過移動ResultSet的記錄指針來取出結果集的內容。除此以外,JDBC還容許經過ResultSet來更新記錄,並提供了ResultSetMetaData來得到ResultSet對象的相關信息

可滾動、可更新的結果集

可滾動的結果集:可使用absolute()、previous()、afterLast()等方法只有移動指針記錄的ResultSet

以默認形式打開的ResultSet是不可更新的,若是但願建立可更新的ResultSet,則必須在Connection在建立Statement或PreparedStatement時,傳入額外的參數:

  • resultSetType:控制ResultSet的類型,該參數能夠取以下三個值

    • ResultSet.TYPE_FORWARD_ONLY:該常量控制記錄指針只能向前移動

    • ResultSet.TYPE_SCROLL_INSENSITIVE:該常量控制記錄指針自由移動(可滾動結果集),但底層的數據改變不影響結果集ResultSet的內容

    • ResultSet.TYPE_SCROLL_SENSITIVE:該常量控制記錄指針自由移動,但底層數據的影響會改變結果集ResultSet的內容

  • resultSetConcurrency:控制ResultSet的併發類型,該參數能夠接收以下兩個值

    • ResultSet.CONCUR_READ_ONLY:該常量表示ResultSet是隻讀併發模式(默認)

    • ResultSet.CONCUR_UPDATABLE:該常量表示ResultSet是更新併發模式

// 使用Connection建立一個PreparedStatement對象
// 傳入控制結果集可滾動、可更新的參數
pstmt = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

可更新的結果集還須要知足以下兩個條件:

  • 全部數據都應該來自一個表

  • 選出的數據集必須包含主列鍵

經過該PreparedStatement建立的ResultSet就是可滾動的、可更新的,程序可調用的updateXxx(int columnIndex, Xxx value)方法來修改記錄指針所指記錄、特定列的值,最後調用ResultSet的updateRow()方法來提交修改

Java8爲ResultSet添加了updateObject(String columnLabel, Object x, SQLType targetSqlType)和updateObject(int columnIndex, Object x, SQLType targetSqlType)兩個默認方法,這兩個方法能夠直接用Object來修改記錄指針所指記錄、特定列的值,其中SQLType用於指定該數據列的類型

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
    {
        // 加載驅動
        Class.forName(driver);
        try(
            // 獲取數據庫鏈接
            Connection conn = DriverManager.getConnection(url , user , pass);
            // 使用Connection來建立一個PreparedStatement對象
            // 傳入控制結果集可滾動,可更新的參數。
            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) + "\t"
                    + rs.getString(2) + "\t" + rs.getString(3));
                // 修改記錄指針全部記錄、第2列的值
                rs.updateString(2 , "學生名" + 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類型數據

Blob——Binary long Object——二進制長對象,Blob列一般用於存儲大文件,典型的Blob內容是一張圖片或者一個聲音文件,因爲他們的特殊性,必須使用特殊的方式來存儲。使用Blob列能夠把照片聲音等文件的二進制數據保存在數據庫裏,並能夠從數據庫裏恢復指定文件

若是須要將圖片插入數據庫,顯然不能經過普通的SQL語句來完成,由於有一個關鍵的問題,Blob常量沒法表示,因此將Blob數據插入數據庫須要使用PreparedStatement。該對象有一個方法:setBinaryStream(int parameterIndex, InputStream x)該方法能夠爲指定參數傳入二進制流,從而能夠實現將Blob數據保存到數據庫的功能

當須要從ResultSet裏取出Blob數據時,能夠調用ResultSet的getBlob(int columnIndex)方法,該方法將返回一個Blob對象Blob對象提供了getBinaryStream()方法獲取該獲取該Blob數據的輸入流,也可使用Blob對象的getBytes()方法直接取出該Blob對象封裝的二進制數據

爲了把圖片放入數據庫,本程序先使用以下SQL語句來創建一個數據表:

create table img_table 
{ 
img_id int auto_increment primary key, 
img_name varchar(255), 
# 建立一個mediumblob類型的數據列,用於保存圖片數據 
ima_data mediumblob 
};

img_data列使用mediumblob類型,而不是blob類型。由於MySQL數據庫裏的blob類型最多隻能存儲64kb的內容,因此使用mediumblob類型,該類型能夠存儲16M內容

// ---------將指定圖片放入數據庫---------
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)
        )
    {
        // 設置圖片名參數
        insert.setString(1, imageName);
        // 設置二進制流參數
        insert.setBinaryStream(2, is, (int)f.length());
        int affect = insert.executeUpdate();
        if (affect == 1)
        {
            // 從新更新ListModel,將會讓JList顯示最新的圖片列表
            fillListModel();
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}
// ---------根據圖片ID來顯示圖片----------
public void showImage(int id)throws SQLException
{
    // 設置參數
    query.setInt(1, id);
    try(
        // 執行查詢
        ResultSet rs = query.executeQuery()
        )
    {
        if (rs.next())
        {
            // 取出Blob列
            Blob imgBlob = rs.getBlob(1);
            // 取出Blob列裏的數據
            ImageIcon icon=new ImageIcon(imgBlob.getBytes(1L, (int)imgBlob.length()));
            imageLabel.setIcon(icon);
        }
    }
}

使用resultsetmetaData分析結果集

描述ResultSet信息的數據——ResultSetMetaData

MetaData即元數據,即描述其它數據的數據,所以ResultSetMetaData封裝了描述ResultSet對象的數據

ResultSet的getMetaData()方法返回該ResultSet對應的ResultSetMetaData對象,就可經過ResultSetMetaData提供的大量方法返回ResultSet的描述信息

  • int getColumnCount():返回該ResultSet的列數量

  • String getColumnName(int column):返回指定索引的列名

  • int getColumnType(int column):返回指定索引的列類型

Java7的RowSet1.1

RowSet接口繼承了ResultSet接口,RowSet接口下包含JdbcRowSet、CachedRowSet、FilteredRowSet、JoinRowSet和WebRowSet經常使用子接口。除了JdbcRowSet須要保持與數據庫的鏈接以外,其他4個子接口都是離線的RowSet,無須保持與數據庫的鏈接

RowSet默認是一個可滾動,可更新,可序列化的結果集,並且它做爲JavaBeans,能夠方便地在網絡間傳輸,用於兩端的數據同步。對於離線RowSet而言,程序在建立RowSet時已把數據從底層數據庫讀取到了內存,所以能夠充分利用計算機的內存,從而下降數據庫服務器的負載,提供程序性能

RowSet規範的接口類圖
clipboard.png

Java7新增的RowSetFactory與RowSet

RowSet接口中定義的經常使用方法:

  • setUrl(String url):設置該RowSet要訪問的數據庫的URL

  • setUsername(String name):設置該RowSet要訪問的數據庫的用戶名

  • setPassword(String password):設置該RowSet要訪問的數據庫的密碼

  • setCommand(String sql):設置使用該sql語句的查詢結果來裝填該RowSet

  • execute():執行查詢

  • populate(ResultSet rs):讓該RowSet直接包裝給定的ResultSet對象

Java7新增了RowSetProvider類和RowSetFactory接口,其中RowSetProvider負載建立RowSetFactory,而RowSetFactory則提供了以下方法來建立RowSet實例:

  • CachedRowSet createCachedRowSet():建立一個默認的CachedRowSet

  • FilteredRowSet createFilteredRowSet():建立一個默認的FilteredRowSet

  • JoinRowSet createJoinRowSet():建立一個默認的JoinRowSet

  • WebRowSet createWebRowSet():建立一個默認的WebRowSet

  • JdbcRowSet createJdbcRowSet():建立一個默認的JdbcRowSet

提供使用RowSetFactory,就能夠把應用程序與RowSet實現類分離開,避免直接使用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
    {
        // 加載驅動
        Class.forName(driver);
        // 使用RowSetProvider建立RowSetFactory
        RowSetFactory factory = RowSetProvider.newFactory();
        try(
            // 使用RowSetFactory建立默認的JdbcRowSet實例
            JdbcRowSet jdbcRs = factory.createJdbcRowSet()
            )
        {
            // 設置必要的鏈接信息
            jdbcRs.setUrl(url);
            jdbcRs.setUsername(user);
            jdbcRs.setPassword(pass);
            // 設置SQL查詢語句
            jdbcRs.setCommand(sql);
            // 執行查詢
            jdbcRs.execute();
            jdbcRs.afterLast();
            // 向前滾動結果集
            while (jdbcRs.previous())
            {
                System.out.println(jdbcRs.getString(1)
                    + "\t" + jdbcRs.getString(2)
                    + "\t" + 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會直接將底層數據讀入內存中,封裝成RowSet對象,而RowSet對象則徹底能夠當成Java Bean來使用。所以不只安全,並且編程簡單。CachedRowSet是全部離線RowSet的父接口

以下程序①處調用了RowSet的populate(ResultSet rs)方法來包裝給的的ResultSet,接着關閉了ResultSet、Statement、Connection等數據庫資源。若是程序直接返回ResultSet,那麼這個Result沒法使用——由於底層的Connection已經關閉;但程序返回的是CachedRowSet,一個離線RowSet,所以程序依然能夠讀取、修改RowSet中的記錄

爲了將程序對離線RowSet所作的修改同步到底層數據庫,程序在調用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
    {
        // 加載驅動
        Class.forName(driver);
        // 獲取數據庫鏈接
        Connection conn = DriverManager.getConnection(url, user, pass);
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql);
        // 使用RowSetProvider建立RowSetFactory
        RowSetFactory factory = RowSetProvider.newFactory();
        // 建立默認的CachedRowSet實例
        CachedRowSet cachedRs = factory.createCachedRowSet();
        // 使用ResultSet裝填RowSet
        cachedRs.populate(rs);    // ①
        // 關閉資源
        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();
        // 向前滾動結果集
        while (rs.previous())
        {
            System.out.println(rs.getString(1)
                + "\t" + rs.getString(2)
                + "\t" + rs.getString(3));
            if (rs.getInt("student_id") == 3)
            {
                // 修改指定記錄行
                rs.updateString("student_name", "安倍晴明");
                rs.updateRow();
            }
        }
        // 從新獲取數據庫鏈接
        Connection conn = DriverManager.getConnection(url, user, pass);
        conn.setAutoCommit(false);
        // 把對RowSet所作的修改同步到底層數據庫
        rs.acceptChanges(conn);
    }
}

離線RowSet的查詢分頁

CachedRowSet的分頁功能:一次只裝載ResultSet裏的某幾條記錄,這樣就能夠避免CachedRowSet佔用內存過大的問題

CachedRowSet提供了以下方法來控制分頁:

  • populate(ResultSet rs, int startRow):使用給定的Result裝填RowSet,從ResultSet的第startRow條記錄但是裝填

  • setPageSize(int pageSize):設置CachedRowSet每次返回記錄條數

  • 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
    {
        // 加載驅動
        Class.forName(driver);
        try(
            // 獲取數據庫鏈接
            Connection conn = DriverManager.getConnection(url , user , pass);
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(sql)
            )
        {
            // 使用RowSetProvider建立RowSetFactory
            RowSetFactory factory = RowSetProvider.newFactory();
            // 建立默認的CachedRowSet實例
            CachedRowSet cachedRs = factory.createCachedRowSet();
            // 設置每頁顯示pageSize條記錄
            cachedRs.setPageSize(pageSize);
            // 使用ResultSet裝填RowSet,設置從第幾條記錄開始
            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);   // ①
        // 向後滾動結果集
        while (rs.next())
        {
            System.out.println(rs.getString(1)
                + "\t" + rs.getString(2)
                + "\t" + rs.getString(3));
        }
    }

    }

事務處理

事務的概念和MySQL事務支持

事務是由一步或幾步數據庫操做序列組成的邏輯執行單元,這系列操做要麼所有執行,要麼所有放棄執行。

事務具備四個特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持續性(Durability)。這四個特性也簡稱ACID性

  • 原子性:事務是應用中最小的執行單位,就如原子是天然界最小顆粒,具備不可再分的特徵同樣。事務是應用中不可再分的最小邏輯執行體

  • 一致性:事務執行的結果,必須使數據庫從一個一致性狀態,變到另外一個一致性狀態。當數據庫中只包含事務成功提交的結果時,數據庫處於一致性狀態。一致性是經過原子性來保證的

  • 隔離性:各個事務的執行互不干擾,任意一個事務的內部操做對其餘併發的事務,都是隔離的。也就是說:併發執行的事務之間不能看到對方的中間狀態,併發執行的事務之間不能相互影響

  • 持續性:持續性也稱爲持久性,指事務一旦提交,對數據所作的任何改變,都要記錄到永久存儲器中,一般是保存進物理數據庫

數據庫的事務有下列語句組成:

  • 一組DML(Data Manipulate Language,即數據操做語言),通過這組DML修改後數據將保持較好的一致性

  • 一個DDL(Data Definition Language,即數據定義語言)語句

  • 一個DCL(Data control Language,即數據控制語言)語句

DDL和DCL語句最多隻能有一個,由於DDL和DCL語句都會致使事務當即提交

當事務所包含的所有數據庫操做都成功執行後,應該提交(commit)事務,使這些修改永久生效。事務提交有兩種方式:顯式提交和自動提交

  • 顯式提交:使用commit

  • 自動提交:執行DDL或DCL,或者程序正常退出

當事務所包含的任意一個數據庫操做執行失敗後,應該回滾(rollback)事務,使該事務中所作的修改所有失效。事務回滾的方式有兩種:顯式回滾和自動回滾

  • 顯式回滾:使用rollback關鍵字

  • 隱式回滾:系統錯誤或者強行退出

MySQL默認關閉事務(即打開自動提交事務),在默認狀況下,在MySQL控制檯輸入一條DML語句,該語句會馬上保存到數據庫中。可使用下面的語句來開啓事務(即關閉自動提交事務):

// 關閉自動提交,即開啓事務
set autocommit = 0; 
// 開啓自動提交,即關閉事務
set autocommit = 1;

調用 set autocommit = 0; 命令後,該命令行窗口裏的全部DML語句都不會當即生效,上一個事務結束後第一條DML語句將開始一個新的事務,然後續執行的全部SQL語句都處於該事務中。除非使用commit提交事務、或正常退出、或運行DDL語句或DCL語句致使事務隱式提交。也可使用rollback回滾來結束事務,使用rollback結束事務將會使此事務中的DML語句所作的修改所有失效

一個MySQL命令行窗口表明一個Session,在該窗口裏設置set autocommit = 0; 至關於關閉了該鏈接Session的自動提交,對其餘鏈接不會有任何影響

若是不想使得整個Session都打開事務,可使用start transaction或begin這兩個命令,它們都表示臨時性地開始一次事務。處於start transaction或begin後的DML語句不會當即生效,除非使用commit顯式提交事務,或者使用DDL語句或DCL語句隱式提交事務

以下SQL將不會對數據庫有任何影響

# 臨時開始事務
begin;
# 向player_table表插入3條數據
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;    # ①
# 回滾事務
rollback;
# 再次查詢
select * from player_table;    # ②

經過使用savepoint設置事務的中間點可讓事務回滾到指定中間點,而不是回滾所有事務。普通的提交、回滾都會結束當前事務,但回滾到指定中間點由於依然處於事務之中,因此不會結束當前事務

savepoint a;
# 回滾到指定中間點
rollback to a;

JDBC的事務支持

JDBC鏈接的事務支持由Connection提供,Connection默認打開自動提交,即關閉事務,在這種狀況下,每條SQL語句一旦執行,便會當即提交到數據庫,永久生效,沒法對其進行回滾操做

能夠調用Connection的setAutoCommit()方法來關閉自動提交,開啓事務

//關閉自動提交,開啓事務
conn.setAutoCommit(false);

一旦事務開始以後,程序能夠像日常同樣建立Statement對象,建立了Statement對象以後,能夠執行任意多條DML語句,這些SQL語句雖然被執行了,但這些SQL語句所做的修改不會生效,由於事務尚未結束。若是全部SQL語句執行成功,程序能夠調用Connection的commit方法來提交事務

//提交事務
conn.commit();

若是任意一條SQL語句執行失敗,應該用Connection的rollback來回滾事務

//回滾事務
conn.rollback();

當Connection遇到一個未處理的SQLException異常時,系統將會非正常退出,事務也會自動回滾。但若是程序捕獲了該異常,則須要在異常處理塊中顯式地回滾事務

Connection設置中間點的方法:

  • Savepoint setSavepoint():在當前事務中建立一個未命名的中間點,並返回表明該中間點的Savepoint對象

  • Savepoint setSavepoint(String name):在當前事務中建立一個具備指定名稱的中間點,並返回表明該中間點的Savepoint對象

一般來講,設置中間點時沒有太大的必要指定名稱,由於Connection回滾到指定中間點時,並非根據名字回滾的,而是根據中間點對象回滾的。Connection提供了rollback(Savepoint savepoint)方法來回滾到指定中間點

Java8加強的批量更新

批量更新必須獲得底層數據庫的支持,可經過調用DatabaseMetaData的supportsBatchUpdates()方法來查看底層數據庫是否支持批量更新

批量更新須要先建立一個Statement對象,而後利用該對象的addBatch()方法將多條SQL語句同時收集起來,最後調用Statement對象的executeBatch()(或executeLargeBatch())方法同時執行這些SQL語句

批量更新代碼:

Statement stmt = conn.createStatement();  
//使用Statement同時收集多個SQL語句  
stmt.addBatch(sql1);  
stmt.addBatch(sql2);  
stmt.addBatch(sql3);  
...  
//同時執行全部的SQL語句  
stmt.executeBatch();

爲了讓批量操做能夠正確地處理錯誤,必須把批量執行的操做視爲單個事務,若是批量更新在執行過程當中失敗,則讓事務回滾到批量操做開始以前的狀態。程序應該在開始批量操做以前先關閉自動提交,而後開始收集更新語句,當批量操做結束以後,提交事務,並恢復以前的自動提交模式

//保存當前的自動的提交模式  
Boolean autoCommit = conn.getAutoCommit();  
//關閉自動提交  
conn.setAutoCommit(false);  
Statement stmt = conn.createStatement();  
//使用Statement同時收集多條SQL語句  
stmt.addBatch(sql1);  
stmt.addBatch(sql2);  
stmt.addBatch(sql3);  
...  
//同時提交全部的SQL語句  
stmt.executeBatch();  
//提交修改  
conn.commit();  
//恢復原有的自動提交模式  
conn.setAutoCommit(autoCommit);

分析數據庫信息

使用DatabaseMetaData分析數據庫信息

JDBC提供了DatabaseMetaData來封裝數據庫鏈接對應數據庫的信息,經過Connection提供的getMetaData()方法就能夠獲取數據庫對應的DatabaseMetaData對象

DatabaseMetaData接口一般由驅動程序供應商提供實現,其目的是讓用戶瞭解底層數據庫的相關信息。使用該接口的目的是發現如何處理底層數據庫,尤爲是對於試圖與多個數據庫一塊兒使用的應用程序

許多DatabaseMetaData方法以ResultSet對象的形式返回查詢信息,而後使用ResultSet的常規方法(如getString()和getInt())便可從這些ResultSet對象中獲取數據。若是查詢的信息不可用,則將返回一個空ResultSet對象

DatabaseMetaData的不少方法都須要傳入一個xxxPattern模式字符串,這裏的xxxPattern不是正則表達式,而是SQL裏的模式字符串,即用百分號(%)表明任意多個字符,使用下劃線(_)表明一個字符。在一般狀況下,若是把該模式字符串的參數值設置爲null,即代表該參數不做爲過濾條件

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{  
        //加載驅動  
        Class.forName(driver);  
        try(  
            //獲取數據庫鏈接  
            Connection conn = DriverManager.getConnection(url, user, pass);  
        ){  
            //獲取DatabaseMetaData對象  
            DatabaseMetaData dbmd = conn.getMetaData();  
            //獲取MySQL支持的全部表類型  
            ResultSet rs = dbmd.getTableTypes();  
            System.out.println("---MySQL支持的表類型信息---");  
            printResultSet(rs);  
            //獲取當前數據庫的所有數據表  
            rs = dbmd.getTables(null, null, "%", new String[]{"TABLE"});  
            System.out.println("---當前數據庫裏的數據表信息---");  
            printResultSet(rs);  
            //獲取student_table表的主鍵  
            rs = dbmd.getPrimaryKeys(null, null, "student_table");  
            System.out.println("---student_table表的主鍵信息---");  
            printResultSet(rs);  
            //獲取當前數據庫的所有存儲過程  
            rs = dbmd.getProcedures(null, null, "%");  
            System.out.println("---當前數據庫裏的存儲過程信息---");  
            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表的所有數據列  
            rs = dbmd.getColumns(null, null, "student_table", "%");  
            System.out.println("---student_table表的所有數據列---");  
            printResultSet(rs);  
        }  
    }  
    public void printResultSet(ResultSet rs) throws SQLException{  
        ResultSetMetaData rsmd = rs.getMetaData();  
        //打印ResultSet的全部列標題  
        for(int i = 0; i < rsmd.getColumnCount(); i++){  
            System.out.print(rsmd.getColumnName(i + 1) + "\t");  
        }  
        System.out.print("\n");  
        //打印ResultSet的所有數據  
        while(rs.next()){  
            for(int i = 0; i < rsmd.getColumnCount(); i ++){  
                System.out.print(rs.getString(i + 1) + "\t");  
            }  
            System.out.print("\n");  
        }  
        rs.close();  
    }  
    public static void main(String args[]) throws Exception{  
        DatabaseMetaDataTest dmdt = new DatabaseMetaDataTest();  
        dmdt.initParam("sql.ini");  
        dmdt.info();  
    }  
}

使用系統表分析數據庫信息

如已肯定應用程序所使用的數據庫系統,則能夠經過數據庫的系統表來分析數據庫信息。系統表又稱爲數據字典,數據字典的數據一般由數據庫系統負責維護,用戶一般只能查詢數據字典,而不能修改數據字典的內容

MySQL數據庫使用information_schema數據庫來保存系統表,在該數據庫裏包含了大量系統表,經常使用系統表的簡單介紹以下:

  • tables:存放數據庫裏全部數據表的信息

  • schemata:存放數據庫裏全部數據庫(與MySQL的Schema對應)的信息

  • views:存放數據庫裏全部視圖的信息

  • columns:存放數據庫裏全部列的信息

  • triggers:存放數據庫裏全部觸發器的信息

  • routines:存放數據庫裏全部存儲過程和函數的信息

  • key_column_usage:存放數據庫裏全部具備約束的鍵信息

  • table_constraints:存放數據庫裏所有約束的表信息

  • statistics:存放數據庫裏所有索引的信息

select * from schemata;  
select * from tables where table_schema = 'select_test';  
select * from columns where table_name = 'student_table';

選擇合適的分析方式

一般而言,若是使用DatabaseMetaData來分析數據庫信息,則具備更好的跨數據庫特性,應用程序能夠作到數據庫無關;但可能沒法準確獲取數據庫的更多細節

使用數據庫系統表來分析數據庫系統信息會更加準確,但使用系統表也有壞處——這種方式與底層數據庫耦合嚴重,採用這種方式將會致使程序只能運行在特定的數據庫之上

一般來講,若是須要得到數據庫信息,包括該數據庫驅動提供了哪些功能,則應該利用DatabaseMetaData來了解該數據庫支持哪些功能。徹底可能出現這樣一種狀況:對於底層數據庫支持的功能,但數據庫驅動沒有提供該功能,程序仍是不能使用該功能。使用DatabaseMetaData則不會出現這種問題

若是須要純粹地分析數據庫的靜態對象,例如分析數據庫系統裏包含多少數據庫、數據表、視圖、索引等信息,則利用系統會更加合適

使用鏈接池管理鏈接

數據庫鏈接的創建以及關閉是極耗費系統資源的操做,在多層結構的應用環境中,這種資源的耗費對系統性能影響尤其明顯。經過前面介紹的方式(經過DriverManager獲取鏈接)獲取的數據庫鏈接,一個數據庫鏈接對象均對應一個物理數據庫鏈接,每次操做都打開一個物理鏈接,使用完後當即關閉鏈接。頻繁地打開、關閉鏈接將形成系統性能低下

數據庫鏈接池的解決方案是: 當應用程序啓動時,系統主動創建足夠的數據庫鏈接,並將這些鏈接組成一個鏈接池。每次應用程序請求數據庫鏈接時,無須從新打開鏈接,而是從鏈接池中取出已有的鏈接使用,使用完後再也不關閉數據庫鏈接,而是直接將鏈接歸還給鏈接池。經過使用鏈接池,將大大提供程序的運行效率

對於共享資源的惡狀況,有一個通用的設計模式:資源池(Resource Pool),用於解決資源的頻繁請求、釋放所形成的性能降低。爲了解決數據庫鏈接的頻繁請求,JDBC2.0規範引入了數據庫鏈接池技術

數據庫鏈接池是Connection對象的工廠。數據庫鏈接池的經常使用參數以下:

  • 數據庫的初始鏈接數

  • 鏈接池的最大鏈接數

  • 鏈接池的最小鏈接數

  • 鏈接池每次增長的容量

JDBC的數據庫鏈接池使用javax.sql.DataSource來表示,DataSource只是一個接口,該接口一般由商用服務器(如WebLogic、WeSphere)等提供實現,也有一些開源組織提供實現(如DBCP和C3P0)

DataSource一般被稱爲數據源,它包含鏈接池和鏈接池管理兩個部分,但習慣上咱們也常常把DataSource稱爲鏈接池

DBCP數據源

DBCP是Apache軟件基金組織下的開源鏈接池實現,該鏈接池依賴該組織下的另外一個開源系統:common-pool。若是須要使用該鏈接池實現,則應在系統中增長以下兩個jar文件:

  • commons-dbcp.jar:鏈接池的實現

  • commons-pool.jar:鏈接池實現的依賴庫

Tomcat的鏈接池正是採用該鏈接池實現的。數據庫鏈接池既能夠與應用服務器整合使用,也能夠由應用程序獨立使用。下面的代碼片斷示範了使用DBCP來得到數據庫鏈接的方式:

// 建立數據源對象  
BasicDataSource ds = new BasicDataSource();  
// 設置鏈接池所需的驅動  
ds.setDriverClassName("com.mysql.jdbc.Driver");  
// 設置鏈接數據庫的URL  
ds.setUrl("jdbc:mysql://localhost:3306/javaee");  
// 設置鏈接數據庫的用戶名  
ds.setUsername("root");  
/ /設置鏈接數據庫的密碼  
ds.setPassword("pass");  
// 設置鏈接池的初始鏈接數  
ds.setInitialSize(5);  
// 設置鏈接池最多可有多少個活動鏈接數  
ds.setMaxActive(20);  
// 設置鏈接池中最少有2個空閒的鏈接  
ds.setMinIdle(2);

數據源和數據庫鏈接不一樣,數據源無須建立多個,它是產生數據庫鏈接的工廠,所以整個應用只須要一個數據源便可。也就是說,對於一個應用,上面代碼只要執行一次便可。建議把上面程序中的ds設置成static成員變量,而且在應用開始時當即初始化數據源對象,程序中全部須要獲取數據庫鏈接的地方直接訪問該ds對象,並獲取數據庫鏈接便可

// 經過數據源獲取數據庫鏈接  
Connection conn = ds.getConnection();  
// 當數據庫訪問結束後,釋放數據庫鏈接  
conn.close();    // 上面代碼並無關閉據庫的物理鏈接
                 // 僅僅把數據庫鏈接釋放,歸還給鏈接池
                 // 讓其餘客戶端可使用該鏈接

C3P0數據源

C3P0數據源性能更勝一籌,Hibernate推薦使用該鏈接池。C3P0鏈接池不只能夠自動清理再也不使用的Connection,還能夠自動清理Statement和ResultSet

若是須要使用C3P0鏈接池,則應在系統中增長以下JAR文件

  • c3p0-0.9.1.2.jar: C3P0鏈接池的實現

    // 建立鏈接池實例  
    ComboPooledDataSource ds = new ComboPooledDataSource();  
    // 設置鏈接池鏈接數據庫所需的驅動  
    ds.setDriverClass("com.mysql.jdbc.Driver");  
    // 設置鏈接數據庫的url  
    ds.setJdbcUrl("jdbc:mysql://localhost:3306/javaee");  
    // 設置鏈接數據庫的用戶名、密碼  
    ds.setUser("root");  
    ds.setPassword("root");  
    // 設置鏈接池的最大鏈接數  
    ds.setMaxPoolSize(40);  
    // 設置鏈接池的最小鏈接數  
    ds.setMinPoolSize(2);  
    // 設置鏈接池的初始鏈接數  
    ds.setInitialPoolSize(10);  
    // 設置鏈接池的緩存Statment的最大數  
    ds.setMaxStatements(180);  
    //得到數據庫鏈接  
    Connection conn = ds.getConnection();
相關文章
相關標籤/搜索