1. ↖回到主目录

2. 高级运用

2.1. 查询时如何才能不读取缓存

请读者先阅读 章节 《TopFox配置参数 》中的一级二级缓存开关

通过设置 readCache 为false, 能实现在开启一级/二级缓存的情况下又不读取缓存, 从而保证读取出来的数据和数据库中的一模一样, 下面通过5个例子来说明.

  1. @RestController
  2. @RequestMapping("/demo")
  3. public class DemoController {
  4. @Autowired
  5. UserService userService;
  6. @TokenOff
  7. @GetMapping("/test1")
  8. public Object test1(UserQTO userQTO) {
  9. //例1: 根据id查询, 通过第2个参数传false 就不读取一二级缓存了
  10. userService.getObject(1, false);
  11. //例2: 根据多个id查询, 要查询的id放入Set容器中
  12. Set setIds = new HashSet();
  13. setIds.add(1);
  14. setIds.add(2);
  15. //通过第2个参数传false 就不读取一二级缓存了
  16. List<UserDTO> list = userService.listObjects(setIds, false);
  17. //例3: 通过QTO 设置不读取缓存
  18. list = userService.listObjects(
  19. userQTO.readCache(false) //禁用从缓存读取(注意不是读写) readCache 设置为 false, 返回自己(QTO)
  20. );
  21. //或者写成:
  22. userQTO.readCache(false);
  23. list = userService.listObjects(userQTO);
  24. //例4: 通过条件匹配器Condition 设置不读取缓存
  25. list = userService.listObjects(
  26. Condition.create() //创建条件匹配器
  27. .readCache(false) //禁用从缓存读取
  28. );
  29. //例5: 通过查询构造器 EntitySelect 设置不读取缓存
  30. list = userService.listObjects(
  31. userService.select()//创建 EntitySelect
  32. .where()
  33. .readCache(false)//禁用从缓存读取
  34. .endWhere() //返回 EntitySelect
  35. );
  36. return list;
  37. }
  38. }

2.2. 缓存开关 thread-cache redis-cache与readCache区别

请读者先阅读 章节 《TopFox配置参数》

  1. 一级缓存 top.service.thread-cache 大于 readCache
  2. 二级缓存 top.service.redis-cache 大于 readCache

也就说, 把一级二级缓存关闭了, readCache设置为true, 也不会读取缓存. 所有方式的查询也不会读取缓存.

2.3. 一级缓存的效果

  • 一级缓存默认是关闭的

只打开某个 service的操作的一级缓存

  1. @Service
  2. public class UserService extends AdvancedService<UserDao, UserDTO> {
  3. @Override
  4. public void init() {
  5. sysConfig.setThreadCache(true); //打开一级缓存
  6. }

全局开启一级缓存, 项目配置文件 application.properties 增加

  1. top.service.thread-cache=true
  • 开启一级缓存后
  1. 一级缓存是只当前线程级别的, 线程结束则缓存消失
  2. 下面的例子, 在开启一级缓后 user1,user2和user3是同一个实例的
  3. 一级缓存的效果我们借鉴了Hibernate框架的数据实体对象持久化的思想
  1. @RestController
  2. @RequestMapping("/demo")
  3. public class DemoController {
  4. @Autowired
  5. UserService userService;
  6. @TokenOff
  7. @GetMapping("/test2")
  8. public UserDTO test2() {
  9. UserDTO user1 = userService.getObject(1);//查询后 会放入一级 二级缓存
  10. UserDTO user2 = userService.getObject(1);//会从一级缓存中获取到
  11. userService.update(user2.setName("张三"));
  12. UserDTO user3 = userService.getObject(1);//会从一级缓存中获取到
  13. return user3;
  14. }
  15. }

2.4. 如何将指定字段的值更新为null

下面的 addNullFields 方法, 当字段类型是Number时, 是不能更新为null的, null 将强制转为零.

2.4.1. 单个dto更新 update

  1. public class UnitTestService extends MultiService<RestSession> {
  2. @Autowired UserService userService;
  3. public void update(){
  4. UserDTO user1 = new UserDTO();
  5. user1.setAge(99);
  6. user1.setId(1);
  7. user1.setName("Luoping");
  8. userService.update(user1);//只更新有值的字段
  9. }
  10. }

生成的Sql语句如下:

  1. UPDATE users
  2. SET name='Luoping',age=99
  3. WHERE (id = 1)

从这个例子我们可以看出, TopFox是将dto中 不为null的字段生成更新sql了, 那么能否实现将指定的字段更新为null呢? 如下修改,增加 user1.addNullFields(“sex, lastDate”); 即可

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. public void update(){
  5. UserDTO user1 = new UserDTO();
  6. user1.setAge(99);
  7. user1.setId(1);
  8. user1.setName("Luoping");
  9. //将指定的字段更新为null, 允许有空格
  10. user1.addNullFields(" sex , lastDate ");
  11. //这样写也支持
  12. user1.addNullFields("sex", "lastDate");
  13. user1.addNullFields("sex,lastDate", "deptId");
  14. userService.update(user1);
  15. }
  16. }

