Spring JDBC的使用詳解

JDBC介紹

從這篇文章開始,我們將會介紹SpringBoot另外一個核心的技術,即數據庫訪問技術,提到數據訪問,學習Java的同學瞬間能就想起JDBC技術,JDBC 是 Java Database Connectivity 的全稱,是Java語言中用來規范客戶端程序如何來訪問數據庫的應用程序接口,提供瞭諸如查詢和更新數據庫中數據的一套標準的API,這套標準不同的數據庫廠傢之間共同準守,並提供各自的具體實現。如圖所示:

這樣設計的好處,就是Java程序隻需要和JDBC API交互,從而屏蔽瞭訪問數據庫的復雜的實現,大大降低瞭Java程序訪問數據庫的復雜度。對於日常開發而言,我們隻需要掌握JDBC API 規范中的幾個核心編程對象即可,這些對象包括DriverManger、Connection、Statement及ResultSet。

DriverManager

DriverManager主要負責加載不同數據庫廠傢提供的驅動程序包(Driver),並且根據不同的請求向Java程序返回數據庫連接(Connection)對象,先看下Driver接口的定義:

public interface Driver {
    //獲取數據庫連接
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

Driver中有個重要的方法 connect,來提供Connection對象

不同的數據庫對Driver,有具體的實現,以MySql為例:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    // 通過 DriverManager 註冊 Driver
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
	}
	…
}

這裡用到瞭DriverManager,DriverManager通過 registerDriver來註冊不同數據庫的Driver,並且還提供瞭getConnection返回數據庫連接對象。

Connection

通過DriverManager可以獲取Connetion對象,Connection對象可以理解與數據庫連接的一種會話(Session),一個Connection對象代表一個數據庫的連接,負責完成與數據庫底層的通訊。

Connection對象提供瞭一組重載的方法來創建Statement和PreparedStatement,Statement和PreparedStatement是SQL執行的載體,另外Connection對象還會涉及事務相關的操作。

Connection對象最核心的幾個方法如下:

public interface Connection  extends Wrapper, AutoCloseable {
	//創建 Statement
	Statement createStatement() throws SQLException;
	//創建 PreparedStatement
	PreparedStatement prepareStatement(String sql) throws SQLException;
	//提交
	void commit() throws SQLException;
	//回滾
	void rollback() throws SQLException;
	//關閉連接
	void close() throws SQLException;
}

Statement/PreparedStatement

Statement和PreparedStatement是由Connection對象來創建的,用來執行靜態的SQL語句並且返回生成的結果集對象,這裡存在兩種類型,一種是普通的Statement,另外一種支持預編譯的PreparedStatement。

所謂預編譯,是指數據庫的編譯器會對 SQL 語句提前編譯,然後將預編譯的結果緩存到數據庫中,下次執行時就可以通過替換參數並直接使用編譯過的語句,從而大大提高 SQL 的執行效率。

以Statement為例,看下Statement最核心的方法:

public interface Statement extends Wrapper, AutoCloseable {
	//執行查詢語句
	ResultSet executeQuery(String sql) throws SQLException; 
	//執行更新語句
	int executeUpdate(String sql) throws SQLException; 
	//執行 SQL 語句
	boolean execute(String sql) throws SQLException; 
	//執行批處理
    int[] executeBatch() throws SQLException;
}

ResultSet

通過Statement或PreparedStatement執行SQL語句,我們引出瞭另外一個對象即為ResultSet對象,代碼如下:

public interface ResultSet extends Wrapper, AutoCloseable {
	//獲取下一個結果
	boolean next() throws SQLException;
	//獲取某一個類型的結果值
	Value getXXX(int columnIndex) throws SQLException;
	…
}

ResultSet對象提供瞭next()方法,用來對整個結果集遍歷操作,如果next()方法返回為true,說明還有下一條記錄,

我們可以調用 ResultSet 對象的一系列 getXXX() 方法來取得對應的結果值。

JDBC訪問數據庫流程

對於開發人員而言,通過JDBC的API是Java訪問數據庫的主要途徑,下面用代碼來展示下訪問數據庫的一個整體流程:

String url = "jdbc:mysql://localhost:3306/test" ;
String username = "root" ;
String password = "root" ;

//1.通過DriverManager獲取connection連接
Connection connection = DriverManager.getConnection(url,username,password);

//2.創建preparedStatement
PreparedStatement preparedStatement = connection.prepareStatement("select * from user");

//3.執行SQL返回ResultSet
ResultSet resultSet = preparedStatement.executeQuery();

