夯基礎:涮一遍Java JDBC

JDBC是Java連接數據庫的標準,為了兼容大部分數據庫,Java提出了JDBC標準,通過這個標準,讓各個數據庫提供實現支持,這樣實現一處編碼,處處運行的Java特性。

習慣了ORM框架,卻忘記了原本的JDBC,所以我覺得有必要復習來夯實一下基礎。

0x00 JDBC 歷史

JDBC是Sun公司為了能夠讓SQL訪問統一的一套純JAVA API設計的一套接口,這種接口是遵循了微軟的ODBC API模式。其驅動實現是各家數據庫供應商編寫的,通過JDBC API可以通過驅動實現數據庫通信。

0x01 鏈接數據庫回顧

基本Web常用的數據庫都是有供Java鏈接的驅動,

三層結構

那么如何使用JDBC?
寫個Demo

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

/**
 * Created by zing on 2017/3/7.
 */
public class JDBCDemo {

  private  void testJDBC() throws ClassNotFoundException, IOException, SQLException {
        //注冊驅動
        Class.forName("com.mysql.jdbc.Driver");
        //JDBC使用類似URL的數據源描述
        String url = "jdbc:mysql://localhost:3306/demo";//忽略
        //但是我們一般不會直接這樣寫死。而是使用配置來描述數據源,用戶名,密碼
        Properties props = new Properties();
        FileInputStream propertiesFile = new FileInputStream("JDBC.properties");
        props.load(propertiesFile);
        propertiesFile.close();

        String DriverStr = props.getProperty("jdbc.Driver");
        String urlStr = props.getProperty("jdbc.url");
        String userName = props.getProperty("jdbc.name");
        String passcode = props.getProperty("jdbc.passworld");

        //打開數據庫鏈接
        Connection connection = DriverManager.getConnection(urlStr,userName,passcode);
        //執行SQL
        Statement sta = connection.createStatement();
        //executeUpdate可以返回數據庫更新的行數
        int efactRow = sta.executeUpdate("UPDATE USER SET Permition = 'admin' WHERE username = 'Zing'");
        //executeQuery可以返回一個查詢的結果集,這個集合的迭代器略有不同Iterator,沒有hasNext方法,初始是,指針在數據前,必須調用next方法才能讀取第一行數據
        ResultSet resultSet = sta.executeQuery("SELECT * FROM USER ;");
        while (resultSet.next()){
            //當前行獲取第一欄的值,具體類型需要看數據庫實現
            resultSet.getString(1);
        }
        //關閉語句
        sta.close();
        //關閉結果集
        resultSet.close();
        //關閉數據庫連接
        connection.close();

        /*
        一般情況下,關閉的操作會放在catch語句的finally塊中,catch處理數據庫異常,finally來關閉連接
        */
    }
}

上面寫的是大雜燴,一般會將獲取連接抽取成一個方法,異常也會捕獲,并在try/catch/finally中的finally塊中,關閉數據庫連接。

API用法可以看看java.sql.Connectionjava.sql.Statementjava.sql.ResultSet,這樣,基本的操作就可以了然了。

boolean execute(String sql) throws SQLException;這個方法可以執行任何SQL,返回執行是否成功

0x02 預編譯SQL

PrepareStatement,一個可以讓數據庫預編譯SQL的API。
并不是所有的SQL都是寫死的,例如:

  SELECT * FROM UserAccount Where Name =

根據名稱來查找用戶,這里的名字自然是用戶自己定義的,如果用Statement,則應該這么寫

public void findUserByName(String name){
  Statement  sta = connection.createStatement();
  String findByName = "SELECT * FROM USER WHERE Name=' "+name+" ';";
  ResultSet resultSet = sta.executeQuery(findByName);
}

如果將name交給普通用戶來輸入,則沒什么問題,但是 如果交給黑客,name他會輸入 小明' OR '1' = '1,這樣語句拼接后就會變成

