天天看點

一文學會Spring JDBC 使用

Spring JDBC

1、JDBC

JDBC 就是 資料庫開發 操作的 代名詞,因為隻要是現代商業項目的開發那麼一定是離不開 資料庫 的,不管你搞的是什麼,隻要是想使用動态的開發結構,那麼一定就是 JDBC ,那麼下面首先來回顧一下傳統JDBC的使用。

一文學會Spring JDBC 使用

JDBC有四種連接配接: 像JDBC-ODBC的連接配接已經确定不再使用了、主要采用的是 JDBC網絡連接配接模式 。

  • 在JDBC的開發之中,一定要配置相應資料庫的驅動程式後才可以使用,是以這就屬于标準的做法,同時還有一點必須明确,不管未來出現了什麼樣的 Java資料庫開發架構,那麼核心的本質隻有一點:JDBC,可是JDBC 标準裡面所定義的 操作結構 是屬于 較為底層 的操作形式,是以使用起來 非常的​

    ​繁瑣​

    ​ ,因為幾乎所有的資料庫的項目都需要​

    ​加載驅動、建立資料庫連接配接、資料庫的操作對象、關閉資料庫​

    ​ ,隻有中間的資料庫的CRUD操作是有差別的,那麼就需要考慮對JDBC進行封裝了,那麼這個時候就有了ORM元件(全稱​

    ​ORMapping​

    ​ 、對象關聯映射,采用對象的形式實作JDBC的開發操作)。
一文學會Spring JDBC 使用

​ 從曆史的發展上來講,ORMapping元件出現較多:JDO、Entity Bean、Hibernate、IBatis、SpringJDBC、MyBatis、JPA标準,當然随着技術的發展與淘汰,基本上現在階段剩下的ORM元件,常用的就是MyBatis(國内網際網路公司)、JPA(國外機構),而SpringJDBC是屬于JDBC的輕度包裝元件(其他的元件都屬于重度包裝),是以使用 ​

​SpringJDBC​

​ 可以 ​

​簡化JDBC​

​ 傳統開發裡面繁瑣的操作步驟。

一文學會Spring JDBC 使用
添加依賴
<properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>5.3.21</spring.version>
        <mysql.version>8.0.30</mysql.version>
</properties>

<dependencies>
        <!--核心依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <!--spring-jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <!--資料庫依賴-->  
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--測試-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.8.2</version>
        </dependency>

        <!--日志依賴-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <!--日志依賴-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>
    </dependencies>      

log4j.properties日志配置檔案 (當啟動程式,沒有任何報錯,但是沒有資訊列印時,需要配置日志)

#将等級為DEBUG的日志資訊輸出到console和file這兩個目的地,console和file的定義在下面的代碼
log4j.rootLogger=DEBUG,console,file

#控制台輸出的相關設定
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.ImmediateFlush=true
log4j.appender.console.Target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%-5p] %d(%r) --> [%t] %l: %m %x %n

#檔案輸出的相關設定
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/logFile.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志輸出級别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG      

2、使用

要想使用JDBC,配置資料源,是關鍵性的一步。

2.1、配置資料源:

2.1.1、注冊資料源對像

建立資料源的配置類:(基于配置類的方式)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        // 驅動資料源
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        // 加載驅動程式
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/yootk"); 
        dataSource.setUsername("root"); 
        dataSource.setPassword("317311");
        return dataSource;
    }
}      

建立資料源的配置類:(基于xml的方式)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--資料源的配置-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/yootk"/>
        <property name="username" value="root"/>
        <property name="password" value="317311"/>
    </bean>
</beans>      
2.1.2、測試:
import look.word.jdbc.config.DataSourceConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.sql.DataSource;

@ContextConfiguration(classes = DataSourceConfig.class) //  兩者二選一即可
//@ContextConfiguration(locations ={"classpath:data-source.xml"}) 
@ExtendWith(SpringExtension.class)
public class TestDataSource {
    // 日志工廠對象
    private static final Logger LOGGER = LoggerFactory.getLogger(TestDataSource.class);
    @Autowired
    private DataSource dataSource;
    @Test
    public void testConnection() throws Exception{
        LOGGER.info("【資料庫連接配接對象】:{}",dataSource);
    }
}

// 執行結果 輸入資料源對象,說明連接配接成功
// [INFO ] 2022-09-14 12:18:59,307(386) --> [main] look.word.test.TestDataSource.testConnection(TestDataSource.java:29): 【資料庫連接配接對象】:org.springframework.jdbc.datasource.DriverManagerDataSource@535779e4      