生成的Sql语句如下:

  1. UPDATE users
  2. SET name='Luoping',age=99,sex=null,lastDate=null
  3. WHERE (id = 1)

2.4.2. 多个dto更新 updateList

多行更新 addNullFields方法一样有效, 例:

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. public void updateList(){
  5. UserDTO user1 = new UserDTO();
  6. user1.setAge(99);
  7. user1.setId(1);
  8. user1.setName("张三");
  9. user1.addNullFields("sex, lastDate");//将指定的字段更新为null
  10. UserDTO user2 = new UserDTO();
  11. user2.setAge(88);
  12. user2.setId(2);
  13. user2.setName("李四");
  14. user2.addNullFields("mobile, isAdmin");//将指定的字段更新为null
  15. List list = new ArrayList();
  16. list.add(user1);
  17. list.add(user2);
  18. userService.updateList(list);//只更新有值的字段
  19. }
  20. }

生成的Sql语句如下:

  1. UPDATE users
  2. SET name='张三',age=99,sex=null,lastDate=null
  3. WHERE (id = 1)
  4. UPDATE users
  5. SET name='李四',age=88,mobile=null,isAdmin=null
  6. WHERE (id = 2)

2.4.3. 自定条件更新 updateBatch

自定义条件更新 addNullFields方法一样有效, 例:

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. public void updateBatch(){
  5. UserDTO dto = new UserDTO();
  6. dto.setAge(99);
  7. dto.setDeptId(11);
  8. dto.addNullFields("mobile, isAdmin");//将指定的字段更新为null
  9. userService.updateBatch(dto, where().eq("sex","男"));
  10. }
  11. }

生成的Sql语句如下:

  1. UPDATE users
  2. SET deptId=11,age=99,mobile=null,isAdmin=null
  3. WHERE (sex = '男')

2.5. 变化值更新技巧(update-mode=2或者3时)

  • update-mode参数 请参考《TopFox配置参数》章节的描述
  • 强调一下, 在参数配置 update-mode=2或3 时, 才存在变化值更新技巧的事儿.

变化值更新技巧, 我们先看一个例子

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. public void findAndUpdate(){
  5. UserDTO user1 = userService.getObject(1);
  6. user1.setAge(11);
  7. user1.setName("王五");
  8. userService.update(user1);
  9. }
  10. }

生成的Sql语句如下:

  1. UPDATE users
  2. SET code='*',orgId='*',name='王五',password='000000',age=11,amount=0.00,isAdmin=false,loginCount=loginCount+0
  3. WHERE (id = 1) AND (version = 23)

从上面生成SQL的效果可以看出, UserDTO 所有字段的值不是null的,都生成了更新SQL. 当表的字段比较多时性能不是最高, 我们是否能实现只针对有变化的字段生成 update SQL语句呢? TopFox是支持的, 代码修改如下:

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. public void findAndUpdate(){
  5. UserDTO user1 = userService.getObject(1);
  6. //增加这行代码就能实现 根据变化值更新, 必须要放在set新值之前. update-mode=1时无效哦
  7. user1.addOriginFromCurrent();
  8. user1.setAge(11);
  9. user1.setName("王五");
  10. userService.update(user1);
  11. }
  12. }