//4.遍歷resultSet結果集
while (resultSet.next()){
    //resultSet.getString("1");
}

//5.釋放資源
resultSet.close();
preparedStatement.close();
connection.close();

配置數據源

上面我們在介紹JDBC的時候,Connection對象是通過DriverManager獲取,Connection對象代表著和數據庫的連接,每次通過DriverManager獲取比較耗時,影響瞭系統的性能。那有沒有辦法能夠復用Connection對象呢,答案是肯定的

JDBC給我們提供瞭DataSource接口來實現Connection的復用,核心代碼如下:

public interface DataSource  extends CommonDataSource, Wrapper {
 
  Connection getConnection() throws SQLException;
 
  Connection getConnection(String username, String password)
    throws SQLException;
}

作為一種基礎組件,不需要開發人員自己實現 DataSource,因為業界已經存在瞭很多優秀的實現方案,如 DBCP、C3P0 、Druid 、Hikari等

SpringBoot默認HikariDataSource作為DataSource的實現,現在我們SpringBoot為例,看下SpringBoot如何通過JDBC來操作數據庫的,在進行數據庫操作之前,我們首先需要先配置DataSource,SpringBoot配置DataSource非常簡單,隻需要在配置文件中添加DataSource的配置:

spring:
  # datasource 數據源配置內容
  datasource:
    url: jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

使用JDBC操縱數據庫