但是基于這種連接配接操作的性能是非常一般的,請追随源代碼,一探究竟。

一文學會Spring JDBC 使用

然後找到我們的AbstractDriverBasedDataSource.getConnection()方法,進入getConnectionFromDriver()方法。

一文學會Spring JDBC 使用

找到getConnectionFromDriver(),他是一個抽象方法,然後找到其子類,DriverManagerDataSource

一文學會Spring JDBC 使用

然後又會發現,我們回到了DriverManagerDataSource,然後我們在進入getConnectionFromDriverManager方法。

一文學會Spring JDBC 使用

最終擷取連接配接的方式,

一文學會Spring JDBC 使用
2.1.3、預設連接配接方式的缺點

這種連接配接的管理方式,是在每一次 擷取連接配接 的時候 才進行 資料庫連接配接的操作了,那麼現在的問題就來了,這樣的管理方式好嗎 ?首先在資料庫連接配接的處理之中,一定會建立若幹個Socket 連接配接,那麼會有 耗時 ,而在資料庫關閉的時候也會存在有同樣的耗時處理,這樣在“次次次高并發”的處理下很難得到有效的控制。 是以在實際項目中最佳資料庫連接配接的管理,一定是基于資料庫連接配接池方式實作的 。是以此時可以考慮在 Spring 内部去實作一個連接配接池的維護。早期的資料庫連接配接池元件提供有一個 C3P0元件,但是現在已經停止維護了。

2.2、HikariCP

在實際的項目應用開發過程之中,為了解決 JDBC連接配接與關閉的延時以及性能問題 ,提供了 資料庫連接配接池 的解決方案,并且針對于該方案提供了成型的HikariCP服務元件。HikariCP (Hikari來自日文,是“光”的含義)是由日本程式員開源的一個資料庫連接配接池元件,該元件擁有如下特點:

  • 宇節碼更加的精簡,這樣可以在緩存中添加更多的程式代碼;
  • 實作了一個無鎖集合,減少了并發通路造成的資源競争問題;
  • 使用了自定義數組類型(FastList)代替了ArrayList,提高了get()與remove()的操作性能;
  • 針對CPU的時間片算法進行了優化,盡可能在一個時間片内完成所有處理操作。

在Spring之中預設推薦的資料庫連接配接池元件就是 HikariCP ,不建議再使用其他的資料庫連接配接池元件,當然國内也有優秀的CP元件,那麼就是阿裡推出的Druid(在性能上可能低于HikariCP,但是提供有完整的管理界面),如果要想使用這個元件,可以采用如下的步驟進行配置。

2.2.1、使用
添加依賴:
<dependency>
                <groupId>com.zaxxer</groupId>
                <artifactId>HikariCP</artifactId>
                <version>5.0.1</version>
            </dependency>      
編寫配置類:

這次我們再用配置檔案的方式,友善擴充

  • 建立配置檔案:src/main/profiles/dev/config/database.properties
一文學會Spring JDBC 使用
yootk.database.driverClassName=com.mysql.cj.jdbc.Driver
yootk.database.jdbcUrl=jdbc:mysql://localhost:3306/yootk
yootk.database.username=root
yootk.database.password=317311
# 【Hikaricp】配置資料庫連接配接逾時時間 機關【毫秒】
yootk.database.connectionTimeOut=3000
# 【Hikaricp】一個連接配接最小維持的時間 機關【毫秒】
yootk.database.idleTimeOut=3000
# 【Hikaricp】一個連接配接最長存活的時間 機關【毫秒】
yootk.database.maxLifetime=6000
# 【Hikaricp】最大儲存的資料庫連接配接執行個體
yootk.database.maximumPoolSize=60
# 【Hikaricp】最小儲存的資料庫連接配接執行個體 (在沒有任何使用者通路時,最少維持的連接配接數量)
yootk.database.minimumIdle=20
# 【Hikaricp】是否為隻讀
yootk.database.readOnly=false      
建立配置對象
@Configuration
//讀取指定位置的資源檔案
@PropertySource("classpath:config/database.properties") 
public class HikariCpDataSourceConfig {
    /**
     * 綁定資源檔案中的配置資料項
     */
    @Value("${yootk.database.driverClassName}")
    private String driverClassName;
    @Value("${yootk.database.jdbcUrl}")
    private String jdbcUrl;
    @Value("${yootk.database.username}")
    private String username;
    @Value("${yootk.database.password}")
    private String password;
    @Value("${yootk.database.connectionTimeOut}")
    private Long connectionTimeOut;
    @Value("${yootk.database.idleTimeOut}")
    private Long idleTimeOut;
    @Value("${yootk.database.maxLifetime}")
    private Long maxLifetime;
    @Value("${yootk.database.maximumPoolSize}")
    private Integer maximumPoolSize;
    @Value("${yootk.database.minimumIdle}")
    private Integer minimumIdle;
    @Value("${yootk.database.readOnly}")
    private boolean readOnly;

