目录

Spring Boot Starter JDBC

Spring 将数据访问的样板代码抽象到模板类之中,我们可以直接使用模板类,从而简化了JDBC代码。

引入下面依赖,自动引入连接池 HikariCP 及 spring-jdbc

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    <version>2.7.4</version>
</dependency>

JDBC 访问数据库方式

  • JdbcTemplate是经典且最流行的 Spring JDBC 方法。这种“最低级别”的方法和所有其他方法都在幕后使用 JdbcTemplate。
  • NamedParameterJdbcTemplate 包装 JdbcTemplate 以提供命名参数而不是传统的 JDBC ?占位符。当 SQL 语句有多个参数时,这种方法提供了更好的文档和易用性。
  • SimpleJdbcInsert 及 SimpleJdbcCall 优化数据库元数据以限制必要配置的数量。这种方法简化了编码,因此您只需提供表或过程的名称,并提供与列名匹配的参数映射。这仅在数据库提供足够的元数据时才有效。如果数据库不提供此元数据,则必须提供参数的显式配置。
  • RDBMS 对象(包括MappingSqlQuery、SqlUpdate和StoredProcedure)要求您在初始化数据访问层期间创建可重用和线程安全的对象。这种方法是在 JDO Query 之后建模的,您可以在其中定义查询字符串、声明参数并编译查询。一旦你这样做了, 就可以使用各种参数值多次调用execute(…​)、update(…​)和方法。findObject(…​)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
server:
  port: 8100
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?rewriteBatchedStatements=true
    username: root
    password: wang0804
    hikari:
      data-source-properties:
        useConfigs: maxPerformance
        rewriteBatchedStatements: true
  jdbc:
    template:
      max-rows: 1000
  sql:
    init:
      schema-locations: classpath*:schema-all.sql
      username: root
      password: wang0804
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
CREATE TABLE `user`
(
    `id`          int unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `name`        varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名',
    `age`         int                     DEFAULT NULL COMMENT '年龄',
    `balance`     decimal(12, 2) NOT NULL DEFAULT '0.00' COMMENT '余额',
    `sex`         tinyint                 DEFAULT '-1' COMMENT '性别',
    `phone`       bigint                  DEFAULT NULL COMMENT '手机',
    `province`    varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '注册地址:省',
    `city`        varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '注册地址:城市',
    `country`     varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '注册地址:县/区',
    `status`      tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否启用 0:否 1:是',
    `create_time` datetime       NOT NULL COMMENT '创建时间',
    `update_time` datetime   DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

实践

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@Repository
public class NamedParameterUserDao implements UserRepository {

  private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;

  public NamedParameterUserDao(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
  }

  @Override
  public int count() {
    String sql = "select count(*) from user where sex = :sex";

    Map<String, Integer> namedParameters =
        Collections.singletonMap("sex", Gender.FEMALE.getValue());

    return Optional.ofNullable(
            this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class))
        .orElse(0);
  }

  @Override
  public int update(String sql, User user) {
    // "update user set balance = :balance where id = :id"
    return namedParameterJdbcTemplate.update(sql, new BeanPropertySqlParameterSource(user));
  }

  @Override
  public int deleteById(Long id) {
    SqlParameterSource namedParameters = new MapSqlParameterSource("id", id);
    return namedParameterJdbcTemplate.update("delete from user where id = :id", namedParameters);
  }

  @Override
  public List<User> findAll() {
    return namedParameterJdbcTemplate.query("select * from user", USER_ROW_MAPPER);
  }

  @Override
  public List<User> findByName(String name) {
    MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
    mapSqlParameterSource.addValue("name", "%" + name + "%");

    return namedParameterJdbcTemplate.query(
        "select * from user where name like :name", mapSqlParameterSource, USER_ROW_MAPPER);
  }

  @Override
  public Optional<User> findById(Integer id) {
    return namedParameterJdbcTemplate.queryForObject(
        "select * from user where id = :id",
        new MapSqlParameterSource("id", id),
        (rs, rowNum) -> Optional.of(USER_ROW_MAPPER.mapRow(rs, rowNum)));
  }

  @Override
  public String getNameById(Integer id) {
    return namedParameterJdbcTemplate.queryForObject(
        "select name from user where id = :id", new MapSqlParameterSource("id", id), String.class);
  }

  @Override
  public int[] batchUpdate(String sql, List<User> users) {
    // SqlParameterSourceUtils.createBatch(users)
    return this.namedParameterJdbcTemplate.batchUpdate(
        sql,
        users.stream()
            .map(
                i ->
                    new BeanPropertySqlParameterSource(i) {
                      @Override
                      public Object getValue(String paramName) throws IllegalArgumentException {
                        Object value = super.getValue(paramName);
                        if (value instanceof Gender gender) {
                          return gender.getValue();
                        }
                        return value;
                      }
                    })
            .toArray(SqlParameterSource[]::new));
  }

  @Override
  public int[][] sectionBatchUpdate(String sql, Collection<User> users) {
    final var sublist = new AtomicInteger();
    var result = new ArrayList<int[]>();
    var futures =
        users.stream()
            .collect(Collectors.groupingBy(t -> sublist.getAndIncrement() / Constant.BATCH_SIZE))
            .values()
            .stream()
            .map(
                sectionList ->
                    CompletableFuture.supplyAsync(() -> batchUpdate(sql, sectionList))
                        .whenComplete((r, e) -> result.add(r)))
            .toArray(CompletableFuture[]::new);

    CompletableFuture.allOf(futures).join();

    return result.toArray(new int[0][]);
  }
}

