一、BaseCloud概述
Base4Cloud主要用于数据库的操作,引用最新base4cloud依赖
<dependency>
<groupId>com.zdetech.base4cloud</groupId>
<artifactId>base4cloud</artifactId>
<version>1.0.0</version>
<classifier>small</classifier>
</dependency>
本次使用的Base4Cloud与之前的BaseDao主要不同:
特点 | Base4Cloud | BaseDao |
---|---|---|
编程方式 | Reactor3非阻塞响应式编程 | 阻塞式编程 |
Web框架 | Spring WebFlux | Spring Web MVC |
数据框架 | Spring Data R2DBC | Spring Data JPA |
非阻塞响应式编程与传统的阻塞式编程有很大不同,下面以BaseDao与BaseCloud比较。
现需要给部门ID为1的人员添加角色ID2,因此需要批量插入用户-角色表。
用户表(sys_user):
字段名 | 类型 | 说明 |
---|---|---|
user_id | bigint | 用户ID |
username | varchar(64) | 用户名 |
password | varchar(255) | 密码 |
birthday | Date | 出生日期 |
salary | decimal | 工资 |
dept_id | bigint | 部门ID |
用户-角色表(sys_user_role):
字段名 | 类型 | 说明 |
---|---|---|
user_id | bigint | 用户ID |
role_id | bigint | 角色ID |
使用BaseDao传统阻塞式编程:
SysUser where = new SysUser();
where.setDeptId(1L);
List<SysUser> list = baseDao.findByFields(where); // 1
for (int i=0; i < list.size(); i++) { // 2
SysUser user = list.get(i); // 3
SysUserRole sysUserRole = new SysUserRole(); // 4
sysUserRole.setUserId(user.getId());
sysUserRole.setRoleId(2L);
baseDao.save(sysUserRole);
}
- 设置查询条件dept_id=1, 根据查询条件查询出用户实体列表
- 遍历查询出的用户
- 得到一个查询出的用户
- 根据用户ID创建用户-角色实体,设置用户ID为查询出的用户ID, 设置角色ID为2, 保存用户-角色实体
使用Base4Cloud非阻塞响应式编程
SysUser where = new SysUser();
where.setDeptId(1L);
return baseDAO.findByFields(where) // 1
.flatMap(user -> { // 2
SysUserRole userRole = new SysUserRole();
userRole.setUserId(user.getUserId());
userRole.setRoleId(2L);
return baseDAO.save(userRole); // 3
});
- 设置查询条件dept_id=1, 根据查询条件查询出用户实体
-
flatMap
操作符依次遍历上一步的结果, 参数user是SysUser实体对象 - 根据用户ID创建用户-角色实体,设置角色ID为2, 保存用户-角色实体, 一定要return, 否则不能执行任何SQL语句
非阻塞响应式编程与传统阻塞式编程不同的是, 所有操作返回的是一个Mono(单值)或Flux(多值), 后续操作是使用Mono或Flux的操作符如:map
flatMap
filter
then
reduce
zipWith
switchIfEmpty
等来实现编程逻辑, 而这些操作符调用的返回依然是Mono或Flux, 所以会一直调用不同的操作符, 直到 return
。需要熟悉这些操作符才能完成编程逻辑。
下面对一些常用操作符举例说明:
-
flatMap
对上一步操作的数据进行迭代(循环)操作,前面已有flatMap
的例子。
-
then
thenMany
thenReturn
then
、thenRetutn
、thenMany
都表示等上一步完成后,忽略上一步的操作结果,直接进行其他操作。
比如上面代码,如果后续还有其他操作,比如保存了用户-角色后,再删除用户ID为1,2,3的用户,可这样写:
SysUser where = new SysUser();
where.setDeptId(1L);
return baseDAO.findByFields(where)
.flatMap(user -> {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(user.getUserId());
userRole.setRoleId(2L);
return baseDAO.save(userRole);
})
.thenMany(baseDAO.findByListIn(SysUser.class, "user_id", Arrays.asList(1, 2, 3))) // 1
.flatMap(baseDAO::delete); // 2
-
thenMany
操作符忽略上一步的数据,直接执行一次查询ID为1,2,3的用户。
此处findByListIn
方法返回是Flux(多值),所以用thenMany
操作符,如果是返回Mono(单值),则应该使用then
操作符。
注意:这儿不能用flatMap
操作符,因为flatMap
是对上一步return baseDAO.save(userRole)
的结果Flux<SysUserRole>
进行迭代,假设插入的用户-角色记录有3条(因为部门ID为1的的用户有3个),则会执行三次baseDAO.findByListIn(SysUser.class, "user_id", Arrays.asList(1, 2, 3))
,这显然是不对的。 - 遍历删除查询出来的用户,此处双冒号::写法
baseDAO::delete
是user -> baseDAO.delete(user)
的简写。
-
collectList
map
如果后面确实要使用上一步return baseDAO.save(userRole)
的结果数据,但只想执行一次操作而不是迭代,可用collectList()
操作符,把上一步结果Flux
转为Mono<List>
再继续调用flatMap
操作符。代码如下:
SysUser where = new SysUser();
where.setDeptId(1L);
return baseDAO.findByFields(where)
.flatMap(user -> {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(user.getUserId());
userRole.setRoleId(2L);
return baseDAO.save(userRole);
})
.map(userRole -> userRole.getUserid()) // 1
.collectList() // 2
.flatMap(ids -> // 3
baseDAO.deleteByListIn(SysUser.class,"user_id", ids)) // 4
- 遍历上一步结果将SysUserRole对象
map
为userId,map
和flatMap
类似都是迭代操作。这儿没用flatMap
,是因为flatMap
要求返回Mono或Flux,就要冗余的写成flatMap(userRole -> Mono.just(userRole.getUserid()))
。如果是调用baseDAO方法,它返回的是Mono或Flux,就要用flatMap
。 - 使用
collectList
把上一步的Flux转为Mono<List>,后面使用flatMap
就只执行一次,而不是遍历多次 - 此处是List对象,就是插入的SysUserRole的userid列表List<Long>
-
baseDAO.deleteByListIn
方法根据ids用IN条件批量删除用户
上面代码只是演示操作符用法,实际不可能这么干。实现的逻辑是:查询部门ID1下的用户,分别给每个用户新增用户-角色表记录,角色ID为2,最后根据新增的用户-角色记录的用户ID在用户表中删除用户记录。
-
zip
zipWith
reduce
cache
前面例子都是从上一步获取一个数据进行处理的情况,如果有一个操作需要多个数据合并起来处理怎么办呢?这时就用到了zip
或zipWith
操作符了,zip
操作符能把多个Flux或Mono合并在成Tuple类型,比如2个合并成Tuple2
,3个合并成Tuple3
,n个合并成TupleN
。TupleN
有getT1
,getT2
,getT3
...getTn
等方法分别获取各个数据。
比如,我们想要从用户表中统计出用户数、平均工资、平均年龄,代码如下:
Flux<User> result = baseDAO.findAll(User.class).cache(); // 1
Mono<Double> totalSalary = result.reduce(0.0, (sum, user) -> sum + user.getSalary()); // 2
Mono<Integer> totalAge = result.reduce(0, (sum, user) -> sum + user.getBrithday().until(LocalDate.now()).getYears()); // 3
Mono<Long> count = result.count(); // 4
return Mono.zip(totalSalary, totalAge, count) // 5
.map(data -> { // 6
JSONObject res = new JSONObject();
res.put("cnt", data.getT3());
res.put("avgSalary", data.getT1() / data.getT3());
res.put("avgAge", data.getT2() / data.getT3());
return res;
})
- 查询所有用户,保存结果到result。这儿用到了
cache
缓存方法,因为后面会3次用到result,如果不加上cache,每次引用result会再次执行findAll查询,就会执行3次findAll查询。所以如果有一个操作结果后面会多次使用,要加上cache
是必须的。 - 使用
reduce
操作符汇总用户工资总额。 - 使用
redeuce
操作符汇总用户年龄总数。 - 使用
count
操作符获取用户数。 - zip操作符合并前面三个结果(工资总额(T1)、年龄总数(T2)、用户数(T3))数据成Tuple3对象。
- 计算平均工资、平均年龄、用户数建立JSONObject对象。
返回结果如下:
{
"cnt": 4,
"avgAge": 34,
"avgSalary": 275.0
}
上面只是为了说明操作符用法,其实可以在sys_sqlds
表配置SQL调用baseDAO.getData
方法更简单。
-
groupBy
reduce
上例是统计全体人员,如果要按部门分组统计平均工资、平均年龄、部门人数呢?需要用到groupBy
操作符,代码如下:
Flux<GroupedFlux<Long, User>> group = baseDAO.findAll(User.class)
.groupBy(User::getDepId); // 1
return group.flatMap(groupedFlux -> groupedFlux.reduce(new JSONObject(), (result, user) -> { // 2
result.put("dept_id", groupedFlux.key());
result.put("cnt", result.getIntValue("cnt") + 1);
result.put("salary", result.getDoubleValue("salary") + user.getSalary());
result.put("age", result.getIntValue("age") + user.getBrithday().until(LocalDate.now()).getYears());
return result;
}))
.map(data -> { // 3
JSONObject obj = new JSONObject();
obj.put("dept_id", data.getIntValue("dept_id"));
obj.put("cnt", data.getIntValue("cnt"));
obj.put("avgSalary", data.getDoubleValue("salary") / data.getIntValue("cnt"));
obj.put("avgAge", data.getIntValue("age") / data.getIntValue("cnt"));
return obj;
})
.collectList();
- 使用groupBy操作符,按员工的dept_id分组,注意分组后的结果类型是Flux<Flux<User>>,即结果本身是一个Flux(多值),而它的每一项也是一个Flux(多值),这个Flux每一项目是User对象,表示同一部门的员工。即数据如下:
[
{ // 部门1
[
{id: 1, username : 'li', birthday: '1979-08-05', salary: 500},
{id: 2, username : 'wang', birthday: '1992-08-25', salary: 200}
],
key: 1 // dept_id=1
},
{ // 部门2
[
{id: 3, username : 'zhang', birthday: '1990-04-25', salary: 300},
{id: 4, username : 'liu', birthday: '1991-03-25', salary: 100}
],
key: 2// dept_id=2
}
]
- 上一步
groupBy
生成的是Flux<Flux<User>>类型,使用flatMap
操作符遍历Flux<Flux<User>>每一项,每一项目数据类型是Flux<User>,所以这儿groupedFlux数据类型就是Flux<User>。使用groupedFlux的reduce
操作符,也就是对它的每一项User做汇总操作,得到员工数cnt,工资总额salary,年龄总数age,组成一个JSONObject对象。 - 计算平均工资、平均年龄,生成JSONObject对象
执行结果:
[
{"cnt":2,"avgAge":36,"avgSalary":350.0,"dept_id":1},
{"cnt":2,"avgAge":31,"avgSalary":200.0,"dept_id":2}
]
上面只是为了说明操作符用法,其实可以在sys_sqlds
表配置SQL调用baseDAO.getData
方法更简单。
总结:非阻塞响应式编程会用到Mono或Flux的各种操作符实现编程逻辑,请熟悉各种操作符。操作符详情说明可参考Reactor3开发参考文档和Reactor3的API文档
二、生成实体类, 一个实体类对应一张表
-
添加数据源,根据你的需要选择你使用的数据库,如MySQL或Oracle。
image - 第一次生成需要修改Generated POJOs.groovy脚本, 脚本会给
password
和kzzf
kzsz
kzsj
kzid
开头的字段加上@JsonIgnore
注解,接口将忽略这些字段,如需要指定字段,请去掉此注解。
右键在数据源中选择要生成的实体表, Tools -> Scripted Extensions -> Go To Scripts Directory, 打开脚本修改如下:
import com.intellij.database.model.DasTable
import com.intellij.database.util.Case
import com.intellij.database.util.DasUtil
/*
* Available context bindings:
* SELECTION Iterable<DasObject>
* PROJECT project
* FILES files helper
*/
packageName = ""
typeMapping = [
(~/(?i)int/) : "Long",
(~/(?i)float|double|decimal|real/): "Double",
(~/(?i)datetime|timestamp/) : "java.time.LocalDateTime",
(~/(?i)date/) : "java.time.LocalDate",
(~/(?i)time/) : "java.time.LocalTime",
(~/(?i)/) : "String"
]
FILES.chooseDirectoryAndSave("Choose directory", "Choose where to store generated files") { dir ->
SELECTION.filter { it instanceof DasTable }.each { generate(it, dir) }
}
def generate(table, dir) {
def className = javaName(table.getName(), true)
def fields = calcFields(table)
packageName = getPackageName(dir)
new File(dir, className + ".java").withPrintWriter("utf-8") { out -> generate(out, className, fields, table) }
}
// 获取包所在文件夹路径
def getPackageName(dir) {
return dir.toString().replaceAll("\\\\", ".").replaceAll("/", ".").replaceAll("^.*src(\\.main\\.java\\.)?", "") + ";"
}
def isNotEmpty(content) {
return content != null && content.toString().trim().length() > 0
}
def generate(out, className, fields, table) {
out.println "package $packageName"
out.println ""
out.println "import com.fasterxml.jackson.annotation.JsonIgnore;"
out.println "import org.springframework.data.annotation.Version;"
out.println "import io.swagger.v3.oas.annotations.media.Schema;"
out.println "import org.springframework.data.relational.core.mapping.Table;"
out.println "import org.springframework.data.annotation.Id;"
out.println "import org.springframework.data.relational.core.mapping.Column;"
out.println "import lombok.Data;"
out.println ""
out.println "@Data"
out.println "@Table(\"" + table.getName() + "\")"
out.println "public class $className {"
fields.each() {
out.println ""
if (isNotEmpty(it.comment)) {
out.println "\t@Schema(description = \"${it.comment.toString()}\")"
}
if (it.annos != "") out.println "\t${it.annos}"
out.println "\t@Column(\"${it.columnName}\")"
out.println "\tprivate ${it.type} ${it.name};"
}
out.println ""
out.println "}"
}
def calcFields(table) {
def pkCnt = 0
DasUtil.getColumns(table).reduce([]) { fields, col ->
def spec = Case.LOWER.apply(col.getDataType().getSpecification())
def typeStr = typeMapping.find { p, t -> p.matcher(spec).find() }.value
if (DasUtil.isPrimary(col)) {
pkCnt++
}
def annos = "";
if (col.getName() == "password" || col.getName().startsWith("kzzf") || col.getName().startsWith("kzsz") || col.getName().startsWith("kzsj") || col.getName().startsWith("kzid")) {
annos = "@JsonIgnore"
} else if (col.getName() == "version") {
annos = "@Version"
} else if (DasUtil.isPrimary(col)) {
if (pkCnt < 2) {
annos = "@Id"
} else {
annos = "@javax.persistence.Id"
}
}
fields += [[
name : javaName(col.getName(), false),
type : typeStr,
comment : col.getComment(),
columnName: col.getName(),
annos : annos
]]
}
}
def javaName(str, capitalize) {
def s = com.intellij.psi.codeStyle.NameUtil.splitNameIntoWords(str)
.collect { Case.LOWER.apply(it).capitalize() }
.join("")
.replaceAll(/[^\p{javaJavaIdentifierPart}[_]]/, "_")
capitalize || s.length() == 1 ? s : Case.LOWER.apply(s[0]) + s[1..-1]
}
image.png
-
右键在数据源中选择要生成实体类的表, Tools -> Scripted Extensions -> Generated POJOs.groovy
image.png
三、baseDAO方法
一、查询方法
<T> Flux<T> findAll(Class<T> clazz)
查询clazz实体类型的所有记录,比如查询所有用户表记录:
return baseDAO.findAll(SysUser.class)
. map(user -> {
System.out.println(user);
return user;
});
<T> Flux<T> findByFields(T where)
根据where条件查询, 如: 查询部门ID为1,用户名为leonine的用户
SysUser where = new SysUser();
where.setDeptId(1L);
where.setUsername("leonine");
return baseDAO.findByFields(where)
. map(user -> {
System.out.println(user);
return user;
});
<T> Mono<T> findByIds(T entity)
用于查询联合主键的表, entity为主键对象, 所有主键都要赋值, 否则报错。比如查询:user_id=1 role_id=2
的用户-角色。
SysUserRole pk = new SysUserRole();
pk.setUserId(1L);
pk.setRoleId(2L);
return baseDAO.findByIds(pk)
.map(userRole -> {
System.out.println(userRole);
return userRole;
});
<T> Mono<T> findById(Class<T> clazz, ID id)
根据唯一主键查询实体,如查询用户ID为1的SysUser实体:
return baseDAO.findById(SysUser.class, 1L)
.map(user -> {
System.out.println(user);
return user;
});
<T> Flux<T> findByListIn(Class<T> clazz, String fieldName, List list)
根据IN条件查询实体,如查询用户ID为1,2,3的SysUser:
baseDAO.findByListIn(SysUser.class, "user_id", Arrays.asList(1, 2, 3))
.map(user -> {
System.out.println(user);
return user;
});
Mono<JSONObject> getTreeData(String sqlId, JSONObject params)
返回树结构对象,sqlId
为sys_sqlds
表中配置的查询,params
为查询参数,查询参数必传rootId
,返回结果必带parenteid
和id
字段,其他字段随意,SQL如:
WITH RECURSIVE ctetable as (
SELECT dept_id id , parent_id parentid ,name as label from sys_dept where 1 = :rootId
UNION ALL
SELECT dept_id id , parent_id parentid , name as label from sys_dept a join ctetable t2
WHERE a.dept_id =t2.parentid
)
SELECT DISTINCT * FROM ctetable a
JSONObject params = new JSONObject();
params.put("rootId",1);
baseDAO.getTreeData("getdepttree",params)
.map(tree->{
System.out.println(tree);
return tree;
});
<T> Flux<T> getData(String sqlId, Class<T> clazz, JSONObject params)
根据配置的SQL返回查询结果,sqlId
为sys_sqlds
表中配置的查询,clazz
为返回的结果实体类型,如果传null,返回的结果为JSONObject类型,params
为查询参数,SQL如:
select * from sys_user where username = :username
JSONObject params = new JSONObject();
params.put("username","leonine");
baseDAO.getData("getsysuser", SysUser.class, params)
.map(user->{
System.out.println(user);
return user;
});
Flux<JSONObject> getData(String sqlId, JSONObject params)
根据配置的SQL返回查询结果,sqlId
为sys_sqlds
表中配置的查询,返回的结果为JSONObject类型,params
为查询参数,SQL如:
select * from sys_user where username = :username
JSONObject params = new JSONObject();
params.put("username","leonine");
baseDAO.getData("getsysuser", params)
.map(user->{
System.out.println(user);
return user;
});
二、修改方法
<T> Mono<T> save(T entity)
保存实体对象,如果主键有赋值且有记录就修改,否则就插入,自增长的主键在下一个操作符如map
flatMap
filter
等中获取。
SysUser newUser = new SysUser();
newUser.setUsername("leonine");
PasswordEncoder ENCODER = new BCryptPasswordEncoder();
newUser.setPassword(ENCODER.encode("123456"));
newUser.setPhone("17712345678");
return baseDAO.save(newUser)
.map(user -> {
System.out.println("新增用户ID:" + user.getUserId());
return user;
});
<T> Mono<Integer> updateByFields(T entity, T where)
根据where条件批量修改记录,entity为包含新值的对象。
SysUser where = new SysUser();
where.setUsername("leonine");
where.setPhone("17712345678");
SysUser updater = new SysUser();
updater.setUsername("leonine updated");
updater.setMail("mail updated");
return baseDAO.updateByFields(updater,where)
.map(cnt -> {
System.out.println("修改了" + cnt + "条记录");
return cnt;
});
<T> Mono<Integer> updateByListIn(T entity, String fieldName, List list)
fieldName字段名,list字段值列表,entity修改的值对象,下面代码批量修改ID为1,2,3的记录,设置username值为leonine:
SysUser entity = new SysUser();
entity.setUsername("leonine");
return baseDAO.updateByListIn(entity, "user_id", Arrays.asList(1, 2, 3))
.map(cnt -> {
System.out.println("修改了" + cnt + "条记录");
return cnt;
});
<T> Mono<T> delete(T entity)
删除对象,entity对象主键必须赋值
SysUser entity = new SysUser();
entity.setUserId(1L);
return baseDAO.delete(entity)
.map(user -> {
System.out.println(user);
return user;
});
<T> Mono<Integer> deleteByFields(T where)
根据where条件批量删除记录。
SysUser where = new SysUser();
where.setDeptId(1L);
return baseDAO.deleteByFields(where)
.map(cnt -> {
System.out.println("删除了" + cnt + "条记录");
return cnt;
});
<T> Mono<Integer> deleteByListIn(Class<T> clazz, String fieldName, List list)
fieldName字段名,list字段值列表,clazz删除的对象,下面代码批量删除ID为1,2,3的记录:
return baseDAO.deleteByListIn(SysUser.class, "user_id" , Arrays.asList(1, 2, 3))
.map(cnt -> {
System.out.println("删除了" + cnt + "条记录");
return cnt;
});
四、查询getData.do编写
与BaseDao基类编写方式相同,请参考BaseDAO基类使用
简述如下:
在sys_sqlds
表中插入记录,数据如下:
sql_id
: 查询唯一id
sql_ds
: 查询语句说明
sql_server
: 查询语句
查询语句编写与原生写法类似,其中参数用 :<参数名>代替,如
select * from user_password where username = :username and password = :password
五、setData.do业务类编写
业务类接口:
public interface AppService {
Mono<Result> addUser(JSONObject jsonObject, BaseDAO baseDAO);
}
业务类接口方法两个参数:前端请求参数jsonObject
、数据操作对象baseDAO
可调用前面介绍的save
、findById
、delete
等方法,返回类型统一为Mono<Result>
。前端如果传的参数是包含多个jsonObject的数组如[ { username:'leonine', phone:'123' }, { userid: 5 , username:'leo', phone:'456' }, { userid: 1, delete: true } ]
包含3个jsonObject对象,setData.do会每次取一个jsonObject调用,总共执行3次这个方法。
业务类实现:
@Service("appService")
public class AppServiceImpl implements AppService {
@Override
@SuppressWarnings("unchecked")
public Mono<Result> addUser(JSONObject jsonObject, BaseDAO baseDAO) {
SysUser sysUser = jsonObject.toJavaObject(SysUser.class); // 1
boolean delete = jsonObject.getBooleanValue("delete"); // 2
if (delete) {
return baseDAO.delete(sysUser)
.flatMap(BaseResult::ok); // 3
} else {
if (sysUser.getUsername() == null) { // 4
return BaseResult.failed("用户名不能为空");
}
return baseDAO.save(sysUser) // 5
.flatMap(user -> {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(user.getUserId());
userRole.setRoleId(2L);
return baseDAO.save(userRole) // 6
.thenReturn(user); // 7
})
.flatMap(BaseResult::ok); // 8
}
}
}
- 把jsonObject转为实体对象
- 获取前端的其他实体对象中没包含的参数如
delete
- 删除用户,调用
BaseResul::ok
,返回成功 - 如果用户名为空,调用
BaseResult.failed
方法返回失败提示:用户名不能为空 - 新增或修改用户
- 新增或修改用户-角色记录,用户ID为新增或修改的用户ID,角色ID为2
-
thenReturn
方法用于直接传递用户信息到下一步,如果没有这一句,传递到下一步的将是baseDAO.save(userRole)
的返回结果,就是保存的SysUserRole对象(用户-角色)记录信息 - 调用
BaseResul::ok
,返回成功到前端,也就是json:{code: 0 , msg :'处理成功', data: <用户信息>}
所有操作最后以BaseResult.ok
方法或BaseResult.failed
结束。成功调用BaseResult.ok
,失败调用BaseResult.failed
,有多种参数形式,具体看代码提示。
比如返回失败BaseResult.failed(5, "用户不存在")
表示错误码5,错误描述:用户不存在
而.flatMap(BaseResult::ok)
实际上是.flatMap(result -> BaseResult.ok(result)
的简写,返回json:{code: 0 , msg :'处理成功', data: <result的值>}
网友评论