    @Bean("dataSource")
    public DataSource dataSource() {
        // Hikari連接配接池資料源
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setJdbcUrl(jdbcUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setPassword(password);
        // 逾時時間
        dataSource.setConnectionTimeout(connectionTimeOut);
        // 空閑逾時
        dataSource.setIdleTimeout(idleTimeOut);
        // 連接配接的最長時間
        dataSource.setMaxLifetime(maxLifetime);
        // 連接配接池最大數量
        dataSource.setMaximumPoolSize(maximumPoolSize);
        // 當沒有連接配接時 最小保留的連接配接數量
        dataSource.setMinimumIdle(minimumIdle);
        // 是否隻讀資料庫
        dataSource.setReadOnly(readOnly);
        return dataSource;
    }
}      
測試類:
import look.word.jdbc.config.HikariCpDataSourceConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.sql.DataSource;
@ContextConfiguration(classes = HikariCpDataSourceConfig.class)
@ExtendWith(SpringExtension.class)
public class TestDataSource {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestDataSource.class);

    @Autowired
    private DataSource dataSource;

    @Test
    public void testConnection() throws Exception {
        LOGGER.info("【資料庫連接配接對象】:{}", dataSource.getConnection());
    }
}      

如果出錯,可以看看日志輸入資訊。

這樣我們就實作了,使用HikariCP擷取連接配接對象了,接下來就會使用HikariCP對具體的資料庫進行操作。

2.3、JdbcTempLate

JdbcTempLate的使用很簡單,隻需要為其指定資料源即可。

我們采用配置類的方式,為其配置資料源

2.3.1、增
添加配置類:
@Configuration
public class JdbcTempLateConfig {
    @Bean // 方法形參 會自動從容器中注入對象
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        return jdbcTemplate;
    }
}      
編寫測試類:
@ContextConfiguration(classes = {HikariCpDataSourceConfig.class, JdbcTempLateConfig.class})
@ExtendWith(SpringExtension.class)
public class TestJdbcTempLate {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestJdbcTempLate.class);

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testConnection() throws Exception {
        String sql = "insert into book(title,author,price) values('java入門','李老師',99.90)";
        LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql));
    }
}      
執行結果:
一文學會Spring JDBC 使用

這個時候就是用JdbcTemplate輕松地實作了資料的插入操作。

但是,可以發現,我們上面的操作,還是存在問題的,比如沒有對sql 進行預處理,會出現 Sql 注入的風險。

2.3.2、改
測試類
@Test
    public void testUpdate() {
        String sql = "update yootk.book set title = ? where bid = ?";
        LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, "Python入門", 2));
    }      
2.3.3、删
測試類
@Test
    public void testDelete() {
        String sql = "delete from yootk.book  where bid = ?";
        LOGGER.info("【插入執行結果】:{}", jdbcTemplate.update(sql, 2));
    }      
2.3.4、增 (傳回id)

在MySQL資料庫裡面,有一種功能,可以通過一個next()處理函數擷取目前所生成的ID号(主要針對于自動增長列),實際上這個功能主要的目的是為了解決增加資料時的ID傳回處理問題了,因為很多的時候需要在資料增加成功之後對指定的ID進行控制,是以才提供了專屬的處理函數,Oracle之中直接使用序列即可,但是MySQL的實作就需要專屬的處理函數了。.在程式的開發之中,如果要想擷取到增長後的ID資料,在SpringJDBC裡面提供有了一個 KeyHolder 接口,在這個接口裡面定義了 擷取主鍵 内容的處理方法。

在平常開發中,我們經常會遇到,插入這個資料後,會需要這個資料的id,然後對其進行一系類操作。