批量插入及更新

?rewriteBatchedStatements=true

useServerPrepStmts在默认情况下已经为false,即使将其更改为true也不会对批处理插入性能产生太大影响。

可以通过切换mysql日志记录(通过SET global general_log = 1)来观察它,该日志记录会将发送到mysql服务器的每个语句登录到一个文件中。

如果您确实需要速度,请使用LOAD DATA INFILE从一个用逗号分隔的文件中加载数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public int[] batchUpdateUsingJdbcTemplate(List<Employee> employees) {
    return jdbcTemplate.batchUpdate("INSERT INTO EMPLOYEE VALUES (?, ?, ?, ?)",
        new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setInt(1, employees.get(i).getId());
                ps.setString(2, employees.get(i).getFirstName());
                ps.setString(3, employees.get(i).getLastName());
                ps.setString(4, employees.get(i).getAddress();
            }
            @Override
            public int getBatchSize() {
                return 50;
            }
        });
}
//  If the batch is too big, we can split it by a smaller batch size.
public int[][] batchInsert(List<Book> books, int batchSize) {

        int[][] updateCounts = jdbcTemplate.batchUpdate(
                "insert into books (name, price) values(?,?)",
                books,
                batchSize,
                new ParameterizedPreparedStatementSetter<Book>() {
                    public void setValues(PreparedStatement ps, Book argument) throws SQLException {
                        ps.setString(1, argument.getName());
                        ps.setBigDecimal(2, argument.getPrice());
                    }
                });
        return updateCounts;

    }

SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(employees.toArray());
int[] updateCounts = namedParameterJdbcTemplate.batchUpdate(
    "INSERT INTO EMPLOYEE VALUES (:id, :firstName, :lastName, :address)", batch);