生成的Sql语句如下:

  1. UPDATE users
  2. SET version=version+1,name='王五',password='000000',age=11
  3. WHERE (id = 1) AND (version = 23)

在稍微改改, 如下(在配置参数 update-mode=3下测试) :

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. public void findAndUpdate(){
  5. UserDTO user1 = userService.getObject(1);
  6. user1.addOriginFromCurrent();
  7. user1.setAge(null); //多了这行
  8. user1.setName("王五");
  9. userService.update(user1);
  10. }
  11. }

执行输出:

  1. ## 注意, 这始终是 变化值, age在数据库的值是11, 现在setAge(null)了, 有变化了.
  2. UPDATE users
  3. SET name='王五',age=null
  4. WHERE (id = 1)

TopFox 实现原理, user1.addOriginFromCurrent() 会为当前值创建一个副本, 在生成SQL时, TopFox会与副本的数据比较, 不同则生成更新SQL.

2.6. 递增递减字段更新 例一

以用户表的DTO为例

  • loginCountAdd 注解中的 Incremental.ADDITION 表示递增, 而 Incremental.SUBTRACT 表示递减
  • loginCountAdd 字段不是数据库存在的一个字段, 更新后 [service.update()] 它值将被处理为 null (判断逻辑 loginCountAdd 与注解的name属性不等时)
  • loginCount 为数据库存在的字段, 执行update后它的值将被处理为 修改后的值
  • @JsonIgnore 注解说明: 如果loginCountAdd字段连null值都不想序列化到redis, 就可以用这个注解
  • transient 修饰符说明: jdk序列化忽略该字段的值

用户表DTO源码:

  1. @Setter
  2. @Getter
  3. @Accessors(chain = true)
  4. @Table(name = "users")
  5. public class UserDTO extends DataDTO {
  6. private Long id
  7. @TableField(name="loginCount", incremental = Incremental.ADDITION)
  8. @JsonIgnore
  9. private transient Integer loginCountAdd;
  10. private Integer loginCount;
  11. }

应用例子:

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. /**
  5. * 递增更新 业务场景,如入库增加库存
  6. */
  7. public void addUpdate(){
  8. UserDTO user1 = new UserDTO().setId(1).setLoginCountAdd(10);
  9. userService.update(user1);
  10. }
  11. }

生成的Sql语句如下:

  1. UPDATE users
  2. SET loginCount=loginCount+10
  3. WHERE id = 1

2.7. 递增递减字段更新 例二

在 《递增递减字段更新 例一》 的基础上, DTO做如下修改, 源码:

  1. @Setter
  2. @Getter
  3. @Accessors(chain = true)
  4. @Table(name = "users")
  5. public class UserDTO extends DataDTO {
  6. private Long id
  7. //@TableField(name="loginCount", incremental = Incremental.ADDITION)
  8. //@JsonIgnore
  9. //private transient Integer loginCountAdd;
  10. @TableField(incremental = Incremental.ADDITION)
  11. private Integer loginCount;
  12. }
  • loginCount为数据库中存在的字段
  • 效果: 更新后 loginCount字段的值将被修改为 改过之后的值, 即与数据库的值保持一致

2.8. 增删改一个方法实现 service.save

以下源码的逻辑重点是, 每个DTO的状态设定方法 dto.dataState(), 目的要告诉TopFox, 这个dto我要做什么操作(insert/update还是delete), 这个标记很重要

