Topfox 領先的快速開發框架介紹
1. topfox 框架例子
-
資料庫指令碼請參考檔案 db.sql, 共3張表: 部門表depts, 使用者表users, 多主鍵欄位表 salary
-
使用資料庫為 mysql8
2. topfox 使用者使用手冊 - 目錄
2.1. 必備
- 官網: https://gitee.com/topfox/topfox
- 文中例子原始碼: https://gitee.com/topfox/topfox-sample
- TopFox技術交流群 QQ: 874732179
2.2. topfox 介紹
在 srpingboot2.x.x 和MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
程式設計規範參考《阿里巴巴Java開發手冊》
借鑑 mybaties plus 部分思想
特性:
- 無侵入:只做增強不做改變,引入它不會對現有工程產生影響
- 損耗小:啟動即會自動注入基本 CURD,效能基本無損耗,直接面向物件操作
- 整合Redis快取: 自帶Redis快取功能, 支援多主鍵模式, 自定義redis-key. 實現對資料庫的所有操作, 自動更新到Redis, 而不需要你自己寫任何程式碼; 當然也可以針對某個表關閉.
- 強大的 CRUD 操作:內建通用 Mapper、通用 Service,僅僅通過少量配置即可實現單表大部分 CRUD 操作,更有強大的條件構造器,滿足各類使用需求
- 支援 Lambda 形式呼叫:通過 Lambda 表示式,方便的編寫各類查詢條件,無需再擔心欄位寫錯
- 支援主鍵自動生成:可自由配置,充分利用Redis提高效能, 完美解決主鍵問題. 支援多主鍵查詢、修改等
- 內建分頁實現:基於 MyBatis 物理分頁,開發者無需關心具體操作,寫分頁等同於普通查詢
- 支援devtools/jrebel熱部署
- 熱載入 支援在不使用devtools/jrebel的情況下, 熱載入 mybatis的mapper檔案
- 內建全域性、區域性攔截外掛:提供delete、update 自定義攔截功能
- 擁有預防Sql注入攻擊功能
- 無縫支援spring cloud: 後續提供分散式呼叫的例子
3. 更新日誌
3.1. 版本1.2.5 更新日誌 2019-07-30
- CamelHelper為駝峰和下劃線命名互轉的處理類
BeanUtil.toUnderlineName 刪除, 用 CamelHelper.toUnderlineName 代替
BeanUtil.toCamelCase 刪除, 用 CamelHelper.toCamel 代替
3.2. 版本1.2.4 更新日誌 2019-07-24
- 全域性快取引數開關
新增 一級快取開關 top.service.thread-cache
新增 二級快取開關 top.service.redis-cache
刪除 top.service.open-redis
- 多主鍵的支援, 包括: 更新, 刪除, 查詢, 資料校驗元件, 修改日誌元件;
- java遠端呼叫返回空物件的處理;
- 技術文件修改
4. 快速入門
4.1. 入門例子: 以使用者表為例, 開發者只需要完成以下4步的程式碼, 就能實現很多複雜的功能
4.1.1. 新建實體物件 UserDTO
@Setter
@Getter
@Accessors(chain = true)
@Table(name = "users", cnName = "使用者表")
public class UserDTO extends DataDTO {
@Id private Integer id;
private String code;
private String name;
private String password;
private String sex;
private Integer age;
...等
}
4.1.2. 新建查詢條件物件Query( 即UserQTO )
@Setter
@Getter
@Accessors(chain = true)
@Table(name = "users")
public class UserQTO extends DataQTO {
private String id;
private String code;
private String name;
private String nameOrEq;
private String sex;
private Date lastDateFrom;
private Date lastDateTo;
}
4.1.3. 新建UserDao
@Component
public interface UserDao extends BaseMapper<UserDTO> {
/**
* 自定方法 mapper.xml 程式碼略
* @param qto
* @return
*/
UserDTO test(UserQTO qto);
}
4.1.4. 新建 UserService
@Service
public class userService extends SimpleService<UserDao, UserDTO> {
@Override
public int insert(UserDTO dto) {
return super.insert(dto);
}
@Override
public int update(UserDTO dto) {
return super.update(dto);
}
@Override
public int deleteByIds(Number... ids) {
return super.deleteByIds(ids);
}
@Override
public int deleteByIds(String... ids) {
return super.deleteByIds(ids);
}
//以上4個方法的程式碼可以刪除, 沒什麼邏輯, 這裡只是告訴讀者有這些方法, 但父類的方法遠遠不止這4個
/**
* 自定的方法
* @param qto
* @return
*/
public List<userDTO> test(UserQTO qto) {
return baseMapper.test(qto);
}
}
實現哪些具體的功能呢, 詳見後面的章節
4.2. 功能強大的查詢
4.2.1. 條件匹配器Condition 查詢一
以下僅僅是條件匹配器的部分功能, 更多功能等待使用者挖掘.
@RestController
@RequestMapping("/condition")
public class ConditionController {
@Autowired
UserService userService;
/**
* 條件匹配器的一個例子
*/
@GetMapping("/query1")
public List<UserDTO> query1(){
//**查詢 返回物件 */
List<UserDTO> listUsers = userService.listObjects(
Condition.create() //建立條件匹配器物件
.between("age",10,20) //生成 age BETWEEN 10 AND 20
.eq("sex","男") //生成 AND(sex = '男')
.eq("name","C","D","E")//生成 AND(name = 'C' OR name = 'D' OR name = 'E')
.like("name","A", "B") //生成 AND(name LIKE '%A%' OR name LIKE '%B%')
//不等
.ne("name","張三","李四")
//等同於 .eq("substring(name,2)","平")
.add("substring(name,2)='平' ")//自定義條件
.le("loginCount",1)//小於等於
.lt("loginCount",2)//小於
.ge("loginCount",4)//大於等於
.gt("loginCount",3)//大於
.isNull("name")
.isNotNull("name")
);
return listUsers;
}
}
生成的WHERE條件如下:
SELECT id,code,name,password,sex,age,amount,mobile,isAdmin,loginCount,lastDate,deptId,createUser,updateUser
FROM users a
WHERE age BETWEEN 10 AND 20
AND (sex = '男')
AND (name = 'C' OR name = 'D' OR name = 'E')
AND (name LIKE '%A%' OR name LIKE '%B%')
AND (name <> '張三' AND name <> '李四')
AND substring(name,2)='平'
AND (loginCount <= 1)
AND (loginCount < 2)
AND (loginCount >= 4)
AND (loginCount > 3)
AND name is null
AND name is not null
LIMIT 0,6666
4.2.2. 條件匹配器Condition 查詢二
@RestController
@RequestMapping("/condition")
public class ConditionController {
@Autowired
UserService userService;
@GetMapping("/query2")
public List<UserDTO> query2(){
//**查詢 返回物件 */
List<UserDTO> listUsers = userService.listObjects(
userService.where() // 等同於 Condition.create() 建立一個條件匹配器物件
.eq("concat(name,id)","A1") //生成 (concat(name,id) = 'A1')
.eq("concat(name,id)","C1","D2","E3")//生成 AND (concat(name,id) = 'C1' OR concat(name,id) = 'D2' OR concat(name,id) = 'E3' )
);
return listUsers;
}
}
生成的WHERE條件如下:
SELECT id,code,name,password,sex,age,amount,mobile,isAdmin,loginCount,lastDate,deptId,createUser,updateUser
FROM users a
WHERE (concat(name,id) = 'A1')
AND (concat(name,id) = 'C1'
OR concat(name,id) = 'D2'
OR concat(name,id) = 'E3' )
4.3. 高階查詢 帶分組, 排序, 自定select 後欄位, 指定分頁的查詢
利用查詢構造器 EntitySelect 和 Condition的查詢
/**
* 核心使用 繼承了 topfox 的SimpleService
*/
@Service
public class CoreService extends SimpleService<UserDao, UserDTO> {
public List<UserDTO> demo2(){
List<UserDTO> listUsers=listObjects(
select("name, count('*')") //通過呼叫SimpleService.select() 獲得或建立一個新的 EntitySelect 物件,並返回它
.where() //等同於 Condition.create()
.eq("sex","男") //條件匹配器自定義條件 返回物件 Condition
.endWhere() //條件結束 返回物件 EntitySelect
.orderBy("name") //設定排序的欄位 返回物件 EntitySelect
.groupBy("name") //設定分組的欄位 返回物件 EntitySelect
.setPage(10,5) //設定分頁(查詢第10頁, 每頁返回5條記錄)
);
return listUsers;
}
}
輸出sql如下:
SELECT name, count('*')
FROM users a
WHERE (sex = '男')
GROUP BY name
ORDER BY name
LIMIT 45,5
4.4. 查詢時如何才能不讀取快取
TopFox 實現了快取處理, 當前執行緒的快取 為一級快取, redis為二級快取.
通過設定 readCache 為false, 能實現在開啟一級/二級快取的情況下又不讀取快取, 從而保證讀取出來的資料和資料庫中的一模一樣, 下面通過5個例子來說明.
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
UserService userService;
@TokenOff
@GetMapping("/test1")
public Object test1(UserQTO userQTO) {
//例1: 根據id查詢, 通過第2個引數傳false 就不讀取一二級快取了
UserDTO user = userService.getObject(1, false);
//例2: 根據多個id查詢, 要查詢的id放入Set容器中
Set setIds = new HashSet();
setIds.add(1);
setIds.add(2);
//通過第2個引數傳false 就不讀取一二級快取了
List<UserDTO> list = userService.listObjects(setIds, false);
//例3: 通過QTO 設定不讀取快取
list = userService.listObjects(
userQTO.readCache(false) //禁用從快取讀取(注意不是讀寫) readCache 設定為 false, 返回自己(QTO)
);
//或者寫成:
userQTO.readCache(false);
list = userService.listObjects(userQTO);
//例4: 通過條件匹配器Condition 設定不讀取快取
list = userService.listObjects(
Condition.create() //建立條件匹配器
.readCache(false) //禁用從快取讀取
);
return list;
}
}
4.5. 查詢 快取開關 thread-cache redis-cache與readCache區別
請讀者先閱讀 章節 《TopFox配置引數》
一級快取 top.service.thread-cache 大於 readCache
二級快取 top.service.redis-cache 大於 readCache
也就說, 把一級二級快取關閉了, readCache設定為true, 也不會讀取快取. 所有方式的查詢也不會讀取快取.
4.6. 開啟一級快取
- 一級快取預設是關閉的
只打開某個 service的操作的一級快取
@Service
public class UserService extends SimpleService<UserDao, UserDTO> {
@Override
public void init() {
sysConfig.setThreadCache(true); //開啟一級快取
}
全域性開啟一級快取, 專案配置檔案 application.properties 增加
top.service.thread-cache=true
- 開啟一級快取後
- 一級快取是隻當前執行緒級別的, 執行緒結束則快取消失
- 下面的例子, 在開啟一級緩後 user1,user2和user3是同一個例項的
- 一級快取的效果我們借鑑了Hibernate框架的資料實體物件持久化的思想
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
UserService userService;
@TokenOff
@GetMapping("/test2")
public UserDTO test2() {
UserDTO user1 = userService.getObject(1);//查詢後 會放入一級 二級快取
UserDTO user2 = userService.getObject(1);//會從一級快取中獲取到
userService.update(user2.setName("張三"));
UserDTO user3 = userService.getObject(1);//會從一級快取中獲取到
return user3;
}
}
4.7. 開啟二級快取 Redis
- 二級快取預設是關閉的
只打開某個 service的操作的二級快取
@Service
public class UserService extends SimpleService<UserDao, UserDTO> {
@Override
public void init() {
sysConfig.setRedisCache(true); //開啟一級快取
}
全域性開啟一級快取, 專案配置檔案 application.properties 增加
top.service.redis-cache=true
:::備註
- 開啟後, 查詢優先會讀取一級快取, 沒有就會讀取二級快取, 再沒有就會從資料庫中獲取
- 開啟後, 利用TopFox 的service的 新增/修改/刪除 操作都會自動同步到 redis中
4.8. QTO字尾增強查詢
我們修改 UserQTO 的原始碼如下:
@Setter
@Getter
@Table(name = "users")
public class UserQTO extends DataQTO {
private String id; //使用者id, 與資料欄位名一樣的
private String name; //使用者姓名name, 與資料欄位名一樣的
private String nameOrEq; //使用者姓名 字尾OrEq
private String nameAndNe; //使用者姓名 字尾AndNe
private String nameOrLike; //使用者姓名 字尾OrLike
private String nameAndNotLike;//使用者姓名 字尾AndNotLike
...
}
- 欄位名 字尾OrEq 當 nameOrEq 寫值為 "張三,李四" 時, 原始碼如下:
package com.test.service;
/**
* 核心使用 demo1 原始碼 集成了 TopFox 的 SimpleService類
*/
@Service
public class CoreService extends SimpleService<UserDao, UserDTO> {
public List<UserDTO> demo1(){
UserQTO userQTO = new UserQTO();
userQTO.setNameOrEq("張三,李四");//這裡賦值
//依據QTO查詢 listObjects會自動生成SQL, 不用配置 xxxMapper.xml
List<UserDTO> listUsers = listObjects(userQTO);
return listUsers;
}
}
則生成SQL:
SELECT ...
FROM SecUser
WHERE (name = '張三' OR name = '李四')
- 欄位名 字尾AndNe 當 nameAndNe 寫值為 "張三,李四" 時, 則生成SQL:
SELECT ...
FROM SecUser
WHERE (name <> '張三' AND name <> '李四')
- 欄位名 字尾OrLike 當 nameOrLike 寫值為 "張三,李四" 時, 則將生成SQL:
SELECT ...
FROM SecUser
WHERE (name LIKE CONCAT('%','張三','%') OR name LIKE CONCAT('%','李四','%'))
- 欄位名 字尾AndNotLike 當 nameAndNotLike 寫值為 "張三,李四" 時, 則生成SQL:
SELECT ...
FROM SecUser
WHERE (name NOT LIKE CONCAT('%','張三','%') AND name NOT LIKE CONCAT('%','李四','%'))
以上例子是TopFox全自動生成的SQL
4.9. 更多的查詢方法
- Response< List < DTO > > listPage(EntitySelect entitySelect)
- List< Map < String, Object > > selectMaps(DataQTO qto)
- List< Map < String, Object > > selectMaps(Condition where)
- List< Map < String, Object > > selectMaps(EntitySelect entitySelect)
- selectCount(Condition where)
- selectMax(String fieldName, Condition where)
- 等等
4.10. 自定條件更新 updateBatch
@Service
public class UnitTestService {
@Autowired UserService userService;
public void test(){
UserDTO dto = new UserDTO();
dto.setAge(99);
dto.setDeptId(11);
dto.addNullFields("mobile, isAdmin");//將指定的欄位更新為null
List<UserDTO> list userService.updateBatch(dto, where().eq("sex","男"));
// list為更新過得記錄
}
}
生成的Sql語句如下:
UPDATE users
SET deptId=11,age=99,mobile=null,isAdmin=null
WHERE (sex = '男')
4.11. 更多的 插入 和更新的程式碼例子
@Service
public class UnitTestService {
@Autowired UserService userService;
...
public void insert(){
//Id為資料庫自增, 新增可以獲得Id
UserDTO dto = new UserDTO();
dto.setName("張三");
dto.setSex("男");
userService.insertGetKey(dto);
logger.debug("新增使用者的Id 是 {}", dto.getId());
}
public void update(){
UserDTO user1 = new UserDTO();
user1.setAge(99);
user1.setId(1);
user1.setName("Luoping");
//將指定的欄位更新為null, 允許有空格
user1.addNullFields(" sex , lastDate , loginCount");
// //這樣寫也支援
// user1.addNullFields("sex","lastDate");
// //這樣寫也支援
// user1.addNullFields("sex, lastDate","deptId");
userService.update(user1);//只更新有值的欄位
}
public void update1(){
UserDTO user1 = new UserDTO();
user1.setAge(99);
user1.setId(1);
user1.setName("Luoping");
userService.update(user1);//只更新有值的欄位
}
public void updateList(){
UserDTO user1 = new UserDTO();
user1.setAge(99);
user1.setId(1);
user1.setName("張三");
user1.addNullFields("sex, lastDate");
UserDTO user2 = new UserDTO();
user2.setAge(88);
user2.setId(2);
user2.setName("李四");
user2.addNullFields("mobile, isAdmin");
List list = new ArrayList();
list.add(user1);
list.add(user2);
userService.updateList(list);//只更新有值的欄位
}
資料校驗元件之實戰- 重複檢查
假如使用者表中已經有一條使用者記錄的 手機號是 13588330001, 然後我們再新增一條手機號相同的使用者, 或者將其他某條記錄的手機號更新為這個手機號, 此時我們希望 程式能檢查出這個錯誤, CheckData物件就是幹這個事的. 檢查使用者手機號不能重複有如下多種寫法:
4.11.1. 示例一
@Service
public class CheckData1Service extends AdvancedService<UserDao, UserDTO> {
@Override
public void beforeInsertOrUpdate(List<UserDTO> list) {
//多行記錄時只執行一句SQL完成檢查手機號是否重複, 並丟擲異常
checkData(list) // 1. list是要檢查重複的資料
// 2.checkData 為TopFox在 SimpleService裡面定義的 new 一個 CheckData物件的方法
.addField("mobile", "手機號") //自定義 有異常丟擲的錯誤資訊的欄位的中文標題
.setWhere(where().ne("mobile","*")) //自定檢查的附加條件, 可以不寫(手機號為*的值不參與檢查)
.excute();// 生成檢查SQL, 並執行, 有結果記錄(重複)則丟擲異常, 回滾事務
}
}
控制檯 丟擲異常 的日誌記錄如下:
##這是 inert 重複檢查 TopFox自動生成的SQL:
SELECT concat(mobile) result
FROM SecUser a
WHERE (mobile <> '*')
AND (concat(mobile) = '13588330001')
LIMIT 0,1
14:24|49.920 [4] DEBUG 182-com.topfox.util.CheckData | mobile {13588330001}
提交資料{手機號}的值{13588330001}不可重複
at com.topfox.common.CommonException$CommonString.text(CommonException.java:164)
at com.topfox.util.CheckData.excute(CheckData.java:189)
at com.topfox.util.CheckData.excute(CheckData.java:75)
at com.sec.service.UserService.beforeInsertOrUpdate(UserService.java:74)
at com.topfox.service.AdvancedService.beforeSave2(AdvancedService.java:104)
at com.topfox.service.SimpleService.updateList(SimpleService.java:280)
at com.topfox.service.SimpleService.save(SimpleService.java:451)
at com.sec.service.UserService.save(UserService.java:41)
- 異常資訊的 "手機號" 是 .addField("mobile", "手機號") 指定的中文名稱
- 假如使用者表用兩條記錄, 第一條使用者id為001的記錄手機號為13588330001, 第一條使用者id為002的記錄手機號為13588330002. <br>如果我們把第2條記錄使用者的手機號13588330002改為13588330001, 則會造成了 資料重複, TopFox執行的檢查重複的SQL語句為:
##這是 update時重複檢查 TopFox自動生成的SQL:
SELECT concat(mobile) result
FROM SecUser a
WHERE (mobile <> '*')
AND (concat(mobile) = '13588330001')
AND (id <> '002') ## 修改使用者手機號那條記錄的使用者Id
LIMIT 0,1
通過這個例子, 希望讀者能理解 新增和更新 TopFox 生成SQL不同的原因.
4.11.2. 更多例子請參考 << 資料校驗元件>> 章節
4.12. 更新日誌元件 ChangeManager 分散式事務 回滾有用哦
獲得修改日誌可寫入到 mongodb中, 控制分散式事務 回滾有用哦
讀取修改日誌的程式碼很簡單, 共寫了2個例子, 如下:
@Service
public class UserService extends AdvancedService<UserDao, UserDTO> {
@Override
public void afterInsertOrUpdate(UserDTO userDTO, String state) {
if (DbState.UPDATE.equals(state)) {
// 例一:
ChangeManager changeManager = changeManager(userDTO)
.addFieldLabel("name", "使用者姓名") //設定該欄位的日誌輸出的中文名
.addFieldLabel("mobile", "手機號"); //設定該欄位的日誌輸出的中文名
//輸出 方式一 引數格式
logger.debug("修改日誌:{}", changeManager.output().toString() );
// 輸出樣例:
/**
修改日誌:
id:000000, //使用者的id
使用者姓名:開發者->開發者2,
手機號:13588330001->1805816881122
*/
// 輸出 方式二 JSON格式
logger.debug("修改日誌:{}", changeManager.outJSONString() );
// 輸出樣例: c是 current的簡寫, 是當前值, 新值; o是 old的簡寫, 修改之前的值
/**
修改日誌:
{
"appName":"sec",
"executeId":"1561367017351_14",
"id":"000000",
"data":{
"version":{"c":"207","o":206},
"使用者姓名":{"c":"開發者2","o":"開發者"},
"手機號":{"c":"1805816881122","o":"13588330001"}
}
}
*/
//************************************************************************************
// 例二 沒有用 addFieldLabel 設定欄位輸出的中文名, 則data中的keys輸出全部為英文
logger.debug("修改日誌:{}", changeManager(userDTO).outJSONString() );
// 輸出 JSON格式
/**
修改日誌:
{
"appName":"sec",
"executeId":"1561367017351_14",
"id":"000000",
"data":{
"version":{"c":"207","o":206},
"name":{"c":"開發者2","o":"開發者"},
"mobile":{"c":"1805816881122","o":"13588330001"}
}
}
*/
//************************************************************************************
}
}
}
4.13. 流水號生成器 KeyBuild
- 簡單的流水號, 我們定義為 是遞增的序列號
- keyBuild()方法是 類庫封裝的建立 KeyBuild物件的方法.
4.13.1. 簡單流水號
:::示例一
- 假如表中只有2條資料, id 欄位的值分別為 001, 002, 則執行下面程式獲得的值是003
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test1() {
//logger為TopFox宣告的日誌物件
//例: 根據UserDTO中欄位名id 來獲取一個純 3位數 遞增的流水號
logger.debug(
keyBuild() //建立一個 KeyBuild物件, 會自動獲取當前Service的 UserDTO 物件
.getKey("id",3) //引數id 必須是 UserDTO中存在的欄位
); //打印出來的值是 003
}
}
:::示例二
- 假如表中只有6條資料, id 欄位的值分別為 06,07, 112,113, 2222,2223 這裡有長度為2,3,4位的Id值, 執行下面的程式, debug的資訊分別是08, 114, 2224.
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test2() {
logger.debug(keyBuild().getKey("id",2)); //打印出來的值是 08
logger.debug(keyBuild().getKey("id",3)); //打印出來的值是 114
logger.debug(keyBuild().getKey("id",4)); //打印出來的值是 2224
//這個例子說明是按照 id欄位 值的長度隔離的.
}
}
總結:
- 流水號是通過分析當前service的UserDTO對應表的已有資料而生成的, 並將分析結果快取到Redis中, 減少對錶的讀取.
- 流水號的生成是按照表名,欄位名和已有資料的長度 隔離的
- 位數滿後會自動增加1位, 例如獲得2位數的流水號, 當99後, 再次獲取會增加一位變為100
- 獲取到流水號後, 是不會因為丟擲異常而回滾, 每次呼叫始終 加一的. <br>例如 獲取到 2224後拋一個異常, 事務是回滾了, 但下次獲取這個流水號, 取到的是 2225(2224不會回滾).這樣設計主要是考慮到"避免分散式下高併發 流水號可能會重複的問題".
- 這是按照呼叫次數 變化的數字, 我們稱之為是 "遞增的次序號". 位數不足用 0 填補
4.13.2. 複雜流水號(含字首|日期|字尾)
- 流水號 = 字首 + 日期字元 + 遞增的序列號 + 字尾
- 如何設定 字首和日期字元,以及字尾呢? 請看如下例子:
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
/**
* 每行資料執行本方法一次,新增和修改的 之前的邏輯寫到這裡, 如通用的檢查, 計算值得處理
*/
public void test3() {
//獲取一個 帶字首TL 帶日期字元(yyMMdd) + 6位數遞增的序列號 的流水號
logger.debug(
keyBuild()
.setPrefix("TL") //設定字首
.setSuffix("END") //設定字尾
.setDateFormat("yyyyMMdd") //設定日期格式
.getKey("id",3) //引數依次是 1.欄位名 2.序列號長度
);
}
}
- 假如生成的流水號 是 TL20190601001END , 其中 TL 是字首, 20190601是年月日, 001是遞增的序列號, END 是字尾
- 日期格式可以自定, 例如: yyyyMMdd yyMM MMdd yyMMdd yMMDD
4.13.3. 批量流水號
一次要獲得多個流水號, 如企業內部系統 的 訂單匯入等, 建議用如下辦法獲得一批流水號
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test4() {
logger.debug("獲得多個流水號");
//獲得多個序列號
ConcurrentLinkedQueue<String> queue =
keyBuild("TL", "yyMMdd") //字首, 設定日期格式
.getKeys("id", 6, 4); //引數依次是 1.欄位名 2.序列號長度 3.要獲得流水號個數
// poll 執行一次, 容器 queue裡面少一個
logger.debug(queue.poll());//獲得第1個序列號
logger.debug(queue.poll());//獲得第2個序列號
logger.debug(queue.poll());//獲得第3個序列號
logger.debug(queue.poll());//獲得第4個序列號
}
}
也可以寫成
package com.test.service;
@Service
public class KeyBuildService extends AdvancedService<UserDao, UserDTO> {
public void test5() {
logger.debug("獲得多個流水號");
//獲得多個序列號
ConcurrentLinkedQueue<String> queue =
keyBuild()
.setPrefix("TL") //設定字首
.setDateFormat("yyyyMMdd") //設定日期格式
.getKeys("id", 6, 4); //引數依次是 1.欄位名 2.序列號長度 3.要獲得流水號個數
... 略
}
}
4.14. 多主鍵 查詢/刪除
下面這個表有兩個欄位作為主鍵, userId 和 deptId :
/**
* 薪水津貼模板表
* 假定一個主管 管理了多個部門, 每管理一個部門, 就有管理津貼作為薪水
*/
@Setter
@Getter
@Accessors(chain = true)
@Table(name = "salary")
public class SalaryDTO extends DataDTO {
/**
* 兩個主鍵欄位, 使用者Id 和部門Id
*/
@Id
private Integer userId;
@Id
private Integer deptId;
/**
* 管理津貼
*/
@JsonFormat(shape = JsonFormat.Shape.NUMBER, pattern = "###0.00")
private BigDecimal amount;
...
}
表 salary 的資料如下:
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物件:
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired
SalaryService salaryService;
protected Logger logger = LoggerFactory.getLogger(getClass());
@GetMapping("/test1")
public SalaryDTO test1() {
return salaryService.getObject("1-2");
}
}
輸出SQL:
SELECT userId,deptId,amount,createUser,updateUser
FROM salary a
WHERE (concat(userId,'-', deptId) = '1-2')
4.14.2. 技巧二 : 多組主鍵值查詢
多組主鍵值查詢, 獲得多個DTO物件:
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired SalaryService salaryService;
@GetMapping("/test2")
public List<SalaryDTO> test2() {
return salaryService.listObjects("1-1,1-2,1-3");
}
}
輸出SQL:
SELECT userId,deptId,amount,createUser,updateUser
FROM salary a
WHERE (concat(userId,'-', deptId) = '1-1'
OR concat(userId,'-', deptId) = '1-2'
OR concat(userId,'-', deptId) = '1-3')
4.14.3. 技巧三: 獲取主鍵欄位拼接的SQL
下面的程式程式碼 打印出來的是字串: (concat(userId,'-', deptId)
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired SalaryService salaryService;
@GetMapping("/test3")
public String test3() {
String idFieldsBySql = salaryService.tableInfo().getIdFieldsBySql();
logger.debug(idFieldsBySql);
return idFieldsBySql;
}
}
4.14.4. 技巧四: 按多組主鍵值刪除
@RestController
@RequestMapping("/salary")
public class SalaryController {
@Autowired SalaryService salaryService;
@GetMapping("/test4")
public void test4() {
salaryService.deleteByIds("1-1,1-2");
}
}
輸出SQL:
DELETE FROM salary
WHERE (concat(userId,'-', deptId) = '1-1'
OR concat(userId,'-', deptId) = '1-2')
5. 上下文物件 AppContext 如何使用
下面原始碼中的 RestSession和RestSessionConfig物件可以參考 <<快速使用>>章節中的相關內容
AppContext 提供了幾個靜態方法, 直接獲取相關物件.
package com.user.controller;
import com.topfox.annotation.TokenOff;
import com.sys.RestSession;
import AbstractRestSessionConfig;
import com.topfox.common.AppContext;
import com.topfox.common.SysConfigRead;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/context")
public class AppContextController {
/**
* AppContext.getRestSessionHandler()是同一個例項
*/
@Autowired RestSessionConfig restSessionConfig;
@TokenOff
@GetMapping("/test1")
public void test1() {
Environment environment = AppContext.environment();
RestSessionConfig restSessionHandlerConfig = (RestSessionConfig)AppContext.getRestSessionHandler();
restrestSessionConfig RestSession restSession = AppContext.getRestSession();
SysConfigRead configRead = AppContext.getSysConfig();
System.out.println(configRead);
}
@TokenOff
@GetMapping("/test2")
public void test2() {
RestSession restSession = restSessionConfig.restSessionConfigsConfigRead configRead = restSessionConfig.restSessionConfig }
}
6. TopFox配置引數
以下引數在專案 application.properties 檔案中配置, 不配置會用預設值. 下面的等號後面的值就是預設值.
6.1. top.log.start="▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼..."
debug 當前執行緒開始 日誌輸出分割
6.2. top.log.prefix="# "
debug 中間日誌輸出字首
6.3. top.log.end=▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲..."
debug 當前執行緒結束 日誌輸出分割符
6.4. top.page-size=100
分頁時,預設的每頁條數
6.5. top.max-page-size=300
不分頁時(pageSize<=0),查詢時最多返回的條數
6.6. [新增] top.service.thread-cache=false
是否開啟一級快取(執行緒快取), 預設false 關閉, 查詢不會讀取一級快取
6.7. [新增] top.service.redis-cache=false
是否開啟二級快取(redis快取), 預設false 關閉, 替代老的 open-redis
6.8. top.service.open-redis=false 作廢
service層是否開啟redis快取,
6.9. top.service.redis-log=flase
日誌級別是DEBUG時, 是否列印 操作redis的操作日誌
預設 false 不列印操作redis的日誌
true 列印操作redis的日誌
引數配置為true時, 控制檯列印的日誌大概如下:
# DEBUG 112-com.topfox.util.DataCache 更新後寫入Redis成功 com.user.entity.UserDTO hashCode=2125196143 id=0
##DEBUG 112-com.topfox.util.DataCache更新後寫入Redis成功 com.user.entity.UserDTO hashCode=1528294732 id=1
##DEBUG 112-com.topfox.util.DataCache查詢後寫入Redis成功 com.user.entity.UserDTO hashCode=620192016 id=2
6.10. top.redis.serializer-json=true
# redis序列化支援兩種, true:jackson2JsonRedisSerializer false:JdkSerializationRedisSerializer
# 注意, 推薦生產環境下更改為 false, 類庫將採用JdkSerializationRedisSerializer 序列化物件,
# 這時必須禁用devtools(pom.xml 註釋掉devtools), 否則報錯.
6.11. top.service.update-mode=3
更新時DTO序列化策略 和 更新SQL生成策略
重要引數:
引數值為 1 時, service的DTO=提交的資料.
更新SQL 提交資料不等null 的欄位 生成 set field=value
引數值為 2 時, service的DTO=修改前的原始資料+提交的資料.
更新SQL (當前值 != 原始資料) 的欄位 生成 set field=value
引數值為 3 時, service的DTO=修改前的原始資料+提交的資料.
更新SQL (當前值 != 原始資料 + 提交資料的所有欄位)生成 set field=value
始終保證了前臺(呼叫方)提交的欄位, 不管有沒有修改, 都能生成更新SQL, 這是與2最本質的區別
6.12. top.service.select-by-before-update=false
top.service.update-mode=1 時本引數才生效
預設值為false
更新之前是否先查詢(獲得原始資料). 如果需要獲得修改日誌, 又開啟了redis, 建議在 update-mode=1時, 將本引數配置為true
6.13. top.service.update-not-result-error=true
根據Id更新記錄時, sql執行結果(影響的行數)為0時是否丟擲異常
預設 true 丟擲異常
false 不拋異常
6.14. top.service.sql-camel-to-underscore=OFF
生成SQL 是否駝峰轉下劃線 預設 OFF
一共有3個值:
- OFF 關閉, 生成SQL 用駝峰命名
- ON-UPPER 開啟, 下劃線並全大寫
- ON-LOWER 開啟, 下劃線並全小寫
7. Topfox 在執行時更改引數值---物件 SysConfig
- SysConfig 介面的實現類是 com.topfox.util.SysConfigDefault
package com.topfox.util;
public interface SysConfig extends SysConfigRead {
/**
* 對應配置檔案中的 top.service.update-mode
*/
void setUpdateMode(Integer value);
/**
* 對應配置檔案中的 top.service.open-redis
*/
void setRedisCache(Boolean value);
/**
* 對應配置檔案中的 top.service.update-not-result-error
*/
void setUpdateNotResultError(Boolean value);
...等等, 沒有全部列出
}
- 以上介面定義的方法是set方法, 允許在執行時 修改, 每個service 都有一個SysConfig的副本, 通過set更改的值只對當前service有效.
- 使用場景舉例:
以引數 open-redis為例: <br> 我們假定專案配置檔案 application.properties中開啟了 讀寫Redis 的功能, 即 top.service.open-redis=true , 此時的含義表示, 當前專案的所有service操作資料庫的增刪改查的資料都會同步到Redis中. 那問題來了, 假如剛好 UserService 需要關閉open-redis, 怎麼處理呢, 程式碼如下:
@Service
public class UserService extends AdvancedService<UserDao, UserDTO> {
@Override
public void init() {
/**
1. sysConfig 為 AdvancedService的父類 SuperService 中定義的 變數, 直接使用即可
2. sysConfig的預設值 來自於 application.properties 中的設定的值,
如果 application.properties 中沒有定義, 則TopFox會自動預設一個
3.sysConfig中定義的引數在這裡都可以更改
*/
//關閉了 UserService 讀寫redis的功能, 其他service不受影響
sysConfig.setOpenRedis(false);
}
}
這樣呼叫了 UserService 的 getObject listObjects update insert delete 等方法操作的資料是不會同步到redis的 . <br>其他引數同理可以在執行時修改
7.1. 必備
- 官網: https://gitee.com/topfox/topfox
- 文中例子原始碼: https://gitee.com/topfox/topfox-sample
- TopFox技術交流群