Spring WebFlux 使用 R2DBC 访问 MySQL

一、介绍

Reactive Programming

响应式编程指的是数据驱动的、异步和并发的编程范式。简而言之,异步数据流编程。对于数据流进行创建、组合、过滤、转换等操作,最终得到所需要的处理和结果。典型的框架有 RxJava、Reactor 等。

WebFlux

WebFlux 是 Spring Framework 提供的新一代 Web 开发框架,区别于 Spring MVC,WebFlux 提供了非阻塞的、基于 Reactive 技术栈的开发框架,以发挥出多核编程的优势。

两者的异同如下:


WebFlux.jpeg

MySQL 支持

直到 Spring Boot 2.3.0.RELEASE,才正式支持基于 r2dbc 的 MySQL 驱动。

本文使用的框架、环境和工具如下:

二、创建工程

本文基于 IntelliJ IDEA 来创建工程,读者也可以自行基于 spring initializr 来创建。

1. 新建工程

点击菜单中【File】→【New】→ 【Project...】,出现以下对话框,选择 Spring initializr:

1.png

说明

  • JDK: 选择 JDK 8 及以上,建议使用 OpenJDK,以避免法律风险
  • 从 2020.1 开始, IntelliJ IDEA 已经自带有 JDK 下载功能,不需要再独立下载和安装
    在 Project SDK 栏中下拉,选择 Download JDK...,出现下面的对话框,选择合适的版本下载即可。


    2.png

2. 填写项目信息

点击 Next,进入填写项目信息界面。

3.png

说明

  • Group 和 Artifact:根据需要修改
  • Type:保持 Maven Project

3. 选择特性

点击 Next,进入选择特性界面。


4.png

说明
选择以下组件:

  • Developer Tools
    • Lombok
  • Web
    • Spring Reactive Web
  • SQL
    • Spring Data R2DBC
    • MySQL Driver

4. 设置项目名称和目录界面

点击 Next,进入设置项目名称和目录界面,保持默认即可。

5.png

5. 生成项目

点击 Finish,生成项目,进入 IDE,典型的 Spring Boot 项目结构。

6.png

三、准备 MySQL 数据

在开始写代码之前,先准备好测试用的数据环境。本文使用的是 MySQL 8.0.19。MySQL 社区版的官方下载地址是 MySQL Community Server

安装 MySQL

如果有现成的 MySQL 环境,请跳过此步骤。

  • Windows 用户:直接下载 MySQL Installer for Windows,安装即可。
  • Mac 用户: 使用 homebrew 安装更为便捷,命令如下:
brew install mysql

创建 database

首先,创建 database spring_r2dbc_samples,并创建用户 spring_r2dbc_samples_user,脚本如下:

CREATE DATABASE `spring_r2dbc_samples` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';

CREATE USER `spring_r2dbc_samples_user`@`%` IDENTIFIED WITH mysql_native_password BY 'B55!3Ufhj';
CREATE USER `spring_r2dbc_samples_user`@`localhost` IDENTIFIED WITH mysql_native_password BY 'B55!3Ufhj';

GRANT all privileges ON `spring_r2dbc_samples`.* TO `spring_r2dbc_samples_user`@`%`;
GRANT all privileges ON `spring_r2dbc_samples`.* TO `spring_r2dbc_samples_user`@`localhost`;

flush privileges;

创建表

本文所使用的表的 ER 图如下:


7.png

基础字段

字段代码 字段名称 说明
id 编号 主键,自增
remark 备注 可选
active 有效标志 缺省为 1
createdAt 创建时间 默认为 CURRENT_TIMESTAMP
createdBy 创建人
updatedAt 更新时间 默认为 CURRENT_TIMESTAMP,记录更新时自动更新
updatedBy 更新人

学生表

除了基础字段,还包含以下主要字段:

字段代码 字段名称 说明
code 学号
name 姓名
gender 性别 M: 男 F: 女
birthday 生日
address 家庭地址

表创建脚本


ALTER TABLE `spring_r2dbc_samples`.`Student` DROP INDEX `idx_main`;