DataSource配好後,我們在本地的數據庫服務中,創建一個test數據庫,並且執行以下DDL創建user表

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用戶名',
  `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密碼',
  `create_time` datetime DEFAULT NULL COMMENT '創建時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

接下來我們創建一個實體類,實體類中屬性和user表中字段一一對應

@Data
public class User {
    /**
     * 主鍵
     */
    private Integer id;
    /**
     * 用戶名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 創建時間
     */
    private Date createTime;

}

註意:這裡使用瞭Lombok的@Data註解來生成get/set方法。

我們再定義一個UserDao接口,

public interface UserDao {
    /**
     *  新增
     * @param user
     * @return
     */
    Integer insert(User user);
    /**
     *  根據ID查詢
     * @param id
     * @return
     */
    User selectById(Integer id);
    /**
     *  根據ID更新
     * @param user
     * @return
     */
    Integer updateById(User user);
    /**
     *  根據ID刪除
     * @param id
     * @return
     */
    Integer deleteById(Integer id);
}

這裡之所以要抽離出一個UserDao一層有兩個原因:第一UserDao隻封裝瞭對use表的數據庫操作,代碼易於維護和管理,第二我們可以基於UserDao接口提供不同的實現來訪問數據庫,比如我們可以提供基於原生JDBC的實現,也可以用JDBCTemplate實現數據庫的訪問,還可以通過Mybatis等

接下來將通過代碼形式來展示下SpringBoot是如何通過JDBC API對數據庫進行CRUD操作的。我們來定義UserDao的具體實現類命名為:UserRawJdbcDao實現以下方法:

新增數據

@Override
    public Integer insert(User user) {
      	final String SQL_INSERT = "INSERT INTO user(username, password, create_time) VALUES(?, ?, ?)";
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        Integer count = 0;
        try{
            connection = dataSource.getConnection();
            statement = connection.prepareStatement(SQL_INSERT, Statement.RETURN_GENERATED_KEYS);
            statement.setString(1,user.getUsername());
            statement.setString(2,user.getPassword());
            statement.setTimestamp(3,new Timestamp(user.getCreateTime().getTime()));
            count = statement.executeUpdate();
            rs = statement.getGeneratedKeys();
            if(rs.next()){
                user.setId(rs.getInt(1));
            }
        }catch (SQLException e){
            e.printStackTrace();
        }finally {
            try {
                if(rs != null){
                    rs.close();
                }
                if(statement != null){
                    statement.close();
                }
                if(connection != null){
                    connection.close();
                }

            } catch (SQLException e) {
                e.printStackTrace();
            }

        }
        return count;
    }

查詢數據

@Override
    public User selectById(Integer id) {
        final String SQL_SELECT_ID = "SELECT id,username,password,create_time FROM user WHERE id = ?";
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        User user = null;
        try{
            connection = dataSource.getConnection();
            statement = connection.prepareStatement(SQL_SELECT_ID);
            statement.setInt(1, id);
            rs = statement.executeQuery();
            if(rs.next()){
                user = new User();
                user.setId(rs.getInt("id"));
                user.setUsername(rs.getString("username"));
                user.setPassword(rs.getString("password"));
                user.setCreateTime(rs.getTimestamp("create_time"));
            }
        }catch (SQLException e){
            e.printStackTrace();
        }finally {
            try {
                if(rs != null){
                    rs.close();
                }
                if(statement != null){
                    statement.close();
                }
                if(connection != null){
                    connection.close();
                }

            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return user;
    }

更新數據

@Override
    public Integer updateById(User user) {
        final String SQL_UPDATE = "UPDATE user SET username = ?, password = ? WHERE id = ?";
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        Integer count = 0;

        try{
            connection = dataSource.getConnection();
            statement = connection.prepareStatement(SQL_UPDATE);
            statement.setString(1,user.getUsername());
            statement.setString(2,user.getPassword());
            statement.setInt(3,user.getId());
            count = statement.executeUpdate();
        }catch (SQLException e){
            e.printStackTrace();
        }finally {
            try {
                if(rs != null){
                    rs.close();
                }
                if(statement != null){
                    statement.close();
                }
                if(connection != null){
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return count;
    }

刪除數據

@Override
    public Integer deleteById(Integer id) {
        final String SQL_DELETE = "DELETE FROM user WHERE id = ?";
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        Integer count = 0;
        try{
            connection = dataSource.getConnection();
            statement = connection.prepareStatement(SQL_DELETE);
            statement.setInt(1,id);
            count = statement.executeUpdate();
        }catch (SQLException e){
            e.printStackTrace();
        }finally {
            try {
                if(rs != null){
                    rs.close();
                }
                if(statement != null){
                    statement.close();
                }
                if(connection != null){
                    connection.close();
                }

            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return count;
    }

到此,SpringBoot通過調用原生的JDBC的API完成對user表的CRUD操作,這代碼對有代碼潔癖的同學簡直不能忍,有大量共性的代碼,如創建Connection、Statement、ResultSet、資源的釋放和異常的處理。這部分封裝和優化SpringBoot已經處理過瞭,SpringBoot提供瞭JdbcTemplate模板工具類實現數據訪問,它簡化瞭JDBC API的使用方法。

使用JdbcTemplate操縱數據庫

同UserRawJdbcDao,我們再定義UserDao的另外一套實現類命名為:UserJdbcDao,這套實現類是通過JdbcTemplate完成對數據庫的操作,完成接口定義的方法如下:

新增數據

 
@Override
public Integer insert(User user){
  
    // 創建 KeyHolder 對象,設置返回的主鍵 ID
    KeyHolder keyHolder = new GeneratedKeyHolder();
    int count = jdbcTemplate.update(INSERT_PREPARED_STATEMENT_CREATOR_FACTORY.newPreparedStatementCreator(
            Arrays.asList(user.getUsername(),user.getPassword(),user.getCreateTime())),keyHolder);
    // 設置 ID 主鍵到 entity 實體中
    if (keyHolder.getKey() != null) {
        user.setId(keyHolder.getKey().intValue());
    }
    // 返回影響行數
    return count;
}

查詢數據

  @Override
    public User selectById(Integer id){
        User result = jdbcTemplate.queryForObject("SELECT id, username, password, create_time FROM user WHERE id=?",
                new BeanPropertyRowMapper<>(User.class), id);
        return result;
    }

更新數據

  @Override
    public Integer updateById(User user) {
        return jdbcTemplate.update("UPDATE user SET username = ?, password = ? WHERE id = ?",
                user.getUsername(),user.getPassword(),user.getId());
    }

刪除數據

@Override
    public Integer deleteById(Integer id){
        return jdbcTemplate.update("DELETE FROM user WHERE id = ?", id);
    }

小結

通過對比我們發現使用JdbcTemplate模板工具類可以大大減少JDBC訪問數據庫的代碼復雜度,作為開發人員我們應該隻關心業務邏輯的具體實現過程,對JDBC底層對象的創建,資源的釋放,異常的捕獲,應該交給框架統一維護和管理。

雖然JdbcTemplate減少的我們訪問數據庫的代碼量,不過使用也有一些問題,比如:新增數據的時候默認無法返回生成主鍵的id,將SQL硬編碼到Java代碼中,如果SQL修改,需要重新編譯Java代碼,不利於系統的維護等。這時我們需要另外一個框架,它就是大名鼎鼎的Mybatis,下一篇我將會介紹SpringBoot如何整合Mybatis。

項目源碼

github:github.com/dragon8844/…

以上就是Spring JDBC的使用詳解的詳細內容,更多關於Spring JDBC的使用的資料請關註WalkonNet其它相關文章!