2025/05/05

Spring Boot 3 Data 1

spring boot 支援資料庫、connection pool、transaction 自動設定。資料庫還支援 H2, HSQL, Derby 嵌入式資料庫。NoSQL 支援 Mongodb, Neo4j, Elasticsearch, Redis, GemFire or Geode, Cassandra, Couchbase, LDAP, InfluxDB。比較常用的是 Redis, MongoDB, Elasticsearch。


嵌入式資料庫

存放在記憶體的資料庫

不需要設定 URL,只要引用 library 即可

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

資料來源 DataSource

java 是透過 javax.sql.DataSource 連接關聯式資料庫。spring boot 只需要引用 spring-boot-starter-data-jdbc 即可,另外還需要該關聯式資料庫的 JDBC driver,例如 MySQL 要用 mysql-connector

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

spring-boot-starter-data-jdbc 包含預設的 HikariCP connection pool

DataSource 自動設定的類別是 DataSourceAutoConfiguration

自動配置檔案 org.springframework.boot.autoconfigure.AutoConfiguration.imports

因為資料庫自動設定類別使用 @ConditionalOnClass,如果 classpath 裡面有以下兩個類別,就會自動完成設定

  • DataSource.class

  • EmbeddedDatabaseType.class

會先設定外部關聯式資料庫 DataSource,再設定嵌入式資料庫。如果沒有自動設定的類別,就會使用 connection pool 的預設設定值

application.yml 的設定參數為 spring.datasource.*

spring:
  application:
    name: data1

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    #driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost/testweb
    username: root
    password: password

也可以使用 MariaDB 的 driver

        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>

driver-class 也要同時修改

    driver-class-name: org.mariadb.jdbc.Driver

因 Dialect 在 MySQL 版本有差異,啟動 application 會出現這樣的錯誤資訊

org.hibernate.dialect.Dialect            : HHH000511: The 5.5.68 version for [org.hibernate.dialect.MySQLDialect] is no longer supported, hence certain features may not work properly. The minimum supported version is 8.0.0. Check the community dialects project for available legacy versions.

這時候要調整使用的 Dialect

        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-community-dialects</artifactId>
        </dependency>

application.yml

spring:
  jpa:
    properties:
      hibernate:
        # dialect: org.hibernate.dialect.MySQLDialect
        dialect: org.hibernate.community.dialect.MySQLLegacyDialect

啟動時,同時會看到 Hikari connection pool 的 log

HikariPool-1 - Starting...
HikariPool-1 - Added connection org.mariadb.jdbc.Connection@3a175162
HikariPool-1 - Start completed.

自訂 DataSource

直接註冊一個實作 DataSource 的 bean 即可

package com.test.data1;

import org.mariadb.jdbc.MariaDbDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration(proxyBeanMethods = false)
public class DbConfig {
    @Bean
    @ConfigurationProperties(prefix="spring.datasource")
    public DataSource dataSource() {
        return new MariaDbDataSource();
    }

}

也可定義兩個 DataSource,但第一個要設定為 @Primary

package com.test.data1;

import org.mariadb.jdbc.MariaDbDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration(proxyBeanMethods = false)
public class DbConfig2 {

    @Primary
    @Bean
    @ConfigurationProperties(prefix="spring.datasource.xx.one")
    public DataSource dataSource1() {
        return new MariaDbDataSource();
    }

    @Bean
    @ConfigurationProperties(prefix="spring.datasource.xx.two")
    public DataSource dataSource2() {
        return new MariaDbDataSource();
    }
}

透過 AbstractRoutingDataSource#determineCurrentLookupKey 決定哪一個 data source。


Connection Pool

spring boot 預設使用 HikariCP

自動設定類別是 PooledDataSourceConfiguration

spring 提供以下幾種 connection pool,依照 libary dependency 判斷順序選擇

  • HikariCP

  • Tomcat

  • DBCP2

  • Oracle UCP

