1. topfox 框架例子

  • 数据库脚本请参考文件 db.sql, 共3张表: 部门表depts, 用户表users, 多主键字段表 salary

  • 使用数据库为 mysql8

  • 框架网址 https://gitee.com/topfox/topfox

2. topfox 用户使用手册 - 目录

2.1. 必备

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
  • 开启一级缓存后
  1. 一级缓存是只当前线程级别的, 线程结束则缓存消失
  2. 下面的例子, 在开启一级缓后 user1,user2和user3是同一个实例的
  3. 一级缓存的效果我们借鉴了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

  • @param xxxDTO 要更新的数据, 不为空的字段才会更新. Id字段不能传值
  • @param where 条件匹配器
  • @return List< DTO >更新的dto集合
@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字段 值的长度隔离的.
    }
}

总结:

  1. 流水号是通过分析当前service的UserDTO对应表的已有数据而生成的, 并将分析结果缓存到Redis中, 减少对表的读取.
  2. 流水号的生成是按照表名,字段名和已有数据的长度 隔离的
  3. 位数满后会自动增加1位, 例如获得2位数的流水号, 当99后, 再次获取会增加一位变为100
  4. 获取到流水号后, 是不会因为抛出异常而回滚, 每次调用始终 加一的. <br>例如 获取到 2224后抛一个异常, 事务是回滚了, 但下次获取这个流水号, 取到的是 2225(2224不会回滚).这样设计主要是考虑到"避免分布式下高并发 流水号可能会重复的问题".
  5. 这是按照调用次数 变化的数字, 我们称之为是 "递增的次序号". 位数不足用 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个值:

  1. OFF 关闭, 生成SQL 用驼峰命名
  2. ON-UPPER 打开, 下划线并全大写
  3. 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. 必备

08-02 14:42