我真的不想再用mybatis和其衍生框架了選擇自研亦是一種解脫

2023-07-26 12:00:39

我真的不想再用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,支援entityinsertupdate但是不支援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