1. topfox 框架例子
数据库脚本请参考文件 db.sql, 共3张表: 部门表depts, 用户表users, 多主键字段表 salary
使用数据库为 mysql8
2. topfox 用户使用手册 - 目录
2.1. 必备
- 官网: https://gitee.com/topfox/topfox
- 文中例子源码: https://gitee.com/topfox/topfox-sample
- TopFox技术交流群 QQ: 874732179
2.2. topfox 介绍
在 srpingboot2.x.x 和MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
编程规范参考《阿里巴巴Java开发手册》
借鉴 mybaties plus 部分思想
特性:
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 集成Redis缓存: 自带Redis缓存功能, 支持多主键模式, 自定义redis-key. 实现对数据库的所有操作, 自动更新到Redis, 而不需要你自己写任何代码; 当然也可以针对某个表关闭.
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
- 支持主键自动生成:可自由配置,充分利用Redis提高性能, 完美解决主键问题. 支持多主键查询、修改等
- 内置分页实现:基于 MyBatis 物理分页,开发者无需关心具体操作,写分页等同于普通查询
- 支持devtools/jrebel热部署
- 热加载 支持在不使用devtools/jrebel的情况下, 热加载 mybatis的mapper文件
- 内置全局、局部拦截插件:提供delete、update 自定义拦截功能
- 拥有预防Sql注入攻击功能
- 无缝支持spring cloud: 后续提供分布式调用的例子
3. 更新日志
3.1. 版本1.2.5 更新日志 2019-07-30
- CamelHelper为驼峰和下划线命名互转的处理类
BeanUtil.toUnderlineName 删除, 用 CamelHelper.toUnderlineName 代替
BeanUtil.toCamelCase 删除, 用 CamelHelper.toCamel 代替
3.2. 版本1.2.4 更新日志 2019-07-24
- 全局缓存参数开关
新增 一级缓存开关 top.service.thread-cache
新增 二级缓存开关 top.service.redis-cache
删除 top.service.open-redis
- 多主键的支持, 包括: 更新, 删除, 查询, 数据校验组件, 修改日志组件;
- java远程调用返回空对象的处理;
- 技术文档修改
4. 快速入门
4.1. 入门例子: 以用户表为例, 开发者只需要完成以下4步的代码, 就能实现很多复杂的功能
4.1.1. 新建实体对象 UserDTO
@Setter
@Getter
@Accessors(chain = true)
@Table(name = "users", cnName = "用户表")
public class UserDTO extends DataDTO {
@Id private Integer id;
private String code;
private String name;
private String password;
private String sex;
private Integer age;
...等
}
4.1.2. 新建查询条件对象Query( 即UserQTO )
@Setter
@Getter
@Accessors(chain = true)
@Table(name = "users")
public class UserQTO extends DataQTO {
private String id;
private String code;
private String name;
private String nameOrEq;
private String sex;
private Date lastDateFrom;
private Date lastDateTo;
}
4.1.3. 新建UserDao
@Component
public interface UserDao extends BaseMapper<UserDTO> {
/**
* 自定方法 mapper.xml 代码略
* @param qto
* @return
*/
UserDTO test(UserQTO qto);
}
4.1.4. 新建 UserService
@Service
public class userService extends SimpleService<UserDao, UserDTO> {
@Override
public int insert(UserDTO dto) {
return super.insert(dto);
}
@Override
public int update(UserDTO dto) {
return super.update(dto);
}
@Override
public int deleteByIds(Number... ids) {
return super.deleteByIds(ids);
}
@Override
public int deleteByIds(String... ids) {
return super.deleteByIds(ids);
}
//以上4个方法的代码可以删除, 没什么逻辑, 这里只是告诉读者有这些方法, 但父类的方法远远不止这4个
/**
* 自定的方法
* @param qto
* @return
*/
public List<userDTO> test(UserQTO qto) {
return baseMapper.test(qto);
}
}
实现哪些具体的功能呢, 详见后面的章节
4.2. 功能强大的查询
4.2.1. 条件匹配器Condition 查询一
以下仅仅是条件匹配器的部分功能, 更多功能等待用户挖掘.
@RestController
@RequestMapping("/condition")
public class ConditionController {
@Autowired
UserService userService;
/**
* 条件匹配器的一个例子
*/
@GetMapping("/query1")
public List<UserDTO> query1(){
//**查询 返回对象 */
List<UserDTO> listUsers = userService.listObjects(
Condition.create() //创建条件匹配器对象
.between("age",10,20) //生成 age BETWEEN 10 AND 20
.eq("sex","男") //生成 AND(sex = '男')
.eq("name","C","D","E")//生成 AND(name = 'C' OR name = 'D' OR name = 'E')
.like("name","A", "B") //生成 AND(name LIKE '%A%' OR name LIKE '%B%')
//不等
.ne("name","张三","李四")
//等同于 .eq("substring(name,2)","平")
.add("substring(name,2)='平' ")//自定义条件
.le("loginCount",1)//小于等于
.lt("loginCount",2)//小于
.ge("loginCount",4)//大于等于
.gt("loginCount",3)//大于
.isNull("name")
.isNotNull("name")
);
return listUsers;
}
}
生成的WHERE条件如下:
SELECT id,code,name,password,sex,age,amount,mobile,isAdmin,loginCount,lastDate,deptId,createUser,updateUser
FROM users a
WHERE age BETWEEN 10 AND 20
AND (sex = '男')
AND (name = 'C' OR name = 'D' OR name = 'E')
AND (name LIKE '%A%' OR name LIKE '%B%')
AND (name <> '张三' AND name <> '李四')
AND substring(name,2)='平'
AND (loginCount <= 1)
AND (loginCount < 2)
AND (loginCount >= 4)
AND (loginCount > 3)
AND name is null
AND name is not null
LIMIT 0,6666
4.2.2. 条件匹配器Condition 查询二
@RestController
@RequestMapping("/condition")
public class ConditionController {
@Autowired
UserService userService;
@GetMapping("/query2")
public List<UserDTO> query2(){
//**查询 返回对象 */
List<UserDTO> listUsers = userService.listObjects(
userService.where() // 等同于 Condition.create() 创建一个条件匹配器对象
.eq("concat(name,id)","A1") //生成 (concat(name,id) = 'A1')
.eq("concat(name,id)","C1","D2","E3")//生成 AND (concat(name,id) = 'C1' OR concat(name,id) = 'D2' OR concat(name,id) = 'E3' )
);
return listUsers;
}
}
生成的WHERE条件如下:
SELECT id,code,name,password,sex,age,amount,mobile,isAdmin,loginCount,lastDate,deptId,createUser,updateUser
FROM users a
WHERE (concat(name,id) = 'A1')
AND (concat(name,id) = 'C1'
OR concat(name,id) = 'D2'
OR concat(name,id) = 'E3' )
4.3. 高级查询 带分组, 排序, 自定select 后字段, 指定分页的查询
利用查询构造器 EntitySelect 和 Condition的查询
/**
* 核心使用 继承了 topfox 的SimpleService
*/
@Service
public class CoreService extends SimpleService<UserDao, UserDTO> {
public List<UserDTO> demo2(){
List<UserDTO> listUsers=listObjects(
select("name, count('*')") //通过调用SimpleService.select() 获得或创建一个新的 EntitySelect 对象,并返回它
.where() //等同于 Condition.create()
.eq("sex","男") //条件匹配器自定义条件 返回对象 Condition
.endWhere() //条件结束 返回对象 EntitySelect
.orderBy("name") //设置排序的字段 返回对象 EntitySelect
.groupBy("name") //设置分组的字段 返回对象 EntitySelect
.setPage(10,5) //设置分页(查询第10页, 每页返回5条记录)
);
return listUsers;
}
}
输出sql如下:
SELECT name, count('*')
FROM users a
WHERE (sex = '男')
GROUP BY name
ORDER BY name
LIMIT 45,5
4.4. 查询时如何才能不读取缓存
TopFox 实现了缓存处理, 当前线程的缓存 为一级缓存, redis为二级缓存.
通过设置 readCache 为false, 能实现在开启一级/二级缓存的情况下又不读取缓存, 从而保证读取出来的数据和数据库中的一模一样, 下面通过5个例子来说明.
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
UserService userService;
@TokenOff
@GetMapping("/test1")
public Object test1(UserQTO userQTO) {
//例1: 根据id查询, 通过第2个参数传false 就不读取一二级缓存了
UserDTO user = userService.getObject(1, false);
//例2: 根据多个id查询, 要查询的id放入Set容器中
Set setIds = new HashSet();
setIds.add(1);
setIds.add(2);
//通过第2个参数传false 就不读取一二级缓存了
List<UserDTO> list = userService.listObjects(setIds, false);
//例3: 通过QTO 设置不读取缓存
list = userService.listObjects(
userQTO.readCache(false) //禁用从缓存读取(注意不是读写) readCache 设置为 false, 返回自己(QTO)
);
//或者写成:
userQTO.readCache(false);
list = userService.listObjects(userQTO);
//例4: 通过条件匹配器Condition 设置不读取缓存
list = userService.listObjects(
Condition.create() //创建条件匹配器
.readCache(false) //禁用从缓存读取
);
return list;
}
}
4.5. 查询 缓存开关 thread-cache redis-cache与readCache区别
请读者先阅读 章节 《TopFox配置参数》
一级缓存 top.service.thread-cache 大于 readCache
二级缓存 top.service.redis-cache 大于 readCache
也就说, 把一级二级缓存关闭了, readCache设置为true, 也不会读取缓存. 所有方式的查询也不会读取缓存.
4.6. 开启一级缓存
- 一级缓存默认是关闭的
只打开某个 service的操作的一级缓存
@Service
public class UserService extends SimpleService<UserDao, UserDTO> {
@Override
public void init() {
sysConfig.setThreadCache(true); //打开一级缓存
}
全局开启一级缓存, 项目配置文件 application.properties 增加
top.service.thread-cache=true
- 开启一级缓存后
- 一级缓存是只当前线程级别的, 线程结束则缓存消失
- 下面的例子, 在开启一级缓后 user1,user2和user3是同一个实例的
- 一级缓存的效果我们借鉴了Hibernate框架的数据实体对象持久化的思想
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
UserService userService;
@TokenOff
@GetMapping("/test2")
public UserDTO test2() {
UserDTO user1 = userService.getObject(1);//查询后 会放入一级 二级缓存
UserDTO user2 = userService.getObject(1);//会从一级缓存中获取到
userService.update(user2.setName("张三"));
UserDTO user3 = userService.getObject(1);//会从一级缓存中获取到
return user3;
}
}
4.7. 开启二级缓存 Redis
- 二级缓存默认是关闭的
只打开某个 service的操作的二级缓存
@Service
public class UserService extends SimpleService<UserDao, UserDTO> {
@Override
public void init() {
sysConfig.setRedisCache(true); //打开一级缓存
}
全局开启一级缓存, 项目配置文件 application.properties 增加
top.service.redis-cache=true
:::备注
- 开启后, 查询优先会读取一级缓存, 没有就会读取二级缓存, 再没有就会从数据库中获取
- 开启后, 利用TopFox 的service的 新增/修改/删除 操作都会自动同步到 redis中
4.8. QTO后缀增强查询
我们修改 UserQTO 的源码如下:
@Setter
@Getter
@Table(name = "users")
public class UserQTO extends DataQTO {
private String id; //用户id, 与数据字段名一样的
private String name; //用户姓名name, 与数据字段名一样的
private String nameOrEq; //用户姓名 后缀OrEq
private String nameAndNe; //用户姓名 后缀AndNe
private String nameOrLike; //用户姓名 后缀OrLike
private String nameAndNotLike;//用户姓名 后缀AndNotLike
...
}
- 字段名 后缀OrEq当 nameOrEq 写值为 "张三,李四" 时, 源码如下:
package com.test.service;
/**
* 核心使用 demo1 源码 集成了 TopFox 的 SimpleService类
*/
@Service
public class CoreService extends SimpleService<UserDao, UserDTO> {
public List<UserDTO> demo1(){
UserQTO userQTO = new UserQTO();
userQTO.setNameOrEq("张三,李四");//这里赋值
//依据QTO查询 listObjects会自动生成SQL, 不用配置 xxxMapper.xml
List<UserDTO> listUsers = listObjects(userQTO);
return listUsers;
}
}
则生成SQL:
SELECT ...
FROM SecUser
WHERE (name = '张三' OR name = '李四')
- 字段名 后缀AndNe当 nameAndNe 写值为 "张三,李四" 时, 则生成SQL:
SELECT ...
FROM SecUser
WHERE (name <> '张三' AND name <> '李四')
- 字段名 后缀OrLike当 nameOrLike 写值为 "张三,李四" 时, 则将生成SQL:
SELECT ...
FROM SecUser
WHERE (name LIKE CONCAT('%','张三','%') OR name LIKE CONCAT('%','李四','%'))
- 字段名 后缀AndNotLike当 nameAndNotLike 写值为 "张三,李四" 时, 则生成SQL:
SELECT ...
FROM SecUser
WHERE (name NOT LIKE CONCAT('%','张三','%') AND name NOT LIKE CONCAT('%','李四','%'))
以上例子是TopFox全自动生成的SQL
4.9. 更多的查询方法
- Response< List < DTO > > listPage(EntitySelect entitySelect)
- List< Map < String, Object > > selectMaps(DataQTO qto)
- List< Map < String, Object > > selectMaps(Condition where)
- List< Map < String, Object > > selectMaps(EntitySelect entitySelect)
- selectCount(Condition where)
- selectMax(String fieldName, Condition where)
- 等等
4.10. 自定条件更新 updateBatch
@Service
public class UnitTestService {
@Autowired UserService userService;
public void test(){
UserDTO dto = new UserDTO();
dto.setAge(99);
dto.setDeptId(11);
dto.addNullFields("mobile, isAdmin");//将指定的字段更新为null
List<UserDTO> list userService.updateBatch(dto, where().eq("sex","男"));
// list为更新过得记录
}
}
生成的Sql语句如下:
UPDATE users
SET deptId=11,age=99,mobile=null,isAdmin=null
WHERE (sex = '男')
4.11. 更多的 插入 和更新的代码例子
@Service
public class UnitTestService {
@Autowired UserService userService;
...
public void insert(){
//Id为数据库自增, 新增可以获得Id
UserDTO dto = new UserDTO();
dto.setName("张三");
dto.setSex("男");
userService.insertGetKey(dto);
logger.debug("新增用户的Id 是 {}", dto.getId());
}
public void update(){
UserDTO user1 = new UserDTO();
user1.setAge(99);
user1.setId(1);
user1.setName("Luoping");
//将指定的字段更新为null, 允许有空格
user1.addNullFields(" sex , lastDate , loginCount");
// //这样写也支持
// user1.addNullFields("sex","lastDate");
// //这样写也支持
// user1.addNullFields("sex, lastDate","deptId");
userService.update(user1);//只更新有值的字段
}
public void update1(){
UserDTO user1 = new UserDTO();
user1.setAge(99);
user1.setId(1);
user1.setName("Luoping");
userService.update(user1);//只更新有值的字段
}
public void updateList(){
UserDTO user1 = new UserDTO();
user1.setAge(99);
user1.setId(1);
user1.setName("张三");
user1.addNullFields("sex, lastDate");
UserDTO user2 = new UserDTO();
user2.setAge(88);
user2.setId(2);
user2.setName("李四");
user2.addNullFields("mobile, isAdmin");
List list = new ArrayList();
list.add(user1);
list.add(user2);
userService.updateList(list);//只更新有值的字段
}
数据校验组件之实战- 重复检查
假如用户表中已经有一条用户记录的 手机号是 13588330001, 然后我们再新增一条手机号相同的用户, 或者将其他某条记录的手机号更新为这个手机号, 此时我们希望 程序能检查出这个错误, CheckData对象就是干这个事的.检查用户手机号不能重复有如下多种写法:
4.11.1. 示例一
@Service
public class CheckData1Service extends AdvancedService<UserDao, UserDTO> {
@Override
public void beforeInsertOrUpdate(List<UserDTO> list) {
//多行记录时只执行一句SQL完成检查手机号是否重复, 并抛出异常
checkData(list) // 1. list是要检查重复的数据
// 2.checkData 为TopFox在 SimpleService里面定义的 new 一个 CheckData对象的方法
.addField("mobile", "手机号") //自定义 有异常抛出的错误信息的字段的中文标题
.setWhere(where().ne("mobile","*")) //自定检查的附加条件, 可以不写(手机号为*的值不参与检查)
.excute();// 生成检查SQL, 并执行, 有结果记录(重复)则抛出异常, 回滚事务
}
}
控制台 抛出异常 的日志记录如下:
##这是 inert 重复检查 TopFox自动生成的SQL:
SELECT concat(mobile) result
FROM SecUser a
WHERE (mobile <> '*')
AND (concat(mobile) = '13588330001')
LIMIT 0,1
14:24|49.920 [4] DEBUG 182-com.topfox.util.CheckData | mobile {13588330001}
提交数据{手机号}的值{13588330001}不可重复
at com.topfox.common.CommonException$CommonString.text(CommonException.java:164)
at com.topfox.util.CheckData.excute(CheckData.java:189)
at com.topfox.util.CheckData.excute(CheckData.java:75)
at com.sec.service.UserService.beforeInsertOrUpdate(UserService.java:74)
at com.topfox.service.AdvancedService.beforeSave2(AdvancedService.java:104)
at com.topfox.service.SimpleService.updateList(SimpleService.java:280)
at com.topfox.service.SimpleService.save(SimpleService.java:451)
at com.sec.service.UserService.save(UserService.java:41)
- 异常信息的 "手机号" 是 .addField("mobile", "手机号") 指定的中文名称
- 假如用户表用两条记录, 第一条用户id为001的记录手机号为13588330001, 第一条用户id为002的记录手机号为13588330002.<br>如果我们把第2条记录用户的手机号13588330002改为13588330001, 则会造成了 数据重复, TopFox执行的检查重复的SQL语句为:
##这是 update时重复检查 TopFox自动生成的SQL:
SELECT concat(mobile) result
FROM SecUser a
WHERE (mobile <> '*')
AND (concat(mobile) = '13588330001')
AND (id <> '002') ## 修改用户手机号那条记录的用户Id
LIMIT 0,1
通过这个例子, 希望读者能理解 新增和更新 TopFox 生成SQL不同的原因.
4.11.2. 更多例子请参考 << 数据校验组件>> 章节
4.12. 更新日志组件 ChangeManager 分布式事务 回滚有用哦
获得修改日志可写入到 mongodb中, 控制分布式事务 回滚有用哦
读取修改日志的代码很简单, 共写了2个例子, 如下:
@Service
public class UserService extends AdvancedService<UserDao, UserDTO> {
@Override
public void afterInsertOrUpdate(UserDTO userDTO, String state) {
if (DbState.UPDATE.equals(state)) {
// 例一:
ChangeManager changeManager = changeManager(userDTO)
.addFieldLabel("name", "用户姓名") //设置该字段的日志输出的中文名
.addFieldLabel("mobile", "手机号"); //设置该字段的日志输出的中文名
//输出 方式一 参数格式
logger.debug("修改日志:{}", changeManager.output().toString() );
// 输出样例:
/**
修改日志:
id:000000, //用户的id
用户姓名:开发者->开发者2,
手机号:13588330001->1805816881122
*/
// 输出 方式二 JSON格式
logger.debug("修改日志:{}", changeManager.outJSONString() );
// 输出样例: c是 current的简写, 是当前值, 新值; o是 old的简写, 修改之前的值
/**
修改日志:
{
"appName":"sec",
"executeId":"1561367017351_14",
"id":"000000",
"data":{
"version":{"c":"207","o":206},
"用户姓名":{"c":"开发者2","o":"开发者"},
"手机号":{"c":"1805816881122","o":"13588330001"}
}
}
*/
//************************************************************************************
// 例二 没有用 addFieldLabel 设置字段输出的中文名, 则data中的keys输出全部为英文
logger.debug("修改日志:{}", changeManager(userDTO).outJSONString() );
// 输出 JSON格式
/**
修改日志:
{
"appName":"sec",
"executeId":"1561367017351_14",
"id":"000000",
"data":{
"version":{"c":"207","o":206},
"name":{"c":"开发者2","o":"开发者"},
"mobile":{"c":"1805816881122","o":"13588330001"}
}
}
*/
//************************************************************************************
}
}
}
4.13. 流水号生成器 KeyBuild
- 简单的流水号, 我们定义为 是递增的序列号
- keyBuild()方法是 类库封装的创建 KeyBuild对象的方法.
4.13.1. 简单流水号
:::示例一
- 假如表中只有2条数据, id 字段的值分别为 001, 002, 则执行下面程序获得的值是003
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test1() {
//logger为TopFox声明的日志对象
//例: 根据UserDTO中字段名id 来获取一个纯 3位数 递增的流水号
logger.debug(
keyBuild() //创建一个 KeyBuild对象, 会自动获取当前Service的 UserDTO 对象
.getKey("id",3) //参数id 必须是 UserDTO中存在的字段
); //打印出来的值是 003
}
}
:::示例二
- 假如表中只有6条数据, id 字段的值分别为 06,07, 112,113, 2222,2223 这里有长度为2,3,4位的Id值, 执行下面的程序, debug的信息分别是08, 114, 2224.
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test2() {
logger.debug(keyBuild().getKey("id",2)); //打印出来的值是 08
logger.debug(keyBuild().getKey("id",3)); //打印出来的值是 114
logger.debug(keyBuild().getKey("id",4)); //打印出来的值是 2224
//这个例子说明是按照 id字段 值的长度隔离的.
}
}
总结:
- 流水号是通过分析当前service的UserDTO对应表的已有数据而生成的, 并将分析结果缓存到Redis中, 减少对表的读取.
- 流水号的生成是按照表名,字段名和已有数据的长度 隔离的
- 位数满后会自动增加1位, 例如获得2位数的流水号, 当99后, 再次获取会增加一位变为100
- 获取到流水号后, 是不会因为抛出异常而回滚, 每次调用始终 加一的. <br>例如 获取到 2224后抛一个异常, 事务是回滚了, 但下次获取这个流水号, 取到的是 2225(2224不会回滚).这样设计主要是考虑到"避免分布式下高并发 流水号可能会重复的问题".
- 这是按照调用次数 变化的数字, 我们称之为是 "递增的次序号". 位数不足用 0 填补
4.13.2. 复杂流水号(含前缀|日期|后缀)
- 流水号 = 前缀 + 日期字符 + 递增的序列号 + 后缀
- 如何设置 前缀和日期字符,以及后缀呢? 请看如下例子:
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
/**
* 每行数据执行本方法一次,新增和修改的 之前的逻辑写到这里, 如通用的检查, 计算值得处理
*/
public void test3() {
//获取一个 带前缀TL 带日期字符(yyMMdd) + 6位数递增的序列号 的流水号
logger.debug(
keyBuild()
.setPrefix("TL") //设置前缀
.setSuffix("END") //设置后缀
.setDateFormat("yyyyMMdd") //设置日期格式
.getKey("id",3) //参数依次是 1.字段名 2.序列号长度
);
}
}
- 假如生成的流水号 是 TL20190601001END , 其中 TL 是前缀, 20190601是年月日, 001是递增的序列号, END 是后缀
- 日期格式可以自定, 例如: yyyyMMdd yyMM MMdd yyMMdd yMMDD
4.13.3. 批量流水号
一次要获得多个流水号, 如企业内部系统 的 订单导入等, 建议用如下办法获得一批流水号
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test4() {
logger.debug("获得多个流水号");
//获得多个序列号
ConcurrentLinkedQueue<String> queue =
keyBuild("TL", "yyMMdd") //前缀, 设置日期格式
.getKeys("id", 6, 4); //参数依次是 1.字段名 2.序列号长度 3.要获得流水号个数
// poll 执行一次, 容器 queue里面少一个
logger.debug(queue.poll());//获得第1个序列号
logger.debug(queue.poll());//获得第2个序列号
logger.debug(queue.poll());//获得第3个序列号
logger.debug(queue.poll());//获得第4个序列号
}
}
也可以写成
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test5() {
logger.debug("获得多个流水号");
//获得多个序列号
ConcurrentLinkedQueue<String> queue =
keyBuild()
.setPrefix("TL") //设置前缀
.setDateFormat("yyyyMMdd") //设置日期格式
.getKeys("id", 6, 4); //参数依次是 1.字段名 2.序列号长度 3.要获得流水号个数
... 略
}
}
4.14. 多主键 查询/删除
下面这个表有两个字段作为主键, userId 和 deptId :
/**
* 薪水津贴模板表
* 假定一个主管 管理了多个部门, 每管理一个部门, 就有管理津贴作为薪水
*/
@Setter
@Getter
@Accessors(chain = true)
@Table(name = "salary")
public class SalaryDTO extends DataDTO {
/**
* 两个主键字段, 用户Id 和部门Id
*/
@Id
private Integer userId;
@Id
private Integer deptId;
/**
* 管理津贴
*/
@JsonFormat(shape = JsonFormat.Shape.NUMBER, pattern = "###0.00")
private BigDecimal amount;
...
}
表 salary 的数据如下:
::: 重要备注:
1-1, 1-2, 1-2 我们称之为3组主键Id值, 任何一组主键值 可以定位到 唯一的行.
4.14.1. 技巧一: 单组主键值查询
多主键时, sql语句主键字段的拼接顺序是 按照 SalaryDTO 中定义的字段顺序来的.
具体来说, 如 concat(userId,'-', deptId) 这个先是 userId, 然后是deptId, 与 SalaryDTO 中定义的字段顺序一致. 因此在拼接Id值时注意顺序要一致.
单组主键值查询, 获得单个DTO对象:
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired
SalaryService salaryService;
protected Logger logger = LoggerFactory.getLogger(getClass());
@GetMapping("/test1")
public SalaryDTO test1() {
return salaryService.getObject("1-2");
}
}
输出SQL:
SELECT userId,deptId,amount,createUser,updateUser
FROM salary a
WHERE (concat(userId,'-', deptId) = '1-2')
4.14.2. 技巧二 : 多组主键值查询
多组主键值查询, 获得多个DTO对象:
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired SalaryService salaryService;
@GetMapping("/test2")
public List<SalaryDTO> test2() {
return salaryService.listObjects("1-1,1-2,1-3");
}
}
输出SQL:
SELECT userId,deptId,amount,createUser,updateUser
FROM salary a
WHERE (concat(userId,'-', deptId) = '1-1'
OR concat(userId,'-', deptId) = '1-2'
OR concat(userId,'-', deptId) = '1-3')
4.14.3. 技巧三: 获取主键字段拼接的SQL
下面的程序代码 打印出来的是字符串: (concat(userId,'-', deptId)
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired SalaryService salaryService;
@GetMapping("/test3")
public String test3() {
String idFieldsBySql = salaryService.tableInfo().getIdFieldsBySql();
logger.debug(idFieldsBySql);
return idFieldsBySql;
}
}
4.14.4. 技巧四: 按多组主键值删除
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired SalaryService salaryService;
@GetMapping("/test4")
public void test4() {
salaryService.deleteByIds("1-1,1-2");
}
}
输出SQL:
DELETE FROM salary
WHERE (concat(userId,'-', deptId) = '1-1'
OR concat(userId,'-', deptId) = '1-2')
5. 上下文对象 AppContext 如何使用
下面源码中的 RestSession和RestSessionConfig对象可以参考 <<快速使用>>章节中的相关内容
AppContext 提供了几个静态方法, 直接获取相关对象.
package com.user.controller;
import com.topfox.annotation.TokenOff;
import com.sys.RestSession;
import AbstractRestSessionConfig;
import com.topfox.common.AppContext;
import com.topfox.common.SysConfigRead;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/context")
public class AppContextController {
/**
* AppContext.getRestSessionHandler()是同一个实例
*/
@Autowired RestSessionConfig restSessionConfig;
@TokenOff
@GetMapping("/test1")
public void test1() {
Environment environment = AppContext.environment();
RestSessionConfig restSessionHandlerConfig = (RestSessionConfig)AppContext.getRestSessionHandler();
restrestSessionConfig RestSession restSession = AppContext.getRestSession();
SysConfigRead configRead = AppContext.getSysConfig();
System.out.println(configRead);
}
@TokenOff
@GetMapping("/test2")
public void test2() {
RestSession restSession = restSessionConfig.restSessionConfigsConfigRead configRead = restSessionConfig.restSessionConfig }
}
6. TopFox配置参数
以下参数在项目 application.properties 文件中配置, 不配置会用默认值. 下面的等号后面的值就是默认值.
6.1. top.log.start="▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼..."
debug 当前线程开始 日志输出分割
6.2. top.log.prefix="# "
debug 中间日志输出前缀
6.3. top.log.end=▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲..."
debug 当前线程结束 日志输出分割符
6.4. top.page-size=100
分页时,默认的每页条数
6.5. top.max-page-size=300
不分页时(pageSize<=0),查询时最多返回的条数
6.6. [新增] top.service.thread-cache=false
是否开启一级缓存(线程缓存), 默认false 关闭, 查询不会读取一级缓存
6.7. [新增] top.service.redis-cache=false
是否开启二级缓存(redis缓存), 默认false 关闭, 替代老的 open-redis
6.8. top.service.open-redis=false 作废
service层是否开启redis缓存,
6.9. top.service.redis-log=flase
日志级别是DEBUG时, 是否打印 操作redis的操作日志
默认 false 不打印操作redis的日志
true 打印操作redis的日志
参数配置为true时, 控制台打印的日志大概如下:
# DEBUG 112-com.topfox.util.DataCache 更新后写入Redis成功 com.user.entity.UserDTO hashCode=2125196143 id=0
##DEBUG 112-com.topfox.util.DataCache更新后写入Redis成功 com.user.entity.UserDTO hashCode=1528294732 id=1
##DEBUG 112-com.topfox.util.DataCache查询后写入Redis成功 com.user.entity.UserDTO hashCode=620192016 id=2
6.10. top.redis.serializer-json=true
# redis序列化支持两种, true:jackson2JsonRedisSerializer false:JdkSerializationRedisSerializer
# 注意, 推荐生产环境下更改为 false, 类库将采用JdkSerializationRedisSerializer 序列化对象,
# 这时必须禁用devtools(pom.xml 注释掉devtools), 否则报错.
6.11. top.service.update-mode=3
更新时DTO序列化策略 和 更新SQL生成策略
重要参数:
参数值为 1 时, service的DTO=提交的数据.
更新SQL 提交数据不等null 的字段 生成 set field=value
参数值为 2 时, service的DTO=修改前的原始数据+提交的数据.
更新SQL (当前值 != 原始数据) 的字段 生成 set field=value
参数值为 3 时, service的DTO=修改前的原始数据+提交的数据.
更新SQL (当前值 != 原始数据 + 提交数据的所有字段)生成 set field=value
始终保证了前台(调用方)提交的字段, 不管有没有修改, 都能生成更新SQL, 这是与2最本质的区别
6.12. top.service.select-by-before-update=false
top.service.update-mode=1 时本参数才生效
默认值为false
更新之前是否先查询(获得原始数据). 如果需要获得修改日志, 又开启了redis, 建议在 update-mode=1时, 将本参数配置为true
6.13. top.service.update-not-result-error=true
根据Id更新记录时, sql执行结果(影响的行数)为0时是否抛出异常
默认 true 抛出异常
false 不抛异常
6.14. top.service.sql-camel-to-underscore=OFF
生成SQL 是否驼峰转下划线 默认 OFF
一共有3个值:
- OFF 关闭, 生成SQL 用驼峰命名
- ON-UPPER 打开, 下划线并全大写
- ON-LOWER 打开, 下划线并全小写
7. Topfox 在运行时更改参数值---对象 SysConfig
- SysConfig 接口的实现类是 com.topfox.util.SysConfigDefault
package com.topfox.util;
public interface SysConfig extends SysConfigRead {
/**
* 对应配置文件中的 top.service.update-mode
*/
void setUpdateMode(Integer value);
/**
* 对应配置文件中的 top.service.open-redis
*/
void setRedisCache(Boolean value);
/**
* 对应配置文件中的 top.service.update-not-result-error
*/
void setUpdateNotResultError(Boolean value);
...等等, 没有全部列出
}
- 以上接口定义的方法是set方法, 允许在运行时 修改, 每个service 都有一个SysConfig的副本, 通过set更改的值只对当前service有效.
- 使用场景举例:
以参数 open-redis为例:<br> 我们假定项目配置文件 application.properties中开启了 读写Redis 的功能, 即 top.service.open-redis=true , 此时的含义表示, 当前项目的所有service操作数据库的增删改查的数据都会同步到Redis中. 那问题来了, 假如刚好 UserService 需要关闭open-redis, 怎么处理呢, 代码如下:
@Service
public class UserService extends AdvancedService<UserDao, UserDTO> {
@Override
public void init() {
/**
1. sysConfig 为 AdvancedService的父类 SuperService 中定义的 变量, 直接使用即可
2. sysConfig的默认值 来自于 application.properties 中的设置的值,
如果 application.properties 中没有定义, 则TopFox会自动默认一个
3.sysConfig中定义的参数在这里都可以更改
*/
//关闭了 UserService 读写redis的功能, 其他service不受影响
sysConfig.setOpenRedis(false);
}
}
这样调用了 UserService 的 getObject listObjects update insert delete 等方法操作的数据是不会同步到redis的 .<br>其他参数同理可以在运行时修改
7.1. 必备
- 官网: https://gitee.com/topfox/topfox
- 文中例子源码: https://gitee.com/topfox/topfox-sample
- TopFox技术交流群 QQ: 874732179