# 我真的不想再用mybatis和其衍生框架了選擇自研亦是一種解脫 [文檔地址](https://xuejm.gitee.io/easy-query-doc/) https://xuejm.gitee.io/easy-query-doc/ [GITHUB地址](https://github.com/ ...
我真的不想再用mybatis和其衍生框架了選擇自研亦是一種解脫
文檔地址 https://xuejm.gitee.io/easy-query-doc/
GITHUB地址 https://github.com/xuejmnet/easy-query
GITEE地址 https://gitee.com/xuejm/easy-query
為什麼要用orm
眾所鄒知orm的出現讓本來以sql實現的複雜繁瑣功能大大簡化,對於大部分程式員而言一個框架的出現是為了生產力的提升.。dbc定義了交互資料庫的規範,任何資料庫的操作都是只需要滿足jdbc規範即可,而orm就是為了將jdbc的操作進行簡化。我個人“有幸”體驗過.net和java的兩個大orm,只能說差距很大,當然語言上的一些特性也讓java在實現orm上有著比較慢的進度,譬如泛型的出現,lambda的出現。
一個好的orm我覺得需要滿足以下幾點
- 強類型,如果不支持強類型那麼和手寫sql沒有區別
- 能實現80%的純手寫sql的功能,好的orm需要覆蓋業務常用功能
- 支持泛型,“如果一個orm連泛型都不支持那麼就沒有必要存在”這是一句現實但是又很殘酷的結論,但是泛型會大大的減少開發人員的編寫錯誤率
- 不應該依賴過多的組件,當然這並不是orm特有的,任何一個庫其實依賴越少越不易出bug
其實說了這麼多總結一下就是一個好的orm應該有ide的提示外加泛型約束幫助開發可以非常順滑的把代碼寫下去,並且錯誤部分可以完全的在編譯期間提現出來,運行時錯誤應該儘可能少的去避免。
為什麼放棄mybatis
首先如果你用過其他語言的orm那麼再用java的mybatis就像你用慣了java的stream然後去自行處理數據過濾,就像你習慣了kotlin的語法再回到java語法,很難受。這種難受不是自動擋到手動擋的差距,而且自動擋到手推車的差距。
xml
配置sql也不知道是哪個“小天才”想出來的,先不說寫代碼的時候java代碼和xml代碼跳來跳去,而且xml下>
,<
必須要配合CDATA
不然xml解析就失敗,別說轉義,我寫那玩意在加轉義你確定讓我後續看得眼睛不要累死嗎?美名其曰xml和代碼分離方便維護,但是你再怎麼方便修改了代碼一樣需要重啟,並且因為代碼寫在xml裡面導致動態條件得能力相對很弱。並且我也不知道mybatis為什麼天生不支持分頁,需要分頁插件來支持,難道一個3202年的orm了還需要這樣嗎,很難搞懂mybatis的作者難道不寫crud代碼的嗎?有些時候簡潔並不是偷懶的原因,當然也有可能是架構的問題導致的。
邏輯刪除的功能我覺得稍微正常一點的企業一定都會有這個功能,但是因為使用了myabtis,因為手寫sql,所以常常會忘記往sql中添加邏輯刪除欄位,從而導致一些奇奇怪怪的bug需要排查,因為這些都是編譯器無法體現的錯誤,因為他是字元串,因為mybatis把這個問題的原因指向了用戶,這一點他很聰明,這個是用戶的錯誤而不是框架的,但是框架要做的就是儘可能的將一些重覆工作進行封裝隱藏起來自動完成。
可能又會有一些用戶會說所見即所得這樣我才能知道他怎麼執行了,但是現在哪個orm沒有sql列印功能,哪個orm框架執行的sql和列印的sql是不一樣的,不是所見即所得。總體而言我覺得mybatis
充其量算是sqltemlate,比sqlhelper好的地方就是他是參數化防止sql註入。當然最主要的呀一點事難道java程式員不需要修改表,不需要動表結構,不需要後期維護的嗎還是說java程式員寫一個項目就換一個地方跳槽,還是說java程式員每個方法都有單元測試。我在轉java後理解了一點,原來這就是你們經常說的java加班嚴重,用這種框架加班不嚴重就有鬼了。
為什麼放棄mybatis衍生框架
有幸在201幾年再網上看到了mybatis-plus
框架,這塊框架一齣現就吸引了我,因為他在處理sql的方式上和.net的orm很相似,起碼都是強類型,起碼不需要java文件和xml文件跳來跳去,平常50%的代碼也是可以通過框架的lambda表達式來實現,我個人比較排斥他的字元串模式的querywrapper
,因為一門強類型語言缺少了強類型提示,在編寫代碼的時候會非常的奇怪。包括後期的重構,當然如果你的代碼後續不需要你維護那麼我覺得你用哪種方式都是ok的反正是一次性的,能出來結果就好了。
繼續說mybatis-plus
,因為工作的需要再2020年左右針對內部框架進行改造,並且讓mybatis-plus支持強類型group by,sum,min,max,any等api。
這個時候其實大部分情況下已經可以應對了,就這樣用了1年左右這個框架,包括後續的update的increment
,decrement
update table set column=column-1 where id=xxx and column>1
全部使用lambda強類型語法,可以應對多數情況,但是針對join始終沒有一個很好地方法。直到我遇到了mpj
也就是mybatis-plus-join
,但是這個框架也有問題,就是這個邏輯刪除在join的子表上不生效,需要手動處理,如果生效那麼在where上面,不知道現在怎麼樣了,當時我也是自行實現了讓其出現在join的on後面,但是因為實現是需要實現某個介面的,所以並沒有pr代碼.
首先定義一個介面
public interface ISoftDelete {
Boolean getDeleted();
}
//其中join mapper是我自己的實現,主要還是`WrapperFunction`的那段定義
@Override
public Scf4jBaseJoinLinq<T1,TR> on(WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> onFunction) {
WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> join= on->{
MPJAbstractLambdaWrapper<T1, ?> apply = onFunction.apply(on);
if(ISoftDelete.class.isAssignableFrom(joinClass)){
SFunction deleted = LambdaHelper.getFunctionField(joinClass, "deleted", Boolean.class);
apply.eq(deleted,false);
}
return apply;
};
joinMapper.setJoinOnFunction(query->{
query.innerJoin(joinClass,join);
});
return joinMapper;
}
雖然實現了join
但是還是有很多問題出現和bug。
- 比如不支持vo對象的返回,只能返回資料庫對象自定義返回列,不然就是查詢所有列
- 再比如如果你希望你的對象update的時候填充null到資料庫,那麼只能在entity欄位上添加,這樣就導致這個欄位要麼全部生效要麼全部不生效.
- 批量插入不支持預設居然是foreach一個一個加,當然這也沒關係,但是你真的想實現批處理需要自己編寫很複雜的代碼並且需要支持全欄位。而不是null列不填充
MetaObjectHandler
,支持entity
的insert
和update
但是不支持lambdaUpdateWrapper
,有時候當前更新人和更新時間都是需要的,你也可以說資料庫可以設置最後更新時間,但是最後修改人呢?- 非常複雜的動態表名,拜托大哥我只是想改一下表名,目前的解決方案就是try-finally每次用完都需要清理一下當前線程,因為tomcat會復用線程,通過threadlocal來實現,話說pagehelper應該也是這種方式實現的吧
當然其他還有很多問題導致最終我沒辦法忍受,選擇了自研框架,當然我的框架自研是參考了一部分的freesql和sqlsuagr的api,並且還有java的beetsql的實現和部分方法。畢竟站在巨人的肩膀上才能看的更遠,不要問我為什麼不參考mybatis的,我覺得mybatis已經把簡單問題複雜化了,如果需要看懂他的代碼是一件很得不償失的事情,最終我發現我的選擇是正確的,我通過參考beetsql
的源碼很快的清楚了java這邊應該需要做的事情,為我編寫後續框架節約了太多時間,這邊也給beetsql
打個廣告https://gitee.com/xiandafu/beetlsql
自研orm有哪些特點
easy-query
一款無任何依賴的java全新高性能orm支持 單表 多表 子查詢 邏輯刪除 多租戶 差異更新 聯級一對一 一對多 多對一 多對多 分庫分表(支持跨表查詢分頁等) 動態表名 資料庫列高效加解密支持like crud攔截器 原子更新 vo對象直接返回
文檔地址 https://xuejm.gitee.io/easy-query-doc/
GITHUB地址 https://github.com/xuejmnet/easy-query
GITEE地址 https://gitee.com/xuejm/easy-query
- 強類型,可以幫助團隊在構建和查詢數據的時候擁有id提示,並且易於後期維護。
- 泛型可以控制我們編寫代碼時候的一些低級錯誤,比如我只查詢一張表,但是where語句裡面可以使用不存在上下文的表作為條件,進一步限制和加強表達式
- easy-query提供了三種模式分別是lambda,property,apt proxy其中lambda表達式方便重構維護,property只是性能最好,apt proxy方便維護,但是重構需要一起重構apt文件
單表查詢
//根據條件查詢表中的第一條記錄
List<Topic> topics = easyQuery
.queryable(Topic.class)
.limit(1)
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LIMIT 1
<== Total: 1
//根據條件查詢id為3的集合
List<Topic> topics = easyQuery
.queryable(Topic.class)
.where(o->o.eq(Topic::getId,"3").eq(Topic::geName,"4")
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` = ? AND t.`name` = ?
==> Parameters: 3(String),4(String)
<== Total: 1
多表
Topic topic = easyQuery
.queryable(Topic.class)
//join 後面是雙參數委托,參數順序表示join表順序,可以通過then函數切換
.leftJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where(o -> o.eq(Topic::getId, "3"))
.firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LEFT JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Total: 1
List<BlogEntity> blogEntities = easyQuery
.queryable(Topic.class)
//join 後面是雙參數委托,參數順序表示join表順序,可以通過then函數切換
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
//join查詢select必須要帶對應的返回結果,可以是自定義dto也可以是實體對象,如果不帶對象則返回t表主表數據
.select(BlogEntity.class, (t, t1) -> t1.columnAll())
.toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic t INNER JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: 3(String)
<== Total: 1
子查詢
```java
//SELECT * FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<BlogEntity> subQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "1"));
List<Topic> x = easyQuery
.queryable(Topic.class).where(o -> o.exists(subQueryable.where(q -> q.eq(o, BlogEntity::getId, Topic::getId)))).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE EXISTS (SELECT 1 FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ? AND t1.`id` = t.`id`)
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 3(ms)
<== Total: 1
//SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<String> idQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "123"))
.select(String.class, o -> o.column(BlogEntity::getId));//如果子查詢in string那麼就需要select string,如果integer那麼select要integer 兩邊需要一致
List<Topic> list = easyQuery
.queryable(Topic.class).where(o -> o.in(Topic::getId, idQueryable)).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE t.`id` IN (SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?)
==> Parameters: false(Boolean),123(String)
<== Time Elapsed: 2(ms)
<== Total: 0
自定義邏輯刪除
//@Component //如果是spring
public class MyLogicDelStrategy extends AbstractLogicDeleteStrategy {
/**
* 允許datetime類型的屬性
*/
private final Set<Class<?>> allowTypes=new HashSet<>(Arrays.asList(LocalDateTime.class));
@Override
protected SQLExpression1<WherePredicate<Object>> getPredicateFilterExpression(LogicDeleteBuilder builder,String propertyName) {
return o->o.isNull(propertyName);
}
@Override
protected SQLExpression1<ColumnSetter<Object>> getDeletedSQLExpression(LogicDeleteBuilder builder, String propertyName) {
// LocalDateTime now = LocalDateTime.now();
// return o->o.set(propertyName,now);
//上面的是錯誤用法,將now值獲取後那麼這個now就是個固定值而不是動態值
return o->o.set(propertyName,LocalDateTime.now())
.set("deletedUser",CurrentUserHelper.getUserId());
}
@Override
public String getStrategy() {
return "MyLogicDelStrategy";
}
@Override
public Set<Class<?>> allowedPropertyTypes() {
return allowTypes;
}
}
//為了測試防止數據被刪掉,這邊採用不存在的id
logicDelTopic.setId("11xx");
//測試當前人員
CurrentUserHelper.setUserId("easy-query");
long l = easyQuery.deletable(logicDelTopic).executeRows();
==> Preparing: UPDATE t_logic_del_topic_custom SET `deleted_at` = ?,`deleted_user` = ? WHERE `deleted_at` IS NULL AND `id` = ?
==> Parameters: 2023-04-01T23:15:13.944(LocalDateTime),easy-query(String),11xx(String)
<== Total: 0
差異更新
- 要註意是否開啟了追蹤
spring-boot
下用@EasyQueryTrack
註解即可開啟- 是否將當前對象添加到了追蹤上下文 查詢添加
asTracking
或者 手動將查詢出來的對象進行easyQuery.addTracking(Object entity)
TrackManager trackManager = easyQuery.getRuntimeContext().getTrackManager();
try{
trackManager.begin();
Topic topic = easyQuery.queryable(Topic.class)
.where(o -> o.eq(Topic::getId, "7")).asTracking().firstNotNull("未找到對應的數據");
String newTitle = "test123" + new Random().nextInt(100);
topic.setTitle(newTitle);
long l = easyQuery.updatable(topic).executeRows();
}finally {
trackManager.release();
}
==> Preparing: UPDATE t_topic SET `title` = ? WHERE `id` = ?
==> Parameters: test1239(String),7(String)
<== Total: 1
關聯查詢
一對一
學生和學生地址
//資料庫對像查詢
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.toList();
//vo自定義列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.select(SchoolStudentVO.class,o->o.columnAll()
.columnInclude(SchoolStudent::getSchoolStudentAddress,SchoolStudentVO::getSchoolStudentAddress))
.toList();
多對一
學生和班級
//資料庫對像查詢
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.toList();
//自定義列
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass,s->s.column(SchoolClassVO::getId))
)
.toList();
//vo自定義列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass)
)
.toList();
一對多
班級和學生
//資料庫對像查詢
List<SchoolClass> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.toList();
//vo自定義列映射返回
List<SchoolClassVO> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolStudents,SchoolClassVO::getSchoolStudents))
.toList();
多對多
班級和老師
List<SchoolClass> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers,1))
.toList();
List<SchoolClassVO> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolTeachers,SchoolClassVO::getSchoolTeachers))
.toList();
動態報名
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a -> "aa_bb_cc")
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a->{
if("t_blog".equals(a)){
return "aa_bb_cc1";
}
return "xxx";
})
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc1 t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> x_t_blog = easyQuery
.queryable(Topic.class)
.asTable(o -> "t_topic_123")
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.asTable("x_t_blog")
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
.select(BlogEntity.class, (t, t1) -> t1.columnAll()).toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic_123 t INNER JOIN x_t_blog t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: false(Boolean),3(String)
<== Total: 0
最後
感謝各位看到最後,希望以後我的開源框架可以幫助到您,如果您覺得有用可以點點star,這將對我是極大的鼓勵
更多文檔信息可以參考git地址或者文檔
文檔地址 https://xuejm.gitee.io/easy-query-doc/
GITHUB地址 https://github.com/xuejmnet/easy-query
GITEE地址 https://gitee.com/xuejm/easy-query