我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱
我真的不想再用 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);
}
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">protected</span> SQLExpression1<ColumnSetter<Object>> <span class="hljs-title function_">getDeletedSQLExpression</span><span class="hljs-params">(LogicDeleteBuilder builder, String propertyName)</span> {
// LocalDateTime now = LocalDateTime.now();
// return o->o.set(propertyName,now);
// 上面的是错误用法, 将 now 值获取后那么这个 now 就是个固定值而不是动态值
return o->o.set(propertyName,LocalDateTime.now())
.set("deletedUser",CurrentUserHelper.getUserId());
}
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getStrategy</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> <span class="hljs-string">"MyLogicDelStrategy"</span>;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> Set<Class<?>> allowedPropertyTypes() {
<span class="hljs-keyword">return</span> 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/