1. 程式人生 > >java中的JDBC詳解 附帶實現配置檔案訪問資料庫

java中的JDBC詳解 附帶實現配置檔案訪問資料庫

JDBC 是Java操作資料庫的規範,它實際上定義了一組標準的資料庫的介面,為了實現通過java操作資料庫,必須實現這些介面,不同的資料庫廠商都提供了對JDBC介面的實現,這些具體的實現被打包成一個jar包(也就是資料庫驅動),供我們在開發的時候直接使用。
JDBC API 中的主要介面:
第一: Driver介面是所有JDBC程式必須實現的介面,該介面專門提供給資料庫廠商使用,定義了驅動的樣式
第二:DriverManager 用於載入JDBC驅動並建立與資料庫的連線
有兩個重要的方法:
1 DriverManager.registerDriver(Driver driver) // 用於向DriverManager註冊給定的JDBC驅動程式
2 DriverManager.getConnection(String url, String user, String pwd) // 建立與資料庫的連線,返回表示連線的Connection物件
第三: Connection 介面

主要方法有三個
1. Connection.createStatement(); // 建立一個Statement物件,靜態sql語句查詢
2. Connection.prepareStatement(String sql); // 建立一個PreparedStatement物件,實現動態sql語句查詢
3. Connection.prepareCall(String sql); // 建立一個CallableStatement物件來呼叫資料庫儲存過程
第四:Statement介面 用於執行查詢返回查詢結果
1 Statement.execute(String sql); // 執行各種SQL語句,返回一個boolean型別值,true表示執行的SQL語句具備查詢結果,可通過Statement.getResultSet()方法獲取
2 Statement.executeUpdate(String sql); // 執行SQL中的insert/update/delete語句,返回一個int值,表示受影響的記錄的數目
3 Statement.executeQuery(String sql); // 執行SQL中的select語句,返回一個表示查詢結果的ResultSet物件
第五:ResultSet介面
用於查詢結果的操作
1 ResultSet.next(); // 將遊標由當前位置移動到下一行
2 ResultSet.getString(String columnName); // 獲取指定欄位的String型別值
3 ResultSet.getString(int columnIndex); // 獲取指定索引的String型別值
4 ResuleSet.previous(); // 將遊標由當前位置移動到上一行

