1. 程式人生 > >Topfox 領先的快速開發框架介紹

Topfox 領先的快速開發框架介紹

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 的資料如下:

userIddeptIdamountcreateUserupdateUser
1111**
1222**
1333**

::: 重要備註:

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. 必備