上一篇的当务之急就是还没有响应式的数据库,JPA无法直接返回Flux或者Mono类型。
目前Spring 响应式支持的数据库有Cassandra,MongoDB,Couchbase和Redis。
非响应式数据库
非响应式数据库的主要支持来自于Spring Data JPA,遗憾的是,这些关系型数据库都不是响应式的。
如果一定要使用,可以在查询到数据后将其转换成响应式,或者响应式数据到达订阅者的时候,将其转换成非响应式数据:
List<Order> orders = repo.findByUser(someUser);
Flux<Order> orderFlux = Flux.fromIterable(orders);
Order order repo.findById(Long id);
Mono<Order> orderMono = Mono.just(order);
响应式数据转换成非响应式数据:
Taco taco = tacoMono.block();
tacoRepo.save(taco);
Iterable<Taco> tacos = tacoFlux.toIterable();
tacoRepo.saveAll(tacos);
一般也可以采用订阅的形式:
tacoFlux.subscribe(taco -> {
tacoRepo.save(taco);
});
注意,这些语句,在查询数据库,保存数据和转换成非响应式数据的时候,都是阻塞的。
如果一个Web应用想要是响应式的,必须从控制器到数据库全部是响应式的,所以要发挥响应式的真正作用,还是赶快来看看真正的响应式吧。
SIA5里提供了Cassandra和MongoDB的例子。不过我打算学习MongoDB和Redis,这里先按照书把MongoDB学一下并运用,之后的Redis要自学补上这一章,毕竟Redis太好用了。
使用MongoDB
MongoDB是一个文档数据库,实际上MongoDB存储的是二进制JSON文件,也被称为BSON,可以像SQL数据库一样查询取出来。
Spring Initializr提供了两个依赖,注意选择Reactive的那个:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-data-mongodb-reactive
</artifactId>
</dependency>
注意,和其他的响应式数据库一样,这是JPA的替代品,所以需要把JPA,H2数据库等一并从依赖里去掉,或者单独创建一个新项目来试验一下。
导入依赖后的默认MongoDB会运行在本机的27017端口,为了方便也可以使用集成的MongoDB,可以在Spring Initializr中勾选,也可以单独添加依赖:
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
与H2一样,所有数据在应用停止之后会全部消失。
MongoDB的配置与其他数据库很相似:
spring.data.mongodb.host
,主机地址,默认localhost
spring.data.mongodb.port
,端口,默认27017
spring.data.mongodb.username
,用户名
spring.data.mongodb.password
,密码
spring.data.mongodb.database
,数据库名称,默认是test
Java类与MongoDB数据库的映射关系
Spring Data MongoDB提供了很多注解,不过只有三个是最常用的:
@Id
,标记文档ID
@Document
,标记一个文档
@Field
,标记一个域(可定义顺序),就是个键名,存入文档中。
对于每一个映射类,@Id
和@Document
是一定要有的,不标记@Field
的属性会默认有同名属性。
把Student类改成MongoDB的类:
package cc.conyli.webflux.domain;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@RequiredArgsConstructor
@Document
public class Student implements Serializable {
@Id
private String id;
private String firstName;
private String lastName;
private Integer courseId;
}
现在因为不使用JPA了,所以@Entity
,@Table
之类的注解都没有了,换成了@Document
注解,表示这个类被映射为一个文档。
默认文档的名称是第一个字母小写的类名,不过也可以自定义:@Document(collection="studentds")
。
@Id
可以用在任意可以序列化的域变量上,包括字符串和Long类型。这个id的名称无所谓,只要是字符串类型,在Mongo内就必定会生成_id
键,如果不指定@Id,会自动生成。这个可以看Spring Data MongoDB - Reference Documentation
把Course做一下修改:
package cc.conyli.webflux.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;
@Data
@NoArgsConstructor(force = true)
@Document
public class Course {
@Id
private String id;
@NonNull
@Size(min=5,message = "At least 5 characters long")
private String courseName;
private Date createAt = new Date();
private List<Student> students;
}
比想象的要简单,@Id不像JPA一样只能是INT或者LONG,也不像Cassandra是UUID,可以是任意可序列化的类型。实际上如果选择String类型作为ID,Mongo自动分配一个值存进去(如果是null的话),实际上是获得了一个数据库管理的ID,无需再手动设置。
注意List<Student>,这个不会单独在Mongo中再另外存一个collection,而是会直接把这个列表进行存储。
原书还展示了Order对象和User对象的存储,一样的简单,除了标识ID之外,几乎不用做任何调整。
当然也会有其他高级和不经常使用的映射方法,不过目前先了解这些足够了。
编写MongoDB的神奇接口
有两个接口可以选择:
ReactiveCrudRepository
,使用.save()
方法保存新老数据
ReactiveMongoRepository
,提供了一些insert()
方法,写入的时候更高效
对于不需要经常变动的文档,可以选择前者。需要经常写入的,推荐选择后者p。
写一个简单的学生接口试试:
package cc.conyli.webflux.repository;
import cc.conyli.webflux.domain.Student;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.web.bind.annotation.CrossOrigin;
@CrossOrigin(origins = "*")
public interface StudentRepo extends ReactiveCrudRepository<Student, Integer> {
}
由于这个接口是响应式,注意了,返回的是Flux或者Mono对象,findAll方法会返回Flux<Student>,findById则会返回Mono<Student>。
再来编写一个使用insert的接口:
package cc.conyli.webflux.repository;
import cc.conyli.webflux.domain.Course;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.web.bind.annotation.CrossOrigin;
import reactor.core.publisher.Flux;
@CrossOrigin(origins = "*")
public interface CourseRepo extends ReactiveMongoRepository<Course, Integer> {
Flux<Course> findByOrderByCreateAtDesc();
}
ReactiveMongoRepository
相比ReactiveCrudRepository
唯一的缺点是仅能用于MongoDB,无法移植。
增和查全部的会了,按照id查找的话,需要重写一下神奇接口中的方法,因为默认的方法是按照Int类型查:
@CrossOrigin(origins = "*")
public interface StudentRepo extends ReactiveCrudRepository<Student, Integer> {
Mono<Student> findById(String id);
}
@GetMapping("/students/{id}")
public Mono<Student> getSingleStudent(@PathVariable("id") String id) {
return studentRepo.findById(id);
}
而修改对于MongoDB来说,就是直接找到对应的id然后更改对象。由于是一个流,可以取出来之后使用流操作,比较方便。
这里后续找到了一篇文章,说的比SIA5要完整详细一些响应式Spring的道法术器(Spring WebFlux 快速上手 + 全面介绍)。
准备跟着这个来做一遍了。