如果都沒有,就直接使用JDBC connection

可設定 spring.datasource.type ,這樣就不會自動選擇 connection pool

spring:
  datasource:
    type: org.apache.commons.dbcp2.BasicDataSource

資料庫初始化

spring boot 可在啟動時處理 DDL, DML

目前是用 SqlInitializationAutoConfiguration 自動設定

對應參數綁定是 SqlInitializationProperties,可透過 spring.sql.init.* 參數設定

spring:
  application:
    name: data1

  datasource:
    #driver-class-name: com.mysql.cj.jdbc.Driver
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost/testweb
    username: root
    password: password
  sql:
    init:
      mode: ALWAYS
      continue-on-error: true
      schema-locations: sql/create_user.sql
      data-locations:
        - classpath:sql/insert_user.sql

mode: 資料庫初始化模式

  • ALWAYS: 會對外部關聯式資料庫初始化,application 每一次啟動,該 sql 都會被執行一次

  • EMBEDDED: 只會對 embedded database 自動初始化

  • NEVER

continue-on-error: true,初始化錯誤,要不要繼續執行。如果 mode 為 ALWAYS,就會忽略重複 insert 有 primary key 的資料的錯誤

schema-locations: DDL script

data-locations: DML script


JdbcTemplate

JdbcTemplate 是 spring 用來簡化 JDBC operation 的 wrapper,只需要引用 spring-boot-starter-data-jdbc 就可以使用

自動設定類別是 JdbcTemplateAutoConfiguration,裡面使用了 JdbcTempateConfiguration,有預設的 JdbcTemplate instance,在 application 中注入即可使用

@Slf4j
@RequiredArgsConstructor
@SpringBootApplication
public class Data1Application {
    public final JdbcTemplate jdbcTemplate;

    public static void main(String[] args) {
        SpringApplication.run(Data1Application.class, args);
    }

    @Bean
    @Transactional
    public CommandLineRunner commandLineRunner() {
        return testJdbcTemplate();
    }

    private CommandLineRunner testJdbcTemplate() {
        return (args) -> {
            log.info("testing JdbcTemplate...");
            String username = jdbcTemplate.queryForObject("select username from user where id = 1", String.class);
            log.info("id = 1, username = {}", username);
        };
    }

}

自訂 JdbcTemplate

可以在 spring.jdbc.template.* 修改 jdbcTemplate 的設定

spring:
  jdbc:
    template:
      max-rows: 3
    private CommandLineRunner testJdbcTemplate2() {
        return (args) -> {
            log.info("testing JdbcTemplate2...");
            List<Map<String, Object>> result= jdbcTemplate.queryForList("select username from user");
            log.info("result list size={}", result.size());
        };
    }

因為設定的限制,回傳的 list size 最多只會是 3


transaction

spring-boot-starter-data-jdbc 會引用 transaction 相關 library,自動設定類別有

  • TransactionAutoConfiguration

  • DataSourceTransactionManagerAutoConfiguration

參數綁定類別是 TransactionProperties

spring.transaction.* 參數設定

新增一個 user DTO 物件

package com.test.data1.entity;

import lombok.Data;
import org.springframework.data.relational.core.mapping.Column;

import java.time.LocalDateTime;

@Data
public class UserDO {

    private long id;

    private String username;

    private String phone;

    @Column(value = "create_time")
    private LocalDateTime createTime;

    private int status;

}

製作 DAO interface UserDao.java

package com.test.data1.dao;

public interface UserDao {
    void update();
}

UserDaoImpl.java

package com.test.data1.dao.impl;

import com.test.data1.dao.UserDao;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class UserDaoImpl implements UserDao {

    public final JdbcTemplate jdbcTemplate;

    @Transactional
    @Override
    public void update() {
        jdbcTemplate.execute("update user set username = 'Petty' where id = 1");
        jdbcTemplate.execute("update user set username = 'Yoga' where id = 2");
        throw new RuntimeException("test exception");
    }

}