JDBC操作資料庫的一般步驟
註冊驅動 (只做一次)
建立連線(Connection)
建立執行SQL的語句(Statement)
執行語句並處理執行結果(ResultSet)
釋放資源

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// 這裡測試的是mysql資料庫名是test; 表是 user; 表的欄位 有 id ,name ,age ,salary
public class JDBCTest {
    public static void main(String[] args) {
        // 第一步: 首先註冊驅動, 驅動一般只會註冊一次
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            System.out.println("找不到驅動程式類,載入驅動失敗");
            e.printStackTrace();
        }
        // 第二步:建立連線 Connect, 設定url ,使用者名稱, 密碼
        // url格式:JDBC:子協議:子名稱//主機名:埠/資料庫名?屬性名=屬性值&…
        // 注意的是url中一定不要加多餘的空格,否則會出錯, useSSL=false是為了解決身份驗證時出現的警告的問題
        // String url = "jdbc:mysql://localhost:3306/test?" + "user=root&password=wsw011152&useUnicode=true&characterEncoding=UTF-8&useSSL=false";
        String url = "jdbc:mysql://localhost:3306/test?useSSL=false";
        String name = "root";
        String psw = "root";
        Connection connect = null;
        try {
            connect = DriverManager.getConnection(url, name, psw);
            // connect = DriverManager.getConnection(url);
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            System.out.println("資料庫連線失敗");
            e.printStackTrace();
        }
        // 第三步: 建立一個 Statement ,一般建議使用 PreparedStatement
        // 1、執行靜態SQL語句。通常通過Statement例項實現。
        // 2、執行動態SQL語句。通常通過PreparedStatement例項實現。
        // 3、執行資料庫儲存過程。通常通過CallableStatement例項實現。
        // String sql = "select * from user where id = ?";
        String sql = "select * from user where id = ?";
        try {
            PreparedStatement ps = connect.prepareStatement(sql);
            ps.setInt(1, 1); // 設定引數
            // 第四步: 執行語句,獲得一個結果集,處理獲得的結果
            ResultSet result = ps.executeQuery();
            while (result.next()) {
                System.out.println(result.getInt("id"));
                System.out.println(result.getString("name"));
                System.out.println(result.getInt("age"));
                System.out.println(result.getString("salary"));
            }
            // 第五步: 關閉資源
            result.close();
            ps.close();
            connect.close();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

下面講解一下 JDBC為什麼建議使用PreparedStatement而不是Statement
首先java提供了三種方式來執行sql語句
CallableStatement;主要用於儲存過程的查詢,
Statement;主要用於通用查詢,適合只對資料庫進行一次性存取的時候,使用它會為每一條sql語句生成一個執行計劃,即使這兩條語句只有引數的不同而已。
PreparedStatement :主要用於引數化查詢,會傳遞引數,反覆查詢

PreparedStatement 相對比與Statement的優勢在下面的幾點:

1. 使用PreparedStatement,資料庫系統會對sql語句進行預編譯(需要JDBC驅動支援預編譯SQL查詢),進行預處理,這條預處理的sql查詢語句可以在將來的查詢中被重用,節省了建立執行計劃的時間,減少了系統的開銷,因此它比Statement的查詢速度更快。
比如使用 Statement進行下面兩句的查詢,則會生成兩個執行計劃,1000個查詢就會生成1000個執行計劃,生成執行計劃十分消耗資源,
select colume from table where colume=1;
select colume from table where colume=2;
但是使用PreparedStatement, 則系統會對sql語句進行預編譯處理,只會生成1個執行計劃,1000個這樣的查詢不會再產生執行計劃,這個執行計劃會被下面同樣的查詢語句所重用,大大提高了速度。
select colume from table where colume=?;
PreparedStatement .setInt(1, 1);
PreparedStatement .setInt(1, 2);
2. 可以寫動態引數化的查詢,用PreparedStatement你可以寫帶引數的sql查詢語句,通過使用相同的sql語句和不同的引數值來做查詢

3. PreparedStatement可以防止SQL注入式攻擊,更加安全
使用PreparedStatement的引數化的查詢可以阻止大部分的SQL注入攻擊。
第一:在使用引數化查詢的情況下,資料庫系統不會將引數的內容視為SQL指令的一部分來處理,而是在資料庫完成SQL指令的編譯後,才套用引數執行,因此就算引數中含有破壞性的指令,也不會被資料庫所執行
第二:在組合SQL字串的時候,先對所傳入的引數做字元取代(將單引號字元取代為連續2個單引號字元,因為連續2個單引號字元在SQL資料庫中會視為字元中的一個單引號字元

strSQL = “SELECT * FROM users WHERE name = ‘” + userName + “’;”
傳入字串:
userName = ” 1’ OR 1=1 “

把userName做字元替換後變成:
userName = ” 1” OR 1=1”

最後生成的SQL查詢語句為:
strSQL = “SELECT * FROM users WHERE name = ‘1” OR 1=1’
這樣資料庫就會去系統查詢name為“1′ ‘ OR 1=1”的記錄,而避免了SQL注入。

PreparedStatement的侷限性:
為了防止SQL注入攻擊,PreparedStatement不允許一個佔位符(?)有多個值,在執行有*IN子句查詢的時候這個問題變得棘手起來*。下面這個SQL查詢使用PreparedStatement就不會返回任何結果:
SELECT * FROM loan WHERE loan_type IN (?)
preparedSatement.setString(1, “‘personal loan’, ‘home loan’, ‘gold loan’”);
解決方式:

// 將in 裡面的變數首先儲存成一個數組
String[] in_datas=new String[]{"1", "2", "3"}; 
StringBuffer buffer = new StringBuffer();
for(int i=0;i<in_datas.length-1;i++){ 
    buffer.append("?,"); 
}
buffer.append("?");
// 在in字句 裡面使用N個 ?。 然後為每一個?賦值
pst = conn.prepareStatement("select id,name from B where id in ( "+buffer.toString()+" )");  //  buffer.toString() ="?,?,?,?...?"
for(int i=0;i<in_datas.length;i++){  
    pst.set(i, in_datas[i]);  
}
// 解決 like 查詢的方式
   String expr = "select * from  table where url like ?";  
   pstmt = con.prepareStatement(expr);  
   String a="a";  
   pstmt.setString(1, "%"+a+"%");//自動新增單引號 (包裝後的引數)  
   pstmt.execute();  

SQL注入:
在SQL注入攻擊裡,惡意使用者通過SQL元資料繫結輸入,比如:某個網站的登入驗證SQL查詢程式碼為:
strSQL = “SELECT * FROM users WHERE name = ‘” + userName + “’ and pw = ‘”+ passWord +”’;”
惡意填入:
userName = “1’ OR ‘1’=’1”;
passWord = “1’ OR ‘1’=’1”;
那麼最終SQL語句變成了:
strSQL = “SELECT * FROM users WHERE name = ‘1’ OR ‘1’=’1’ and pw = ‘1’ OR ‘1’=’1’;”
因為WHERE條件恆為真,這就相當於執行:
strSQL = “SELECT * FROM users;”
因此可以達到無賬號密碼亦可登入網站。如果惡意使用者要是更壞一點,使用者填入:
strSQL = “SELECT * FROM users;”
SQL語句變成了:
strSQL = “SELECT * FROM users WHERE name = ‘any_value’ and pw = ”; DROP TABLE users”
這樣一來,雖然沒有登入,但是資料表都被刪除了。

防止SQL注入的幾種方式:
第一種:使用預編譯語句,通過繫結變數的方式使得SQL語句的語義不會發生變化,避免使用動態拼接的sql語句進行查詢’,傳進來的引數只會被當成?代表的變數來使用,這樣就無法改變SQL語句的結構。
具體的實現包括: 使用儲存過程 或者 使用PreparedStatement實現執行引數化的查詢。
第二種: 永遠不要相信使用者的輸入,在web頁面就進行傳入資料的檢驗,使用正則表示式等方式過濾特殊的符號,進行校驗, 但是一旦出現了新的SQL攻擊方式,正則表示式庫也需要隨之修改
第三種: 做好資料庫帳號許可權管理,對於使用者,儘量不要賦予其管理員的許可權進行登入訪問。
第四種: 嚴格加密處理使用者的機密資訊

下面我自己實現了一個讀取資料庫配置檔案進行資料庫的連線,可以連線不同的資料庫的一個實現。

第一步:配置檔案 database.properties ,存放了各種資料庫的配置資訊

# mysql database driver
MySQLdriverClass=com.mysql.jdbc.Driver
MySQLurl=jdbc:mysql://127.0.0.1/test?useSSL=false
MySQLusername=root
MySQLpassword=wsw011152
# sqlserver 2008 database driver
SqlServerdriverClass=net.sourceforge.jtds.jdbc.Driver
SqlServerurl=jdbc:jtds:sqlserver://localhost:1433;DatabaseName=test;
SqlServerusername=sa
SqlServerpassword=sa
# Oracle  database driver
OracledriverClass=oracle.jdbc.driver.OracleDriver
Oracleurl=jdbc:oracle:thin:@localhost:1521:test
Oracleusername=scott
Oraclerpassword=tiger

第二步: 建立一個工具類用來讀取這個配置檔案中的資訊

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;

public class PropertiesUtil {
   private static Properties prop;
   static{
        try {
            prop = new Properties();
            prop.load(new FileInputStream("database.properties"));
        } catch (FileNotFoundException e) {
            System.out.println("載入配置檔案失敗");
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 根據資料庫名獲取其驅動
    public static String getDriverProperties(String key){
        return prop.getProperty(key + "driverClass");
    }
    // 根據資料庫名獲取url
    public static String getUrlProperties(String key){
        return prop.getProperty(key + "url");
    }
    // 獲取使用者名稱
    public static String getUsernameProperties(String key){
        return prop.getProperty(key + "username");
    }
    // 獲取密碼
    public static String getPasswordProperties(String key){
        return prop.getProperty(key + "password");
    }
}

第三步:連線資料庫

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import com.mysql.jdbc.PreparedStatement;

public class JDBCByProperties {
    public static void main(String[] args) {

        // 只需要將不同的驅動放在配置檔案中,就可以實現連線不同的資料庫
        try {
            // 1.註冊驅動   
            Class.forName(PropertiesUtil.getDriverProperties("MySQL"));
            // 2. 連線資料庫
            Connection conn = DriverManager.getConnection(
                    PropertiesUtil.getUrlProperties("MySQL"),       PropertiesUtil.getUsernameProperties("MySQL"),              PropertiesUtil.getPasswordProperties("MySQL"));
            // 3. 建立執行的sql語句, 建議使用PreparedStatement
            String sql = "select * from user where id = ?";
            PreparedStatement prep = null;
            prep = (PreparedStatement) conn.prepareStatement(sql);
            prep.setInt(1, 1);
            // 4. 查詢sql 語句, 返回一個結果集
            ResultSet result = prep.executeQuery();
            // 5. 處理結果集, 釋放資源
            while (result.next()) {
                System.out.println(result.getInt("id"));
                System.out.println(result.getString("name"));
                System.out.println(result.getInt("age"));
                System.out.println(result.getString("salary"));
            }
            conn.close();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            System.out.println("驅動註冊未成功");
            e.printStackTrace();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}