return updateCounts;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Autowired
  private NamedParameterJdbcTemplate namedJdbcTemplate;

  @Value("${jdbc.batch_insert_size}")
  private int batchSize;

  private static final ExecutorService executor = Executors.newFixedThreadPool(10);

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void batchInsertAsync(List<User> users) throws InterruptedException, ExecutionException {
    StopWatch timer = new StopWatch();

    String sql = "INSERT INTO `USER` (USERNAME, PASSWORD, CREATEDTIME, UPDATEDTIME, USERTYPE, DATEOFBIRTH)"
        + " VALUES(?,?,?,?,?,?)";

    final AtomicInteger sublists = new AtomicInteger();

    CompletableFuture[] futures = users.stream()
        .collect(Collectors.groupingBy(t -> sublists.getAndIncrement() / batchSize))
        .values()
        .stream()
        .map(ul -> runBatchInsert(ul, sql))
        .toArray(CompletableFuture[]::new);

    CompletableFuture<Void> run = CompletableFuture.allOf(futures);

    timer.start();
    run.get();
    timer.stop();

    log.info("batchInsertAsync -> Total time in seconds: " + timer.getTotalTimeSeconds());
  }

  public CompletableFuture<Void> runBatchInsert(List<User> users, String sql) {
    return CompletableFuture.runAsync(() -> {
      jdbcTemplate.batchUpdate(sql, new UserBatchPreparedStatementSetter(users));
    }, executor);
  }

流式查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
jdbcTemplate.query(
        con -> {
          PreparedStatement preparedStatement =
              con.prepareStatement(
                  "select * from user", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
          preparedStatement.setFetchSize(Integer.MIN_VALUE);
          preparedStatement.setFetchDirection(ResultSet.FETCH_FORWARD);
          return preparedStatement;
        },
        rs -> {
          while (rs.next()) {
            System.out.println(USER_ROW_MAPPER.mapRow(rs, rs.getRow()));
          }
        });

    Stream<User> userStream =
        jdbcTemplate.queryForStream(
            conn -> {
              var ps =
                  conn.prepareStatement(
                      "SELECT * FROM user",
                      ResultSet.TYPE_FORWARD_ONLY,
                      ResultSet.CONCUR_READ_ONLY);
              ps.setFetchSize(Integer.MIN_VALUE);
              ps.setFetchDirection(ResultSet.FETCH_FORWARD);
              return ps;
            },
            USER_ROW_MAPPER);

RowMapper<User> USER_ROW_MAPPER =
      (rs, rowNum) ->
          new User()
              .setId(rs.getInt("id"))
              .setAge(rs.getInt("age"))
              .setName(rs.getString("name"))
              .setBalance(rs.getBigDecimal("balance"))
              .setSex(Gender.getByValue(rs.getInt("sex")))
              .setStatus(
                  EnumUtil.getBy(
                      Status.class,
                      i -> {
                        try {
                          return i.ordinal() == rs.getInt("status");
                        } catch (SQLException e) {
                          throw new RuntimeException(e);
                        }
                      }))
              .setCreateTime(rs.getObject("create_time", OffsetDateTime.class))
              .setUpdateTime(rs.getObject("update_time", OffsetDateTime.class));

枚举

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
new BeanPropertySqlParameterSource(i) {
  @Override
  public Object getValue(String paramName) throws IllegalArgumentException {
    Object value = super.getValue(paramName);
    if (value instanceof Gender gender) {
      return gender.getValue();
    }
    return value;
  }
}

单元测试 @JdbcTest

  • 在不使用 Spring Data JDBC 并且只需要 DataSource 的测试中使用
  • @JdbcTest 创建一个 JdbcTemplate 用于测试内存嵌入式数据库设置
  • 不扫描普通的 @ConfigurationProperties 和 @Component bean。
  • 默认情况下,JDBC 为每个测试测试回滚
  • 如果想使用实际的数据库,请使用 @AutoConfigureTestDatabase
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@JdbcTest
@Sql({"schema.sql", "test-data.sql"})
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyTransactionalTests {
  @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    void whenInjectInMemoryDataSource_thenReturnCorrectEmployeeCount() {
        EmployeeDAO employeeDAO = new EmployeeDAO();
        employeeDAO.setJdbcTemplate(jdbcTemplate);

        assertEquals(4, employeeDAO.getCountOfEmployees());
    }
}

附录