文中涉及的例子源碼下載: https://gitee.com/topfox/topfox-sample
TopFox技術交流群 QQ: 874732179
在 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: 後續提供分散式調用的例子
新增 一級緩存開關 top.service.thread-cache
新增 二級緩存開關 top.service.redis-cache
刪除 top.service.open-redis
多主鍵的支持, 包括: 更新, 刪除, 查詢, 數據校驗組件, 修改日誌組件;
java遠程調用返回空對象的處理;
技術文檔修改
@Setter
@Getter
@Accessors(chain = true)
@Table(name = "users", cnName = "用戶表")
public class UserDTO extends DataDTO {
@Id private Integer id;
private String code;
private String name;
private String password;
private String sex;
private Integer age;
...等
}
@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;
}
@Component
public interface UserDao extends BaseMapper<UserDTO> {
/**
* 自定方法 mapper.xml 代碼略
* @param qto
* @return
*/
UserDTO test(UserQTO qto);
}
@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);
}
}
實現哪些具體的功能呢, 詳見後面的章節
以下僅僅是條件匹配器的部分功能, 更多功能等待用戶挖掘.
@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
@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' )
利用查詢構造器 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
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;
}
}
請讀者先閱讀 章節 《TopFox配置參數》
一級緩存 top.service.thread-cache 大於 readCache
二級緩存 top.service.redis-cache 大於 readCache
也就說, 把一級二級緩存關閉了, readCache設置為true, 也不會讀取緩存. 所有方式的查詢也不會讀取緩存.
只打開某個 service的操作的一級緩存
@Service
public class UserService extends AdvancedService<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;
}
}
我們修改 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
...
}
當 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 = '李四')
當 nameAndNe 寫值為 "張三,李四" 時, 則生成SQL:
SELECT ...
FROM SecUser
WHERE (name <> '張三' AND name <> '李四')
當 nameOrLike 寫值為 "張三,李四" 時, 則將生成SQL:
SELECT ...
FROM SecUser
WHERE (name LIKE CONCAT('%','張三','%') OR name LIKE CONCAT('%','李四','%'))
當 nameAndNotLike 寫值為 "張三,李四" 時, 則生成SQL:
SELECT ...
FROM SecUser
WHERE (name NOT LIKE CONCAT('%','張三','%') AND name NOT LIKE CONCAT('%','李四','%'))
以上例子是TopFox全自動生成的SQL
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)
等等
@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 = '男')
@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對象就是干這個事的.
檢查用戶手機號不能重複有如下多種寫法:
@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不同的原因.
獲得修改日誌可寫入到 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"}
}
}
*/
//************************************************************************************
}
}
}
[admin
]