SELECT * FROM USER WHERE Name=' 小明' OR '1' = '1';

這一句就會把數據庫所有的用戶全部查出來了,很嚴重的注入漏洞,基本就會被脫庫了。

所以Java JDBC定義的預編譯SQL的API。
上例子:


import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class JDBCDemo {

    private void testJDBC() {

        Connection connection = null;
        Statement sta = null;
        ResultSet resultSet = null;
        PreparedStatement preSta = null;
        try {
            connection = getConnection();
            //執行SQL
            
            String findByName = "SELECT * FROM USER WHERE Name=?;";
            preSta = connection.prepareStatement(findByName);
            preSta.setString(1,"Zing");
            resultSet = preSta.executeQuery();


        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            /*
                一般情況下,關閉的操作會放在catch語句的finally塊中,catch處理數據庫異常,finally來關閉連接
            */
            //關閉語句
            try {
                preSta.close();
                //關閉結果集
                resultSet.close();
                //關閉數據庫連接
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }


    }

    public Connection getConnection() throws ClassNotFoundException, IOException, SQLException {
        //注冊驅動
        Class.forName("com.mysql.jdbc.Driver");
        //JDBC使用類似URL的數據源描述
        //但是我們一般不會直接這樣寫死。而是使用配置來描述數據源,用戶名,密碼
        Properties props = new Properties();
        FileInputStream propertiesFile = new FileInputStream("JDBC.properties");
        props.load(propertiesFile);
        propertiesFile.close();

        String DriverStr = props.getProperty("jdbc.Driver");
        String urlStr = props.getProperty("jdbc.url");
        String userName = props.getProperty("jdbc.name");
        String passcode = props.getProperty("jdbc.passworld");
        return DriverManager.getConnection(urlStr, userName, passcode);

    }
}

順便重構了之前的代碼。
我們用 ?占位,留下可變參數的位置,后來再用setString(int parameterIndex, String x)這個方法將數據填充進SQL,這樣,如果參數含有SQL關鍵字時,就不能通過編譯,查不到結果。可以避免SQL注入。

preSta.setString(1,"Zing");表示,在第一個?處設置參數為Zing
當然參數是數字,日期時,可以使用,
void setDouble(int parameterIndex, double x) throws SQLException
setDate(int parameterIndex, java.sql.Date x) throws SQLException;

等方法,根據不同類型設置參數。

0x03 數據庫類型與轉義

數據庫類型和Java類型是有一點不一樣的,但是JDBC定義了其中的大部分類型,這里不一一列舉

MySQL部分類型對照表,有興趣可以查一查

JDBC中的轉義是為了讓Java訪問數據庫時,得到普遍的支持。一般用于下列特性

  • 時間日期的字面常亮
  • 標量函數調用
  • 存儲過程調用
  • 外連接查詢
  • LIKE子句中轉義字符

數據庫的日期轉換成Java的日期,是通過ISO8601標準衡定并相互轉換的

d表示DATE、t表示TIME、ts表示TIMESTANP

{d '2017-01-22'}
{t '19:30:29'}
{ts '2017-01-22 19:30:29.989'}

標量函數是獲取一個數值的函數,一般調用時嵌入標準函數名和參數,這個很少見到有人使用的,就不舉例了。

存儲過程,是數據庫自建的存儲方式,不同的數據庫存儲過程基本不一樣,要調用存儲過程,需要用call來進行轉義

{call PROC01(?,?)}
{call PROC02}

如果你不明白什么存儲過程,可以看看數據庫相關的資料。

外連接,就是Outter Join,借用核心卷II中的例子

SELECT * FROM {oj Books LEFT OUTER JOIN Publishers ON Books.Publish_ID = Publishers.Publish_ID }

這條語句表示查詢找不到出版商的書,相反如果是RIGHT OUTER JOIN則會查詢出沒有出版書的出版商,如果需要查到全部,則用FULL OUTER JOIN
。這里用轉義是因為有些數據庫實現不太統一。

Like子句轉義,是因為下劃線和百分號在Like條件里是特殊的含義,需要用轉義來表示

SELECT * FROM User WHERE Name LIKE %!_%ming {escape '!'}

{escape '!'}表示將!定義為轉義符號,!_表示字面量下劃線

0x04 事務

為了保證數據和業務邏輯的完整性,我們可以將一系列的SQL語句構建成一個事物,當所有語句都順利執行的時候,事務可以被提交。但是如果中途被阻礙,則數據會被回滾,將數據恢復成執行前的樣子。

首先需要關閉數據庫自動提交

connection.setAutoCommit(false);

然后根據實際業務執行多條UPDATE INSERT DELETE語句

statement.executeUpdate("SQL1");
statement.executeUpdate("SQL2");
statement.executeUpdate("SQL3");

當所有語句順利執行后,調用

connection.commit();

如果遇到異常或錯誤,則可以調用

connection.rollback();

其中JDBC支持事務保存點和批量更新
保存點:將事務的某一階段設置為保存點后,可以控制回滾時,恢復到這個保存點的數據。從而更加精確的控制回滾操作
批量更新就是將大量數據一次性存入,或修改大量數據時使用的。兩個??:

statement.executeUpdate("SQL1");
Savepoint step1 = connection.setSavepoint();
statement.executeUpdate("SQL2");
if(something==false){
  connection.rollback(step1);
}
String updateSQL = "……";
statement.addBatch(updateSQL);
while(needUpdate){
  command = "……"+"updateSQL2"
  statement.addBatch(updateSQL);
}
//批量執行
int effectRows = statement.executeBatch();

批量執行中一定不能有查詢語句,否則會拋出異常。

0x05 文件查詢和存入數據庫

不建議這么搞,數據庫存入太多大文件會導致數據庫龐大,備份和恢復的成本將增加。
在數據庫中,二進制大對象稱為Blob,字符型大對象為Clob
這里演示一下查詢和存儲

       //讀取
        PreparedStatement preparedStatement01 = connection.prepareStatement("SELECT picture FROM PictureTab WHERE picName=?;");
        preparedStatement01.setString(1,"superman");
        ResultSet rs = preparedStatement01.executeQuery();
        if(rs.next()){
            Blob picBlob = rs.getBlob(1);
            Image pic = ImageIO.read(picBlob.getBinaryStream());
        }

        //存儲
        Blob pictureBlob = connection.createBlob();
        int offset = 0;
        OutputStream outStram = pictureBlob.setBinaryStream(offset);
        ImageIO.write(pictureBlob,"PNG",outStram);
        PreparedStatement preparedStatement02 = connection.prepareStatement("INSERT INTO PictureTab VALUE (?,?);");
        preparedStatement02.setString(1, "SuperMan");
        preparedStatement02.setBlob(2,pictureBlob);
        preparedStatement02.executeUpdate();

0x06 其他一些概念

  • 元數據:數據庫的結構和表信息等描述數據庫結構和組成部分的數據
  • 多結果集:一次查詢,使用多個Select SQL語句是,會得到一個多結果集
  • 可滾動結果集:可以向前,向后查詢的結果集,之前的只能用Next向后查詢,使
Statement stat = Connection.createStatement(ResultSet.TYPE_SCROLL_INSENSTIVE , ResultSet.CONCUR_READ_ONLY )

在獲取結果集的時候,會變成一個可滾動集。

  • 獲取數據庫生成鍵值statemwnt.getGeneratedKeys();
  • 行集 RowSet接口繼承了ResultSet,但不需要長時間占用數據庫鏈接。

love&peace
我的博客:https://micorochio.github.io/
轉載請注明出處:https://micorochio.github.io/2017/03/10/basic-of-java-JDBC/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容