UnitTestService.saveTest 源码如下:

  1. @Service
  2. public class UnitTestService extends MultiService<RestSession> {
  3. @Autowired UserService userService;
  4. public void saveTest(){
  5. ic void saveTest(){
  6. UserDTO user1 = new UserDTO();
  7. user1.setAge(11);
  8. user1.setId(1);
  9. user1.setName("张三");
  10. user1.addNullFields("sex, lastDate");
  11. UserDTO user2 = new UserDTO();
  12. user2.setAge(12);
  13. user2.setId(2);
  14. user2.setName("李四");
  15. user2.addNullFields("mobile, isAdmin");
  16. UserDTO user3 = new UserDTO();
  17. user3.setId(3);
  18. List list = new ArrayList();
  19. //TopFox将 自动调用 service.insert 方法
  20. user1.dataState(DbState.INSERT);
  21. //user1.setState(DbState.INSERT);
  22. //TopFox将 自动调用 service.update 方法
  23. user2.dataState(DbState.UPDATE);
  24. //user2.setState(DbState.UPDATE);
  25. //TopFox将 自动调用 service.delete 方法
  26. user3.dataState(DbState.DELETE);
  27. //user3.setState(DbState.DELETE);
  28. list.add(user1);
  29. list.add(user2);
  30. list.add(user3);
  31. userService.save(list);
  32. }
  33. }
  • 在企业内部ERP或者后台系统中, 可能会常碰到一种需求, 一个表格, 可以同时新增修改和删除多条数据, 然后点击一个保存按钮提交到后台. xxxService.save这个方法就是满足这个需求的.
  • 看到这里, 有过前端开发经验的程序员会问, 这个标记能否开放给 HTML5前端, 或者说调用方指定呢?答案是肯定的, 我们先修改 UserDTO的代码, 重点是增加 String state字段, 名字自定, 必须要加上@State注解即可, 修改后的源码如下
  1. @Setter
  2. @Getter
  3. @Accessors(chain = true)
  4. @Table(name = "users")
  5. public class UserDTO extends DataDTO {
  6. /**
  7. *数据状态字段 数据库不存在的字段, 用于描述 transient修饰符 表示 改字段不会被序列化
  8. * @see com.topfox.data.DbState ;
  9. */
  10. @State @TableField(exist = false)
  11. private transient String state= DbState.NONE;
  12. @Id private Integer id;
  13. private String code;
  14. private String name;
  15. ...略
  16. }

然后, 上面的 UnitTestService.saveTest 源码中,

  1. user1.dataState(DbState.INSERT); 可以改为 user1.setState(DbState.INSERT);
  2. user2.dataState(DbState.UPDATE); 可以改为 user2.setState(DbState.UPDATE);
  3. user3.dataState(DbState.DELETE); 可以改为 user3.setState(DbState.DELETE);

两种写法效果是一样的

然后增加控制层

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserController {
  4. /**
  5. * 一次提交,存在 增加 修改 删除的情况
  6. *
  7. * @return
  8. */
  9. @PostMapping("/save")
  10. public Response<List<UserDTO>> save(@RequestBody List<UserDTO> list) {
  11. userService.save(list);
  12. return new Response<List<UserDTO>>(list);
  13. }
  14. }

此时, HTML5前端, 或者说调用方(如PostMan工具)就能指定dto的 state 状态了, 提交的数据格式如下:

  1. // UnitTestService.saveTest 源码效果一样
  2. [
  3. {
  4. "state":"i", //user1 "state":"i" 表示是insert, 将生成插入SQL
  5. "id":1,
  6. "name":"张三",
  7. "age":11,
  8. "sex":,
  9. "lastDate":
  10. },
  11. {
  12. "state":"u", //user2 "state":"u" 表示是 update,将生成更新SQL
  13. "id":2,
  14. "name":"李四",
  15. "age":12,
  16. "mobile":,
  17. "isAdmin":
  18. },
  19. {
  20. "state":"d", //user3 "state":"d" 表示是 delete,将生成删除SQL
  21. "id":3
  22. }
  23. ]

总结两个重点:

  • json格式中的 state 名字 要与 UserDTO中 @State注解的字段名 一样
  • state的值 必须是 com.topfox.data.DbState 中的定义值(i/u/d/n)
  • com.topfox.data.DbState 源码如下
  1. package com.topfox.data;
  2. /**
  3. * DTO的状态值定义
  4. * i 新增
  5. * u 更新
  6. * d 删除
  7. * n 无状态
  8. */
  9. public class DbState {
  10. /*** 新增*/
  11. public static final String INSERT="i";
  12. /*** 删除 */
  13. public static final String DELETE="d";
  14. /** * 修改 */
  15. public static final String UPDATE="u";
  16. /*** 无状态*/
  17. public static final String NONE ="n";
  18. }

2.9. 多主键 查询/删除

下面这个表有两个字段作为主键, userId 和 deptId :

  1. /**
  2. * 薪水津贴模板表
  3. * 假定一个主管 管理了多个部门, 每管理一个部门, 就有管理津贴作为薪水
  4. */
  5. @Setter
  6. @Getter
  7. @Accessors(chain = true)
  8. @Table(name = "salary")
  9. public class SalaryDTO extends DataDTO {
  10. /**
  11. * 两个主键字段, 用户Id 和部门Id
  12. */
  13. @Id
  14. private Integer userId;
  15. @Id
  16. private Integer deptId;
  17. /**
  18. * 管理津贴
  19. */
  20. @JsonFormat(shape = JsonFormat.Shape.NUMBER, pattern = "###0.00")
  21. private BigDecimal amount;
  22. ...
  23. }

表 salary 的数据如下:

userId deptId amount createUser updateUser
1 1 11 * *
1 2 22 * *
1 3 33 * *

::: 重要备注:

1-1, 1-2, 1-2 我们称之为3组主键Id值, 任何一组主键值 可以定位到 唯一的行.

2.9.1. 技巧一: 单组主键值查询

多主键时, sql语句主键字段的拼接顺序是 按照 SalaryDTO 中定义的字段顺序来的.

具体来说, 如 concat(userId,’-‘, deptId) 这个先是 userId, 然后是deptId, 与 SalaryDTO 中定义的字段顺序一致. 因此在拼接Id值时注意顺序要一致.

单组主键值查询, 获得单个DTO对象:

  1. @RestController
  2. @RequestMapping("/salary")
  3. public class SalaryController {
  4. @Autowired
  5. SalaryService salaryService;
  6. protected Logger logger = LoggerFactory.getLogger(getClass());
  7. @GetMapping("/test1")
  8. public SalaryDTO test1() {
  9. return salaryService.getObject("1-2");
  10. }
  11. }

输出SQL:

  1. SELECT userId,deptId,amount,createUser,updateUser
  2. FROM salary a
  3. WHERE (concat(userId,'-', deptId) = '1-2')

2.9.2. 技巧二 : 多组主键值查询

多组主键值查询, 获得多个DTO对象:

  1. @RestController
  2. @RequestMapping("/salary")
  3. public class SalaryController {
  4. @Autowired SalaryService salaryService;
  5. @GetMapping("/test2")
  6. public List<SalaryDTO> test2() {
  7. return salaryService.listObjects("1-1,1-2,1-3");
  8. }
  9. }

输出SQL:

  1. SELECT userId,deptId,amount,createUser,updateUser
  2. FROM salary a
  3. WHERE (concat(userId,'-', deptId) = '1-1'
  4. OR concat(userId,'-', deptId) = '1-2'
  5. OR concat(userId,'-', deptId) = '1-3')

2.9.3. 技巧三: 获取主键字段拼接的SQL

下面的程序代码 打印出来的是字符串: (concat(userId,’-‘, deptId)

  1. @RestController
  2. @RequestMapping("/salary")
  3. public class SalaryController {
  4. @Autowired SalaryService salaryService;
  5. @GetMapping("/test3")
  6. public String test3() {
  7. String idFieldsBySql = salaryService.tableInfo().getIdFieldsBySql();
  8. logger.debug(idFieldsBySql);
  9. return idFieldsBySql;
  10. }
  11. }

2.9.4. 技巧四: 按多组主键值删除

  1. @RestController
  2. @RequestMapping("/salary")
  3. public class SalaryController {
  4. @Autowired SalaryService salaryService;
  5. @GetMapping("/test4")
  6. public void test4() {
  7. salaryService.deleteByIds("1-1,1-2");
  8. }
  9. }

输出SQL:

  1. DELETE FROM salary
  2. WHERE (concat(userId,'-', deptId) = '1-1'
  3. OR concat(userId,'-', deptId) = '1-2')