Data1Application.java

@Slf4j
@RequiredArgsConstructor
@SpringBootApplication
public class Data1Application {
    public final JdbcTemplate jdbcTemplate;

    public final UserDao userDao;

    public static void main(String[] args) {
        SpringApplication.run(Data1Application.class, args);
    }

    @Bean
    @Transactional
    public CommandLineRunner commandLineRunner() {
        return useJdbcTemplate();
    }

    private CommandLineRunner useJdbcTemplate() {
        return (args) -> {
            log.info("using JdbcTemplate...");
            BeanPropertyRowMapper<UserDO> rowMapper = new BeanPropertyRowMapper<>(UserDO.class);
            UserDO userDO = jdbcTemplate.queryForObject("select * from user where id = 2", rowMapper);
            log.info("user info : {}", userDO);

            List<UserDO> userDOList = jdbcTemplate.query("select * from user", rowMapper);
            log.info("user list: {}", userDOList);

            log.info("userDao.update()...");
            userDao.update();

            List<UserDO> userDOList2 = jdbcTemplate.query("select * from user", rowMapper);
            log.info("user list2: {}", userDOList2);
        };
    }
}

雖然在 update() 裡面修改了 username,但因爲 @Transactional 的關係,資料 rollback 回原本的狀態,修改的資料會被 rollback。


transaction 失敗的原因

  1. Database 不支援 transaction。MySQL 的 MyISAM 不支援 transaction,在 5.5 以後預設是使用 InnoDB,這個才有支援 transaction

  2. 沒有被 spring 管理

    UserDaoImpl 裡面的 @Service 如果刪除,就不會受到 spring 管理

    //@Service
    public class UserDaoImpl implements UserDao {
    }
  3. @Transactional 的 method 必須要是 public method

    如果要用在非 public method,必須開啟 AspectJ framework 的靜態代理模式

  4. 呼叫內部 method

    business method 一定要經過 spring 才能處理 transaction

    以下這兩種狀況,transaction 都不會生效

    @Service
    public class UserDaoImpl implements UserDao {
       public void update1(User user) {
           //call update2
           update2(user);
       }
    
       @Transactional
       public void update2(User user) {
           //update user
       }
    }
    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional
       public void update1(User user) {
           //call update2
           update2(user);
       }
    
       @Transactional(propagation = Propagation.REQUIRES_NEW)
       public void update2(User user) {
           //update user
       }
    }

    解決方法是在 UserDaoImpl 裡面注入自己,用該物件,呼叫另一個 method

    @EnableAspectJAutoProxy(exposeProxy = true)

    取得當前的 proxy,並呼叫 method

    ((UserDaoImpl)AopContext.currentProxy()).update2();
  5. 沒有配置 transaction manager

    必須要有 PlatformTransactionManager 才能處理 transaction

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
       return new DataSourceTransactionalManager(dataSource);
    }

    spring-boot-starter-data-jdbc 裡面自動設定了一個 DataSourceTransactionalManager,所以可直接使用

  6. 設定不支援 transaction

    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional
       public void update1(User user) {
           //call update2
           update2(user);
       }
    
       @Transactional(propagation = Propagation.NOT_SUPPORTED)
       public void update2(User user) {
           //update user
       }
    }
  7. 沒有 throw exception,就不會 rollback

    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional
       public void update1(User user) {
           try{
           } catch(Exception e) {
           }
       }
    }
  8. exception 類別不同

    因為 spring 預設針對 RuntimeException 進行 rollback,如果該 method throw 的 exception 類別錯誤,也不會觸發 rollback,除非在 @Transactional 設定 rollbackFor Exception 類別

    @Service
    public class UserDaoImpl implements UserDao {
       @Transactional(rollbackFor = Exception.class)
       public void update1(User user) {
           try{
           } catch(Exception e) {
              throw new Exception(e.getMessage());
           }
       }
    }