[Spring 深度解析]第5章 Spring之DAO

Young丶發表於2020-12-23

第5章 ◄Spring之DAO►

​ 在上一章節中,我們瞭解了Spring框架中的AOP模組,這一章節我們開始學習Spring框架中的DAO模組。
本章主要涉及的知識點:

​ ● JDBC基本用法:Statement、PreparedStatement、CallableStatement的使用。
​ ● JDBC高階用法:批處理、事務處理。
​ ● Spring DAO模組:JdbcDaoSupport、MappingSqlQuery等物件的使用。
​ ● Spring事務管理:TransactionProxyFactoryBean、DataSourceTransactionManager的配置與使用。

5.1 JDBC詳解

​ 在瞭解Spring的DAO模組時需要有一定的資料庫基礎,Java語言與資料庫連線使用的是JDBC,所以有必要學習一下JDBC的內容。

5.1.1 JDBC介紹

​ JDBC(Java DB Connection,Java資料庫連線)是一種可用於執行SQL語句的Java API(Application Programming Interface,應用程式設計介面)。它由一些Java語言編寫的類和介面組成。JDBC為資料庫應用開發人員和資料庫前臺工具開發人員提供了一種標準的應用程式設計介面,使開發人員可以用純Java語言編寫完整的資料庫應用程式。JDBC代表Java資料庫連線。它是一個軟體層,允許開發者在Java中編寫客戶端/伺服器應用。
​ 通過使用JDBC,開發人員可以很方便地將SQL語句傳送給幾乎任何一種資料庫。也就是說,開發人員可以不必寫一個程式訪問Sybase,寫另一個程式訪問Oracle,再寫一個程式訪問Microsoft的SQL Server。用JDBC寫的程式能夠自動將SQL語句傳送給相應的資料庫管理系統(DBMS)。不但如此,使用Java編寫的應用程式可以在任何支援Java的平臺上執行,不必在不同的平臺上編寫不同的應用。Java和JDBC的結合可以讓開發人員在開發資料庫應用時真正實現“Write Once,Run Everywhere!”。

5.1.2 操作步驟

​ JDBC可以連線不同的資料庫,不同的資料庫也可以被不同的工具連線,但在連線時基本都是固定的幾個步驟。

1.驅動引入

​ JDBC是對外開放的介面,資料庫提供商實現了這些介面,這些介面的組合就是驅動。資料庫有好多種,例如MySQL、Oracle等,需要註冊不同的驅動來操作對應的資料庫,註冊驅動也得要有驅動才是,所以首先要將驅動引入專案。

2.註冊驅動

​ 引入驅動之後應用程式也不知道是用的什麼資料庫,只是把驅動下載了下來放到專案中,所以得註冊一下才知道是誰,註冊之後會返回對應的驅動管理物件。就和入職一樣,你到公司了但不報到,那也不知道來了沒來,報到了才會有針對個人的流程。

3.建立連線

​ 資料庫和應用程式是分隔開來的,資料庫可能存放在遠端,那怎麼和資料庫搭上呢?這就需要連線了。

4.執行操作

​ 連線上之後要幹什麼呢,不能一直連著不幹事情啊,這也是資源的一種浪費,所以連線之後執行資料庫的操作增、刪、改、查等。

5.返回結果

​ 增、刪、改、查操作結束之後,總要有一個結果,不然怎麼知道成功與否,查詢的話會返回查詢的資料,增加、刪除、修改會返回影響的行數。

6.釋放資源

​ 把結果也返回了,但不能老連著資料庫,這樣佔用資源,建立的物件也沒有釋放,還佔空間,所以用完了就把它關掉。

5.1.3 Statement的使用

​ Statement是Java執行資料庫操作的一個重要介面,用於在已經建立資料庫連線的基礎上,向資料庫傳送要執行的SQL語句。Statement物件用於執行靜態SQL語句,並返回它所生成結果的物件。
​ 預設情況下,同一時間每個Statement物件只能開啟一個ResultSet物件。因此,如果讀取一個ResultSet物件與讀取另一個交叉,那麼這兩個物件必須是由不同的Statement物件生成的。如果存在某個語句開啟的當前ResultSet物件,那麼Statement介面中的所有執行方法都會隱式關閉它。
​ 在使用Statement之前先進行資料準備,這裡在本地MySQL中建立了一個資料庫daodemodb和一張表t_user,並在表中增加了幾條資料用來測試。