如果要想擷取到增長後的ID資料,在SpringJDBC裡面提供有了一個 KeyHolder 接口,在這個接口裡面定義了擷取主鍵内容的處理方法。

測試類
@Test
    public void testInsertReturnId() {
        String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
        GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); // 擷取KEY的處理資訊
        int count = jdbcTemplate.update(new PreparedStatementCreator() {
            @Override
            public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); // 對sql進行預處理
                ps.setString(1, "Springboot實戰");
                ps.setString(2, "老李");
                ps.setDouble(3, 99.00);
                return ps;
            }
        }, keyHolder);
        LOGGER.info("【插入執行影響行數】:{},目前插入資料的ID:{}", count, keyHolder.getKey());
    }
// 執行結果
// look.word.test.TestJdbcTempLate.testInsertReturnId(TestJdbcTempLate.java:61): 【插入執行影響行數】:1,目前插入資料的ID:4      

如果在 ​

​PreparedStatement ps = con.prepareStatement(sql);​

​ 中,沒有指定需要傳回KEY,則會出現異常。

2.3.5、批處理
一文學會Spring JDBC 使用
測試類:

這種方式是基于集合的。

@Test
    public void testInsertBatch() {
        List<String> titles = List.of("Springboot開發實戰", "SSM開發案例", "Netty開發實戰", "Redis開發實戰");
        List<Double> prices = List.of(90.1, 98.9, 78.9, 98.9);
        String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
        this.jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {  // 執行批量插入
             //@param i  集合索引
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, titles.get(i));
                ps.setString(2, "老李老師");
                ps.setDouble(3, prices.get(i));
            }
            @Override
            public int getBatchSize() {
                return titles.size(); //總長度
            }
        });
    }      

基于對象

@Test
    public void testInsertBatch2() {
        List<Object[]> params = List.of(
                new Object[]{"Spring開發實戰", "11", 89.0},
                new Object[]{"Spring開發實戰1", "11", 89.0},
                new Object[]{"Spring開發實戰2", "11", 89.0},
                new Object[]{"Spring開發實戰3", "11", 89.0}
        );
        String sql = "insert into yootk.book(title,author,price) values(?,?,?)";
        int[] result = jdbcTemplate.batchUpdate(sql, params);//批量插入
        System.out.println("result = " + result);
    }      
2.3.4、查
查詢單個
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
    private Integer bid;
    private String title;
    private String author;
    private Double price;
}      
測試類:
// 查詢單個
    @Test
    public void testQuery() {
        String sql = "select  bid, title, author, price from yootk.book  where bid = ?";
        Book book = jdbcTemplate.queryForObject(sql, new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                Book book = new Book();
                book.setBid(rs.getInt(1));
                book.setTitle(rs.getString(2));
                book.setAuthor(rs.getString(3));
                book.setPrice(rs.getDouble(4));
                return book;
            }
        }, 3); // 這裡的3 是對預處理資料的回填 多個需按照順序編寫
        System.out.println("【queryForObject 查詢結果】book = " + book);
    }      
查詢多個
// 查詢所有
    @Test
    public void testQueryAll() {
        String sql = "select  bid, title, author, price from yootk.book ";
        List<Book> list = jdbcTemplate.query(sql, new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                Book book = new Book();
                book.setBid(rs.getInt(1));
                book.setTitle(rs.getString(2));
                book.setAuthor(rs.getString(3));
                book.setPrice(rs.getDouble(4));
                return book;
            }
        });
        list.stream().forEach(System.out::println);
    }      
分頁查詢
// 分頁
    @Test
    public void testQuerySpAll() {
        int current = 2; // 頁數
        int size = 5;// 每頁數量
        String sql = "select  bid, title, author, price from yootk.book limit ? ,?  ";
        List<Book> list = jdbcTemplate.query(sql, new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                Book book = new Book();
                book.setBid(rs.getInt(1));
                book.setTitle(rs.getString(2));
                book.setAuthor(rs.getString(3));
                book.setPrice(rs.getDouble(4));
                return book;
            }
        }, (current - 1) * size, size);
        list.stream().forEach(System.out::println);
    }      
統計行數
// 查詢行數
    @Test
    public void testQueryCount() {
        String sql = "select  count(*) from yootk.book where title like ?";
        long count = jdbcTemplate.queryForObject(sql, new RowMapper<Long>() {
            @Override
            public Long mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getLong(1);
            }
        }, "%Spring%");
        LOGGER.info("【資料庫記錄總行數】{}", count);
    }