DROP TABLE `spring_r2dbc_samples`.`Student`;

CREATE TABLE `spring_r2dbc_samples`.`Student`  (
    `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `code` varchar(50) NOT NULL,
    `name` varchar(50) NOT NULL,
    `gender` char(1) NOT NULL,
    `birthday` date NOT NULL,
    `address` varchar(300) NULL,
    `remark` varchar(1000) NULL,
    `active` tinyint NOT NULL DEFAULT 1,
    `createdAt` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `createdBy` varchar(50) NOT NULL,
    `updatedAt` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
    `updatedBy` varchar(50) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE INDEX `idx_main`(`code`)
);

准备测试数据

-- Student

delete from Student;

insert into Student(code, name, gender, birthday, address, createdBy, updatedBy)
values
      ('S0001', 'Tom', 'M', '2001-03-05', null, 'TEST', 'TEST')
    , ('S0002', 'Ted', 'M', '2001-06-12', null, 'TEST', 'TEST')
    , ('S0003', 'Mary', 'F', '2001--9-12', 'Chicago', 'TEST', 'TEST')
;

四、访问数据

配置数据源

首选,把 /src/main/resources/application.properties 改名为 application.yml,即 YAML 格式。这个纯属个人喜好,觉得配置起来结构更清晰点。YAML 早先是随着 RubyRuby on Rails 的流行而流行起来的,简单直接,比起 json 少了括号和双引号,作为配置文件,还是非常不错的。

修改配置文件 application.yml,加入以下配置:

spring:
  r2dbc:
    url: r2dbcs:mysql://localhost:3306/spring_r2dbc_samples?sslMode=DISABLED
    username: spring_r2dbc_samples_user
    password: B55!3Ufhj

创建实体类

  • 创建子 package entity
  • 创建实体类 Student
  • 使用 Lombok@Data 来使得 Student 类可访问
  • @ReadOnlyProperty 的作用是防止代码修改创建时间和更新时间,这个会由 MySQL 自动完成
  • 目前 R2DBC 尚不支持 Audit 功能,所以 createdBy 和 updatedBy 还不能自动设置

代码如下:

package com.example.webfluxmysqldemo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Data
public class Student {
    @Id
    private Long id;

    private String code;
    private String name;
    private String gender;
    private LocalDate birthday;
    private String address;

    private String remark;
    private boolean active;

    @ReadOnlyProperty
    private LocalDateTime createdAt;
    private String createdBy;

    @ReadOnlyProperty
    private LocalDateTime updatedAt;
    private String updatedBy;
}

创建仓库类

仓库(repository) 类似于原先的 dao 的角色,主要提供各种底层数据访问功能。Spring Data JPA 中首选推出了 repository 的概念, Spring Data R2DBC 也基本沿用,但是功能上没有 JPA 那么强大。

  • 创建子 package repository
  • 创建仓库类 StudentRepository,继承自 ReactiveCrudRepository

代码如下:

package com.example.webfluxmysqldemo.repository;

import com.example.webfluxmysqldemo.entity.Student;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface StudentRepository extends ReactiveCrudRepository<Student, Long> {
}

创建控制器

  • 创建子 package controller
  • 创建控制器 StudentController,并映射到 /api/students
  • 注入 StudentRepository
  • 创建方法,获取所有学生

代码如下:

package com.example.webfluxmysqldemo.controller;

import com.example.webfluxmysqldemo.entity.Student;
import com.example.webfluxmysqldemo.repository.StudentRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/students")
public class StudentController {
    private final StudentRepository studentRepository;

    public StudentController(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

        @GetMapping("")
    public Flux<Student> index() {
        return studentRepository.findAll();
    }
}

编译和启动


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2020-06-07 16:22:27.000  INFO 65817 --- [           main] c.e.w.WebfluxMysqlDemoApplication        : Starting WebfluxMysqlDemoApplication on Arthur-MacBook-Pro.local with PID 65817 (/Users/arthur/github/arthurlee/webflux-mysql-demo/target/classes started by arthur in /Users/arthur/github/arthurlee/webflux-mysql-demo)
2020-06-07 16:22:27.002  INFO 65817 --- [           main] c.e.w.WebfluxMysqlDemoApplication        : No active profile set, falling back to default profiles: default
2020-06-07 16:22:27.648  INFO 65817 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2020-06-07 16:22:27.737  INFO 65817 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 82ms. Found 1 R2DBC repository interfaces.
2020-06-07 16:22:28.692  INFO 65817 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2020-06-07 16:22:28.701  INFO 65817 --- [           main] c.e.w.WebfluxMysqlDemoApplication        : Started WebfluxMysqlDemoApplication in 2.167 seconds (JVM running for 2.934)

访问数据

使用 http://localhost:8080/api/students 访问接口,返回数据如下:
以下数据是使用 Postman 来

[
    {
        "id": 4,
        "code": "S0001",
        "name": "Tom",
        "gender": "M",
        "birthday": "2001-03-05",
        "address": null,
        "remark": null,
        "active": true,
        "createdAt": null,
        "createdBy": null,
        "updatedAt": null,
        "updatedBy": null
    },
    {
        "id": 5,
        "code": "S0002",
        "name": "Ted",
        "gender": "M",
        "birthday": "2001-06-12",
        "address": null,
        "remark": null,
        "active": true,
        "createdAt": null,
        "createdBy": null,
        "updatedAt": null,
        "updatedBy": null
    },
    {
        "id": 6,
        "code": "S0003",
        "name": "Mary",
        "gender": "F",
        "birthday": "2001-09-12",
        "address": "Chicago",
        "remark": null,
        "active": true,
        "createdAt": null,
        "createdBy": null,
        "updatedAt": null,
        "updatedBy": null
    }
]

出现问题

\color{red}{发现返回数据中,createAt、createdBy、updatedAt、updatedBy 四个字段没有返回值。}查询资料后,发现缺省情况下,createAt 会转换成数据库字段 created_at,所以没有成功映射。

解决问题

定位问题后,可以添加一个配置,自定义命名转换规则。

  • 创建子 package config
  • 添加配置类 R2dbcConfig (名称随意)
  • 添加 Bean,返回 NamingStrategy

代码如下:

package com.example.webfluxmysqldemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.relational.core.mapping.NamingStrategy;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;

@Configuration
public class R2dbcConfig {
    @Bean
    public NamingStrategy namingStrategy() {
        return new NamingStrategy() {
            @Override
            public String getColumnName(RelationalPersistentProperty property) {
                return property.getName();
            }
        };
    }
}

重新运行和验证

重新运行,访问接口,得到数据如下,问题解决

[
    {
        "id": 4,
        "code": "S0001",
        "name": "Tom",
        "gender": "M",
        "birthday": "2001-03-05",
        "address": null,
        "remark": null,
        "active": true,
        "createdAt": "2020-06-07T15:47:07",
        "createdBy": "TEST",
        "updatedAt": "2020-06-07T15:47:07",
        "updatedBy": "TEST"
    },
    {
        "id": 5,
        "code": "S0002",
        "name": "Ted",
        "gender": "M",
        "birthday": "2001-06-12",
        "address": null,
        "remark": null,
        "active": true,
        "createdAt": "2020-06-07T15:47:07",
        "createdBy": "TEST",
        "updatedAt": "2020-06-07T15:47:07",
        "updatedBy": "TEST"
    },
    {
        "id": 6,
        "code": "S0003",
        "name": "Mary",
        "gender": "F",
        "birthday": "2001-09-12",
        "address": "Chicago",
        "remark": null,
        "active": true,
        "createdAt": "2020-06-07T15:47:07",
        "createdBy": "TEST",
        "updatedAt": "2020-06-07T15:47:07",
        "updatedBy": "TEST"
    }
]

五、参考

后续

本文引领大家进入 R2DBC的世界,搭建出了一个可运行的最小项目,后续将着重介绍 R2DBC 提供的各种功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343