/* 資料庫建立 */
CREATE DATABASE `daodemodb` /*!40100 DEFAULT CHARACTER SET utf8 */;
/*建立測試表*/
CREATE TABLE `t_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `money` decimal(10,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*資料準備*/
insert into t_user(name,age,money)
values
('張三','24',666.66),
('李四','25',888.88),
('王二','26',999.99),
('小明','27',555.55),
('小趙','28',333.33)

​ 按照上面的操作步驟,需要引入驅動,這裡使用pom.xml引入mysql的jdbc驅動。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.12</version>
</dependency>

​ 然後就是在程式碼中依次註冊驅動、建立連線、執行操作、返回結果、釋放資源步驟,下面程式碼演示的就是這個過程,從t_user表中查詢資料並列印到日誌中。

import java.sql.*;

public class StatementDemo {

  public static void main(String[] args) throws SQLException {
    // TODO Auto-generated method stub
    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    try {
      // 註冊驅動
      DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
      // 通過註冊的驅動獲得連線物件Connection
      conn =
          DriverManager.getConnection(
              "jdbc:mysql://127.0.0.1:3306/daodemodb?useUnicode=true&characterEncoding=UTF-8"
                  + "&serverTimezone=UTC&useSSL=false",
              "root",
              "123456");
      // 通過Statement物件執行操作 返回結果ResultSet
      stmt = conn.createStatement();
      // 返回結果
      rs = stmt.executeQuery("select * from t_user");
      while (rs.next()) {
        System.out.println(
            "姓名:"
                + rs.getString("name")
                + "  年齡:"
                + rs.getInt("age")
                + "  餘額:"
                + rs.getDouble("money"));
      }
    } catch (SQLException e) {
      System.out.println(e.getMessage());
      e.printStackTrace();
    } finally {
      // 釋放資源
      if (conn != null) {
        conn.close();
      }
      if (stmt != null) {
        stmt.close();
      }
      if (rs != null) {
        rs.close();
      }
    }
  }
}

輸出結果:

姓名:張三  年齡:24  餘額:666.66
姓名:李四  年齡:25  餘額:888.88
姓名:王二  年齡:26  餘額:999.99
姓名:小明  年齡:27  餘額:555.55
姓名:小趙  年齡:28  餘額:333.33

5.1.4 使用PreparedStatement返回自增主鍵

​ 實際上有三種Statement物件,它們都作為在給定連線上執行SQL語句的包容器:Statement、PreparedStatement(從Statement繼承而來)和CallableStatement(從PreparedStatement繼承而來)。它們都專用於傳送特定型別的SQL語句:Statement物件用於執行不帶引數的簡單SQL語句;PreparedStatement物件用於執行帶或不帶IN引數的預編譯SQL語句;CallableStatement物件用於執行對資料庫已存在的儲存過程的呼叫。Statement介面提供了執行語句和獲取結果的基本方法。PreparedStatement介面新增了處理IN引數的方法;而CallableStatement新增了處理OUT引數的方法。
​ 這裡向t_user表中插入一條資料,並返回自增主鍵id的值。在準備SQL時使用?來做引數的佔位符,在例項化PreparedStatement物件之後對SQL進行傳參,這樣也能防止注入式攻擊。

package spring.tutorial.chapter5.jdbc;

import java.sql.*;

public class PreparedStatementDemo {
  public static void main(String[] args) throws SQLException {
    // TODO Auto-generated method stub
    Connection conn = null;
    PreparedStatement prestmt = null;
    ResultSet rs = null;
    try {
      // 註冊驅動
      DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
      // 通過註冊的驅動獲得連線物件Connection
      conn =
          DriverManager.getConnection(
              "jdbc:mysql://127.0.0.1:3306/daodemodb?useUnicode=true&characterEncoding=UTF-8"
                  + "&serverTimezone=UTC&useSSL=false",
              "root",
              "123456");
      // PreparedStatement物件
      String sql = "insert into t_user (name,age,money) values(?,?,?)";
      prestmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
      prestmt.setString(1, "小李");
      prestmt.setInt(2, 25);
      prestmt.setDouble(3, 222.22);
      // 返回結果
      int result = prestmt.executeUpdate();
      if (result > 0) {
        System.out.println("新增成功");
        rs = prestmt.getGeneratedKeys();
        while (rs.next()) {
          System.out.println("生成的主鍵ID為:" + rs.getInt(1));
        }
      }
    } catch (SQLException e) {
      System.out.println(e.getMessage());
      e.printStackTrace();
    } finally {
      // 釋放資源
      if (conn != null) {
        conn.close();
      }
      if (prestmt != null) {
        prestmt.close();
      }
      if (rs != null) {
        rs.close();
      }
    }
  }
}

輸出結果:

新增成功
生成的主鍵ID為:6

5.1.5 使用CallableStatement呼叫儲存過程

​ 在使用資料庫的過程中,可能會呼叫儲存過程,可以使用CallableStatement來呼叫儲存過程。
​ ● 呼叫儲存函式:{?= call <procedure-name>[(<arg1>,<arg2>, ...)]}
​ ● 呼叫儲存過程:{call <procedure-name>[(<arg1>,<arg2>, ...)]}
​ 通過CallableStatement物件的registerOutParameter()方法註冊Out引數。通過CallableStatement物件的setXxx()方法設定IN或In out引數,若想將引數設為null,可以使用setNull()。如果所呼叫的是帶返回引數的儲存過程,還需要通過CallableStatement物件的getXxx()獲取輸出引數。
​ 在資料庫中建立了儲存過程p_selectUserByAge,根據使用者年齡查詢使用者,儲存過程一個傳入引數age,一個傳出引數count,引數count存放根據年齡查詢的使用者個數。

CREATE  PROCEDURE `p_selectUserByAge`(age int, out count int)
BEGIN
  set count=(select count(1) from t_user t where t.age =age);
  select * from  t_user t where t.age =age;
END

​ 在下面的程式碼中先使用Connection的prepareCall方法來例項化CallableStatement,再使用CallableStatement物件的registerOutParameter方法設定傳入引數,最後執行儲存過程返回結果。

package spring.tutorial.chapter5.jdbc;

import java.sql.*;

public class CallableStatementDemo {

  public static void main(String[] args) throws SQLException {
    // TODO Auto-generated method stub
    Connection conn = null;
    CallableStatement callstmt = null;
    ResultSet rs = null;
    try {
      // 註冊驅動
      DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
      // 通過註冊的驅動獲得連線物件Connection
      conn =
          DriverManager.getConnection(
              "jdbc:mysql://127.0.0.1:3306/daodemodb?useUnicode=true&characterEncoding=UTF-8"
                  + "&serverTimezone=UTC&useSSL=false",
              "root",
              "123456");
      // CallableStatement物件
      callstmt = conn.prepareCall("{call p_selectUserByAge(?,?)}");
      callstmt.setInt(1, 25);
      // 設定傳入引數
      callstmt.registerOutParameter(2, Types.INTEGER);
      rs = callstmt.executeQuery();
      while (rs.next()) {
        System.out.println(
            "姓名:"
                + rs.getString("name")
                + "  年齡:"
                + rs.getInt("age")
                + "  出生日期:"
                + rs.getDouble("money"));
      }
      System.out.println("儲存過程返回值:" + callstmt.getInt(2));
    } catch (SQLException e) {
      System.out.println(e.getMessage());
      e.printStackTrace();
    } finally {
      // 釋放資源
      if (conn != null) {
        conn.close();
      }
      if (callstmt != null) {
        callstmt.close();
      }
      if (rs != null) {
        rs.close();
      }
    }
  }
}

輸出結果:

姓名:李四  年齡:25  出生日期:888.88
姓名:小李  年齡:25  出生日期:222.22
儲存過程返回值:2

5.1.6 批處理

​ 在實際開發中往往會批量執行SQL,Statement和PreparedStatement都支援批量執行SQL語句,但這些SQL必須是Insert、Update、Delete這種執行後返回一個Int型別的數,表示影響的行數。Statement和PreparedStatement都是通過addBatch()方法新增一條SQL語句,通過executeBatch()方法批量執行SQL語句,返回一個Int型別的陣列,表示各SQL的返回值,這樣就減少了注入驅動、建立連線這些步驟,提升了效率。首先看一下Statement批處理的例子:

package spring.tutorial.chapter5.jdbc;

import java.sql.*;

public class StatementSQLBatch {
  public static void main(String[] args) throws SQLException {
    // TODO Auto-generated method stub
    Connection conn = null;

    ResultSet rs = null;
    Statement stmt = null;
    try {
      // 註冊驅動
      DriverManager.registerDriver(new com.mysql.jdbc.Driver());
      // 通過註冊的驅動獲得連線物件Connection
      conn =
          DriverManager.getConnection(
              "jdbc:mysql://127.0.0.1:3306/daodemodb?useUnicode=true&characterEncoding=UTF-8"
                  + "&serverTimezone=UTC&useSSL=false",
              "root",
              "123456");

      stmt = conn.createStatement();
      for (int i = 0; i < 2; i++) {
        String sql =
            "insert into t_user (name,age,money) values('StatementTest"
                + i
                + "',"
                + 25
                + i
                + ",222.22)";
        stmt.addBatch(sql);
      }
      // 批處理
      int[] result = stmt.executeBatch();
      System.out.println("影響的行數分別為:");
      for (int i = 0; i < result.length; i++) {
        System.out.print(result[i] + "  ");
      }
    } catch (SQLException e) {
      System.out.println(e.getMessage());
      e.printStackTrace();
    } finally {
      // 釋放資源
      if (conn != null) {
        conn.close();
      }
      if (stmt != null) {
        stmt.close();
      }
      if (rs != null) {
        rs.close();
      }
    }
  }
}

​ 由於Statement無法傳遞引數,必須是完整的SQL語句,因此先將SQL拼接之後通過addBatch(sql)方法加入到批處理中,然後通過executeBatch方法執行批處理返回影響行數的陣列。
​ PreparedStatement既可以是完整的SQL,也可以用帶引數的不完整的SQL。我們看一下使用PreparedStatement進行批處理的例子。

package spring.tutorial.chapter5.jdbc;

import java.sql.*;

public class PreparedStatementSQLBatch {
  public static void main(String[] args) throws SQLException {
    Connection conn = null;

    ResultSet rs = null;
    PreparedStatement prestmt = null;
    try {
      // 註冊驅動
      DriverManager.registerDriver(new com.mysql.jdbc.Driver());
      // 通過註冊的驅動獲得連線物件Connection
      conn =
          DriverManager.getConnection(
              "jdbc:mysql://127.0.0.1:3306/daodemodb?useUnicode=true&characterEncoding=UTF-8"
                  + "&serverTimezone=UTC&useSSL=false",
              "root",
              "123456");
      String sql = "insert into t_user (name,age,money) values(?,?,?)";
      prestmt = conn.prepareStatement(sql);
      for (int i = 0; i < 2; i++) {
        prestmt.setString(1, "PreparedStatementTest" + i);
        prestmt.setInt(2, 25 + i);
        prestmt.setDouble(3, 222.22);
        prestmt.addBatch();
      }
      // 批處理
      int[] result = prestmt.executeBatch();
      System.out.println("影響的行數分別為:");
      for (int i = 0; i < result.length; i++) {
        System.out.print(result[i] + "  ");
      }
    } catch (SQLException e) {
      System.out.println(e.getMessage());
      e.printStackTrace();
    } finally {
      // 釋放資源
      if (conn != null) {
        conn.close();
      }
      if (prestmt != null) {
        prestmt.close();
      }
      if (rs != null) {
        rs.close();
      }
    }
  }
}

​ 這裡使用佔位符?來初始化SQL,然後通過不帶引數的addBatch加入批處理中,最後還是通過executeBatch執行批處理操作。

​ 上面演示了Statement、PreparedStatement批處理的使用,這裡還要說明一下,批量執行SQL需要資料庫的支援,有些資料庫可能不支援。批量處理將多條SQL語句提交給資料庫一塊執行,效率高一些,但如果資料比較多,比如幾萬條SQL,就需要分批次執行,例如200條執行一次,如果為了增加一致性,可以在批量處理的基礎上增加事務。

5.1.7 事務處理

​ 關係型資料庫一般都支援事務。事務有四大特性:原子性、一致性、隔離性、永續性。

​ ● 原子性:原子性是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾。例如轉賬,A賬戶轉給B賬戶,包含兩個操作,將A賬戶的錢減去,然後將B賬戶加上對應的錢數,不可能A賬戶減了B賬戶沒加上,也不可能A賬戶沒減就給B賬戶加上了。兩個操作要麼都成功,要麼都失敗。
​ ● 一致性:一致性是指事務必須使資料庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之後都必須處於一致性狀態。假設賬戶A和賬戶B兩者的錢加起來一共是5000,那麼不管A和B之間如何轉賬、轉幾次賬,事務結束後兩個使用者的錢相加起來應該還得是5000,這就是事務的一致性。這個就涉及隔離級別的問題了。
​ ● 隔離性:隔離性是當多個使用者併發訪問資料庫時,比如操作同一張表時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作所干擾,多個併發事務之間要相互隔離。
​ ● 永續性:永續性是指一個事務一旦被提交了,那麼對資料庫中的資料的改變就是永久性的,即便是在資料庫系統遇到故障的情況下也不會丟失提交事務的操作。
​ 本例中使用t_user表的兩個使用者來模擬轉賬操作。目前李四賬戶有888.88元、張三賬戶有666.66元,讓使用者李四給使用者張三轉賬111.11元,使兩個賬戶都有777.77元。

image-20201126224722717

​ 事務有兩個結果:一是成功,二是回滾。在事務中,任何一個操作發生異常都會回滾。

package spring.tutorial.chapter5.jdbc;

import java.sql.*;

public class TransactionDemo {
  public static void main(String[] args) throws SQLException {

    Connection conn = null;

    ResultSet rs = null;
    PreparedStatement prestmt = null;
    try {
      // 註冊驅動
      DriverManager.registerDriver(new com.mysql.jdbc.Driver());
      // 通過註冊的驅動獲得連線物件Connection
      conn =
          DriverManager.getConnection(
              "jdbc:mysql://127.0.0.1:3306/daodemodb?useUnicode=true&characterEncoding=UTF-8"
                  + "&serverTimezone=UTC&useSSL=false",
              "root",
              "123456");
      // 手動開啟事務
      conn.setAutoCommit(false);
      String sql = "update  t_user set money=money-? where id=?";
      prestmt = conn.prepareStatement(sql);

      prestmt.setDouble(1, -111.11);
      prestmt.setInt(2, 2);
      prestmt.addBatch();

      prestmt.setDouble(1, 111.11);
      prestmt.setInt(2, 1);
      prestmt.addBatch();

      // 批處理
      prestmt.executeBatch();

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

    } catch (SQLException e) {
      // 事務回滾
      conn.rollback();
      System.out.println(e.getMessage());
      e.printStackTrace();
    } finally {
      // 釋放資源
      if (conn != null) {
        conn.close();
      }
      if (prestmt != null) {
        prestmt.close();
      }
      if (rs != null) {
        rs.close();
      }
    }
  }
}

​ 上面程式碼只是做測試並未做金額是否滿足轉賬要求檢查,先使用“conn.setAutoCommit(false);”將自動提交設定為手動提交,預設是自動,然後批量執行兩個SQL語句,在“conn.commit();”提交事務之前如果沒有出現錯誤,執行結果會儲存到資料庫,一旦出現異常,就會執行“conn.rollback();”回滾操作。執行上面程式碼轉賬成功的輸出結果如下:

image-20201126225013604

​ 上面是提交成功的例子。為了演示事務回滾,可以在提交事務“conn.rollback();”之前製造一個異常“inta=1/0;”,然後執行,發現資料庫的值並不會改變,並丟擲了異常。

image-20201126225058274

5.2 Spring DAO模組

​ Spring的DAO模組提供了對JDBC、Hibernate、MyBatis等DAO層支援,本節介紹DAO模組對JDBC的支援。DAO模組依賴commons-dbcp.jar、commons-pool.jar。

5.2.1 JdbcDaoSupport的使用

​ 傳統的JDBC需要建立連線、開啟、執行SQL、關閉連線這一系列步驟。Spring框架對JDBC進行了封裝,我們只需使用封裝好的JdbcTemplate執行SQL語句。既然是JdbcDaoSupport的使用,為什麼是使用JdbcTemplate呢?因為JdbcDaoSupport提供了JdbcTemplate物件,通過JdbcTemplate物件進行資料庫操作。可以轉到定義,檢視JdbcDaoSupport、JdbcTemplate兩個類的具體實現。我們通過下面的例子來了解JdbcDaoSupport的使用,這裡還是使用JDBC章節的資料庫daodemodb和表t_user資訊。

​ 第一步,根據t_user表資訊準備Model類User,定義id、name、age、money屬性,並宣告兩個建構函式。

package spring.tutorial.chapter5.SpringDao.model;

public class User {
  private int Id;
  private String Name;
  private int Age;
  private double Money;
  public User() {}

  public User(String name, int age, double money) {

    Name = name;
    Age = age;
    Money = money;
  }

  @Override
  public String toString() {

    return "Id:"
        + this.getId()
        + " Name:"
        + this.getName()
        + " Age:"
        + this.getAge()
        + " Money:"
        + this.getMoney();
  }

 	//getter setter
}

​ 第二步,定義介面類IUserDAO,在介面中宣告兩個方法:QueryAllUser方法屬於查詢操作,查詢所有User;AddUser屬於更新操作,新增User。

public interface IUserDAO {
  public List<User> QueryAllUser();

  public Boolean AddUser(User user);

  public Boolean transfer(int fromUserId, int toUserId, float transferMoney);
}

​ 第三步,就是JdbcDaoSupport的使用了。在下面的SpringDAODemo類中首先繼承JdbcDaoSupport,同時實現IUserDAO介面中的方法。JdbcDaoSupport提供了JdbcTemplate物件,SpringDAODemo繼承了JdbcDaoSupport,所以也就可以直接獲取到JdbcTemplate物件,然後執行該物件的方法進行資料庫操作。

相關文章