# topfox-sample **Repository Path**: geekcheng_admin/topfox-sample ## Basic Information - **Project Name**: topfox-sample - **Description**: No description available - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2019-07-29 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1. topfox-sample - 数据库脚本请参考文件 db.sql, 共3张表: 部门表depts, 用户表users, 多主键字段表 salary - 使用数据库为 mysql8 - 本示例项目依据 topfox 开发, 网址 https://gitee.com/topfox/topfox # 2. topfox 用户使用手册 - 目录 - [快速使用](https://gitee.com/topfox/topfox/blob/dev/guide/explain-sample.md) - [高级运用](https://gitee.com/topfox/topfox/blob/dev/guide/explain-question.md) - [TopFox配置参数](https://gitee.com/topfox/topfox/blob/dev/guide/explain-configuration.md) - [上下文对象](https://gitee.com/topfox/topfox/blob/dev/guide/explain-tools.md) - [核心使用](https://gitee.com/topfox/topfox/blob/dev/guide/explain-core.md) - [条件匹配器](https://gitee.com/topfox/topfox/blob/dev/guide/explain-condition.md) - [实体查询构造器](https://gitee.com/topfox/topfox/blob/dev/guide/explain-entityselect.md) - [流水号生成器](https://gitee.com/topfox/topfox/blob/dev/guide/explain-keybuild.md) - [数据校验组件](https://gitee.com/topfox/topfox/blob/dev/guide/explain-checkdata.md) - [更新日志组件](https://gitee.com/topfox/topfox/blob/dev/guide/explain-updatelog.md) - [自动填充组件](https://gitee.com/topfox/topfox/blob/dev/guide/explain-filldata.md) - [Response 返回结果对象](https://gitee.com/topfox/topfox/blob/dev/guide/explain-response.md) ## 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.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 ```java @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 ) ```java @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 ```java @Component public interface UserDao extends BaseMapper { /** * 自定方法 mapper.xml 代码略 * @param qto * @return */ UserDTO test(UserQTO qto); } ``` ### 4.1.4. 新建 UserService ```java @Service public class userService extends SimpleService { @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 test(UserQTO qto) { return baseMapper.test(qto); } } ``` 实现哪些具体的功能呢, 详见后面的章节 ## 4.2. 功能强大的查询 ### 4.2.1. 条件匹配器Condition 查询一 以下仅仅是条件匹配器的部分功能, 更多功能等待用户挖掘. ```java @RestController @RequestMapping("/condition") public class ConditionController { @Autowired UserService userService; /** * 条件匹配器的一个例子 */ @GetMapping("/query1") public List query1(){ //**查询 返回对象 */ List 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条件如下: ```SQL 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 查询二 ```java @RestController @RequestMapping("/condition") public class ConditionController { @Autowired UserService userService; @GetMapping("/query2") public List query2(){ //**查询 返回对象 */ List 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条件如下: ```sql 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的查询 [实体查询构造器](https://gitee.com/topfox/topfox/blob/dev/guide/explain-entityselect.md) ```java /** * 核心使用 继承了 topfox 的SimpleService */ @Service public class CoreService extends SimpleService { public List demo2(){ List 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如下: ```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个例子来说明. ```java @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 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 { @Override public void init() { sysConfig.setThreadCache(true); //打开一级缓存 } ``` 全局开启一级缓存, 项目配置文件 application.properties 增加 ``` top.service.thread-cache=true ``` - 开启一级缓存后 1. 一级缓存是只当前线程级别的, 线程结束则缓存消失 2. 下面的例子, 在开启一级缓后 user1,user2和user3是同一个实例的 3. 一级缓存的效果我们借鉴了Hibernate框架的数据实体对象持久化的思想 ```java @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 { @Override public void init() { sysConfig.setRedisCache(true); //打开一级缓存 } ``` 全局开启一级缓存, 项目配置文件 application.properties 增加 ``` top.service.redis-cache=true ``` :::备注 - 开启后, 查询优先会读取一级缓存, 没有就会读取二级缓存, 再没有就会从数据库中获取 - 开启后, 利用TopFox 的service的 新增/修改/删除 操作都会自动同步到 redis中 ## 4.8. QTO后缀增强查询 我们修改 UserQTO 的源码如下: ```java @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 写值为 "张三,李四" 时, 源码如下: ```java package com.test.service; /** * 核心使用 demo1 源码 集成了 TopFox 的 SimpleService类 */ @Service public class CoreService extends SimpleService { public List demo1(){ UserQTO userQTO = new UserQTO(); userQTO.setNameOrEq("张三,李四");//这里赋值 //依据QTO查询 listObjects会自动生成SQL, 不用配置 xxxMapper.xml List listUsers = listObjects(userQTO); return listUsers; } } ``` 则生成SQL: ```sql SELECT ... FROM SecUser WHERE (name = '张三' OR name = '李四') ``` - 字段名 后缀AndNe 当 nameAndNe 写值为 "张三,李四" 时, 则生成SQL: ```sql SELECT ... FROM SecUser WHERE (name <> '张三' AND name <> '李四') ``` - 字段名 后缀OrLike 当 nameOrLike 写值为 "张三,李四" 时, 则将生成SQL: ```sql SELECT ... FROM SecUser WHERE (name LIKE CONCAT('%','张三','%') OR name LIKE CONCAT('%','李四','%')) ``` - 字段名 后缀AndNotLike 当 nameAndNotLike 写值为 "张三,李四" 时, 则生成SQL: ```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 - @param xxxDTO 要更新的数据, 不为空的字段才会更新. Id字段不能传值 - @param where 条件匹配器 - @return List< DTO >更新的dto集合 ```java @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 list userService.updateBatch(dto, where().eq("sex","男")); // list为更新过得记录 } } ``` 生成的Sql语句如下: ```sql UPDATE users SET deptId=11,age=99,mobile=null,isAdmin=null WHERE (sex = '男') ``` ## 4.11. 更多的 插入 和更新的代码例子 ```java @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. 示例一 ```java @Service public class CheckData1Service extends AdvancedService { @Override public void beforeInsertOrUpdate(List list) { //多行记录时只执行一句SQL完成检查手机号是否重复, 并抛出异常 checkData(list) // 1. list是要检查重复的数据 // 2.checkData 为TopFox在 SimpleService里面定义的 new 一个 CheckData对象的方法 .addField("mobile", "手机号") //自定义 有异常抛出的错误信息的字段的中文标题 .setWhere(where().ne("mobile","*")) //自定检查的附加条件, 可以不写(手机号为*的值不参与检查) .excute();// 生成检查SQL, 并执行, 有结果记录(重复)则抛出异常, 回滚事务 } } ``` 控制台 抛出异常 的日志记录如下: ```sql92 ##这是 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.
如果我们把第2条记录用户的手机号13588330002改为13588330001, 则会造成了 数据重复, TopFox执行的检查重复的SQL语句为: ```sql92 ##这是 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个例子, 如下: ```java @Service public class UserService extends AdvancedService { @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 ```JAVA package com.test.service; @Service public class KeyBuildService extends AdvancedService { 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. ```java package com.test.service; @Service public class KeyBuildService extends AdvancedService { 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字段 值的长度隔离的. } } ``` 总结: 1. 流水号是通过分析当前service的UserDTO对应表的已有数据而生成的, 并将分析结果缓存到Redis中, 减少对表的读取. 2. 流水号的生成是按照表名,字段名和已有数据的长度 隔离的 3. 位数满后会自动增加1位, 例如获得2位数的流水号, 当99后, 再次获取会增加一位变为100 4. 获取到流水号后, 是不会因为抛出异常而回滚, 每次调用始终 加一的.
例如 获取到 2224后抛一个异常, 事务是回滚了, 但下次获取这个流水号, 取到的是 2225(2224不会回滚).这样设计主要是考虑到"避免分布式下高并发 流水号可能会重复的问题". 5. 这是按照调用次数 变化的数字, 我们称之为是 "递增的次序号". 位数不足用 0 填补 ### 4.13.2. 复杂流水号(含前缀|日期|后缀) - 流水号 = 前缀 + 日期字符 + 递增的序列号 + 后缀 - 如何设置 前缀和日期字符,以及后缀呢? 请看如下例子: ```java package com.test.service; @Service public class KeyBuildService extends AdvancedService { /** * 每行数据执行本方法一次,新增和修改的 之前的逻辑写到这里, 如通用的检查, 计算值得处理 */ 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. 批量流水号 一次要获得多个流水号, 如企业内部系统 的 订单导入等, 建议用如下办法获得一批流水号 ```java package com.test.service; @Service public class KeyBuildService extends AdvancedService { public void test4() { logger.debug("获得多个流水号"); //获得多个序列号 ConcurrentLinkedQueue 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个序列号 } } ``` 也可以写成 ```java package com.test.service; @Service public class KeyBuildService extends AdvancedService { public void test5() { logger.debug("获得多个流水号"); //获得多个序列号 ConcurrentLinkedQueue queue = keyBuild() .setPrefix("TL") //设置前缀 .setDateFormat("yyyyMMdd") //设置日期格式 .getKeys("id", 6, 4); //参数依次是 1.字段名 2.序列号长度 3.要获得流水号个数 ... 略 } } ``` ## 4.14. 多主键 查询/删除 下面这个表有两个字段作为主键, userId 和 deptId : ```java /** * 薪水津贴模板表 * 假定一个主管 管理了多个部门, 每管理一个部门, 就有管理津贴作为薪水 */ @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 的数据如下: | userId | deptId | amount | createUser | updateUser | | ------ | ------ | ------ | ---------- | ---------- | | 1 | 1 | 11 | * | * | | 1 | 2 | 22 | * | * | | 1 | 3 | 33 | * | * | ::: 重要备注: 1-1, 1-2, 1-2 我们称之为3组主键Id值, 任何一组主键值 可以定位到 唯一的行. ### 4.14.1. 技巧一: 单组主键值查询 多主键时, sql语句主键字段的拼接顺序是 按照 SalaryDTO 中定义的字段顺序来的. 具体来说, 如 concat(userId,'-', deptId) 这个先是 userId, 然后是deptId, 与 SalaryDTO 中定义的字段顺序一致. 因此在拼接Id值时注意顺序要一致. 单组主键值查询, 获得单个DTO对象: ```java @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: ```sql SELECT userId,deptId,amount,createUser,updateUser FROM salary a WHERE (concat(userId,'-', deptId) = '1-2') ``` ### 4.14.2. 技巧二 : 多组主键值查询 多组主键值查询, 获得多个DTO对象: ```java @RestController @RequestMapping("/salary") public class SalaryController { @Autowired SalaryService salaryService; @GetMapping("/test2") public List test2() { return salaryService.listObjects("1-1,1-2,1-3"); } } ``` 输出SQL: ```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) ```java @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. 技巧四: 按多组主键值删除 ```java @RestController @RequestMapping("/salary") public class SalaryController { @Autowired SalaryService salaryService; @GetMapping("/test4") public void test4() { salaryService.deleteByIds("1-1,1-2"); } } ``` 输出SQL: ```sql DELETE FROM salary WHERE (concat(userId,'-', deptId) = '1-1' OR concat(userId,'-', deptId) = '1-2') ``` # 5. 上下文对象 AppContext 如何使用 下面源码中的 RestSession和RestSessionConfig对象可以参考 <<快速使用>>章节中的相关内容 AppContext 提供了几个静态方法, 直接获取相关对象. ```java package com.user.controller; import com.topfox.annotation.TokenOff; import com.sys.RestSession; import com.sys.RestSessionConfig; 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(); //与 restSessionConfig.get()的获得的对象一样 RestSession restSession = (RestSession)AppContext.getAbstractRestSession(); SysConfigRead configRead = AppContext.getSysConfig(); System.out.println(configRead); } @TokenOff @GetMapping("/test2") public void test2() { RestSession restSession = restSessionConfig.get(); SysConfigRead configRead = restSessionConfig.getSysConfig(); } } ``` # 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个值: 1. OFF 关闭, 生成SQL 用驼峰命名 2. ON-UPPER 打开, 下划线并全大写 3. ON-LOWER 打开, 下划线并全小写 # 7. Topfox 在运行时更改参数值---对象 SysConfig - SysConfig 接口的实现类是 com.topfox.util.SysConfigDefault ```java 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为例:
       我们假定项目配置文件 application.properties中开启了 读写Redis 的功能, 即 top.service.open-redis=true , 此时的含义表示, 当前项目的所有service操作数据库的增删改查的数据都会同步到Redis中. 那问题来了, 假如刚好 UserService 需要关闭open-redis, 怎么处理呢, 代码如下: ```java @Service public class UserService extends AdvancedService { @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的 .
其他参数同理可以在运行时修改 ## 7.1. 必备 - 官网: https://gitee.com/topfox/topfox - 文中例子源码: https://gitee.com/topfox/topfox-sample - TopFox技术交流群 QQ: 874732179