MyBatis-Pro,新一代的MyBatis增强框架
地址
框架功能
- 包含单表增删改查方法
- 与通用Mapper、MyBatis-Plus等三方框架兼容(三者选其一即可,功能类似)
- 【可选】内置枚举类型处理器,优雅解决枚举类型问题,不需要手动转换
- 【可选】内置泛型Service,避免重复造轮子编写大量类似的Service方法代码
- 【可选】内置两种方式逻辑删除,可放心大胆的在生产环境进行delete操作,不用担心误删数据
- 【可选】分页插件 支持单表、多表关联查询、支持复杂的多表分页查询
- 【可选】sql打印插件 已经用实际参数替换了
?
占位符,可以从日志文件拷贝出来直接执行 - 【可选】乐观锁插件 透明解决乐观锁问题
接入方法
Spring Boot
<dependency>
<groupId>com.github.dreamroute</groupId>
<artifactId>mybatis-pro-boot-starter</artifactId>
<version>latest version</version>
</dependency>
目前支持的配置
# 是否开启内置枚举转换器,默认false
mybatis.pro.enable-enum-type-handler = false
# 是否开启逻辑删除,默认false
mybatis.pro.enable-logical-delete = false
# 逻辑删除开启时有效:逻辑删除类型,backup-备份方式;update-更新方式,默认backup
mybatis.pro.logical-delete-type=backup
# 逻辑删除开启时,并且删除类型是backup时有效:逻辑删除表名,默认logical_delete
mybatis.pro.logical-delete-table=
# 逻辑删除开启,删除类型是update时有效,状态列,默认status:
mybatis.pro.logical-delete-column=xxx
# 逻辑删除开启,删除类型是update时有效,表示未被删除,默认1
mybatis.pro.logical-delete-active=
# 逻辑删除开启,删除类型是update时有效,表示被删除,默认0
mybatis.pro.logical-delete-in-active=
内置方法
- 将你的Mapper接口继承
com.github.dreamroute.mybatis.pro.sdk.BaseMapper
接口 - 在启动类上使用@MapperScan注解指明你的Mapper接口路径
- 此时你的接口就拥有了Mapper接口的所有通用方法,如下:
- 方法参数cols为可变参数,代表列名,指定select需要查询的哪些列,不传则代表查询全部列,所有的
findBy
方法也支持String... cols
这种动态列方式,cols
必须是方法的最后一个参数
T selectById(ID id, String... clos); // 根据主键id查询单个对象
List<T> selectByIds(List<ID> ids, String... clos); // 根据主键id集合查询多个对象
List<T> selectAll(String... clos); // 查询全部
int insert(T entity); // 新增
int insertExcludeNull(T entity); // 新增,值为null的属性不进行保存,使用数据库默认值
int insertList(List<T> entityList); // 批量新增
int updateById(T entity); // 根据主键id修改
int updateByIdExcludeNull(T entity); // 根据主键id修改,值为null的属性不进行修改
int deleteById(ID id); // 根据id删除(物理删除)
int deleteByIds(List<ID> ids); // 根据id列表进行删除(物理删除)
实体对象注解
@Data
@Table("smart_user")
public class User {
@Id
private Long id;
private String name;
private String password;
private Long version;
@Transient
private Integer gender;
// 列的别名,数据库列和Java实体的属性不一致时使用此属性
@Column("mobile")
private String phoneNo;
}
说明:
- @com.github.dreamroute.mybatis.pro.core.annotations.Table:属性必填,为表名
- @com.github.dreamroute.mybatis.pro.core.annotations.Id:标记的字段表示主键(必填),默认为自增,可根据@Id的属性type属性修改主键策略
- @com.github.dreamroute.mybatis.pro.core.annotations.Transient:(可选)表示在新增时此字段不持久化到数据库
- @com.github.dreamroute.mybatis.pro.core.annotations.Column:实体属性与数据列的映射关系(可选,mybatis的mybatis.configuration.map-underscore-to-camel-case=true时会自动进行下划线转驼峰,默认是false)
特别说明
框架内部由于使用了findBy
,existBy
, deleteBy
, countBy
这几个xxxBy开头的方法,在框架中属于是特殊方法,因此你的mapper普通查询方法不能使用这种关键字开头,会与框架冲突
灵魂功能
1、Mapper接口的方法名根据特定的书写规则进行查询,用户无需编写sql语句
2、方法名以findBy、countBy、existBy、deleteBy开头,属性首字母大写,多个属性使用And或者Or连接
3、对于findByXxxIn和findByXxxNotIn这种传入的参数是List类型的,那么方法参数名也定义成list,否则可能会报错,比如findByNameIn(List<String> list),如果不清楚某些Mapper方法的参数应该如何命名,那么请参考下方【全部功能】举的例子,比如between查询的两个参数就应该使用(start, end),而不能用其他的名字
比如:
public interface UserMapper extends Mapper<User, Long> {
// select '全部列' from xx where name = #{name} and password = #{password}
User findByNameAndPassword(String name, String password);
// select count(*) c from xx where name = #{name}
int countByName(String name);
// select 'cols参数指定的列' from xx where name = #{name} and password like '%#{password}%'
List<User> findByNameAndPasswordLike(String name, String password, String... cols);
// delete from xx where name = #{name} and version = #{version}
int deleteByNameAndVersion(String name, Long version);
}
参数为空问题
有时候查询参数为空的时候,我们期望此条件不参与查询,比如Mapper接口为:
User findByNameAndPassword(String name, String password)
我们希望name为null
或者空字符串时候,name这个条件不参与查询,在Mapper方法尾部加上Opt
即可
User findByNameAndPasswordOpt(String name, String password)
此时的sql语句就成为了:
select '全部列' from user where password = #{password}
Opt
结尾的mapper方法自动就将空参数给移除掉了
指定列名
在进行findBy
和内置的select方法查询时,如果不希望使用select '全部列'
的方式,那么可以在定义mapper接口的时候最后一个参数定义成动态参数或者数组,参数名为:cols
,在调用的时候传入列名即可,比如:
public interface UserMapper {
User findById(Long id, String... cols);
}
// 调用
User user = userMapper.findById(1L, "id", "name");
驼峰和下划线转换问题
mybatis有一个配置叫做:map-underscore-to-camel-case
,表示下划线是否转驼峰,默认false
,mybatis-pro也依赖此配置。
对于findBy countBy existBy deleteBy
开头的方法,后续的条件如果是驼峰,那么就根据上述的属性表示是否转换。比如方法:
findByUserName(String userName);
如果map-underscore-to-camel-case = true
,那么就会将驼峰userName
转换成下划线user_name
。这种最后sql类似这样:
select '全部列' from user where user_name = #{userName}
如果map-underscore-to-camel-case = false
,那么userName
就保持不变, sql类似这样:
select '全部列' from user where userName = #{userName}
全部功能
一个方法可以有多个and或者or拼接多个条件,
如:findByNameLikeOrPasswordIsNotNullAndVersion(String name, String password, Long version)
效果:where name like '%#{name}%' or password is not null and version = #{version}
关键字 | 示例 | 效果 |
---|---|---|
and | findByNameAndPassword(String name, String password) | where name = #{name} and # |
or | findByNameOrPassword(String name, String password) | where name = #{name} or # |
count | countByName(String name) | select count(*) c from xx where name = # |
exist | existByName(String name) | 查询结果大于等于1,那么返回true,否则返回false |
delete | deleteByName(String name) | delete from x where name = # |
Between | findByAgeBetween(Integer start, Integer end ) | where age between #{start} and # |
LT(LessThan) | findByAgeLT(Integer age) | where age < # |
LTE(LessThanEqual) | findByAgeLTE(Integer age) | where age <= # |
GT(GreaterThan) | findByAgeGT(Integer age) | where age > # |
GTE(GreaterThanEqual) | findByAgeLTE(Integer age) | where age >= # |
IsNull | findByNameIsNull | where name is null |
IsNotNull | findByNameIsNotNull | where name is not null |
IsBlank | findByNameIsBlank | where name is null or name = '' |
IsNotBlank | findByNameIsNotBlank | where name is not null and name != '' |
Like | findByNameAndPasswordLike(String name, String password) | where name = #{name} and password like '%#{password}%' |
NotLike | findByNameNotLike(String name) | where name not like '%#{name}%' |
StartWith | findByNameStartWith(String name) | where name like '#{name}%' |
EndWith | findByNameEndWith(String name) | where name like '%#{name}' |
Not | findByNameNot(String name) | where name <> # |
In | findByNameIn(List<String> list) | where name in ('A', 'B', 'C') |
NotIn | findByNameNotIn(List<String> list) | where name not in ('A', 'B', 'C') |
OrderBy | findByNameOrderById(String name) | where name = #{name} order by id |
Desc | findByNameOrderByIdDesc(String name) | where name = #{name} order by id desc |
最佳实践
提供一些工作中总结出来的一些最佳实践,包括通用泛型Service,放在mybatis-pro-service模块里面
通用泛型Service
-
由于我们业务系统大多数的service都需要具备基础的crud功能,所以此框架提供了泛型Service能力
-
使用方法:
-
你的Mapper接口继承
com.github.dreamroute.mybatis.pro.service.mapper.BaseMapper
接口,如下:public interface UserMapper extends BaseMapper<User, Long> {}
-
你的Service(比如
UserService
)继承com.github.dreamroute.mybatis.pro.service.BaseService<T, ID>
,泛型参数分别是对应的实体类型和主键id类型,如下:public interface UserService extends BaseService<User, Long> {}
-
你的Service实现类(比如
UserServiceImpl
),需要继承com.github.dreamroute.mybatis.pro.service.AbstractServiceImpl
,并且实现你你的UserService
接口,如下:@Service public class UserServiceImpl extends AbstractServiceImpl<User, Long> implements UserService {}
-
在数据库创逻辑删除备份表建表(可以不创建),关于逻辑删除见下方“逻辑删除说明”:
CREATE TABLE `logical_delete` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `table_name` varchar(100) NOT NULL, `data` json NOT NULL, `delete_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- 于是你的UserService就具备了如下能力:
T insert(T entity); T insertExcludeNull(T entity); List<T> insertList(List<T> entityList); int delete(ID id); int deleteDanger(ID id); int delete(List<ID> ids); int deleteDanger(List<ID> ids); int update(T entity); int updateExcludeNull(T entity); T select(ID id); List<T> select(List<ID> ids); List<T> selectAll();
- 接下来你就可以在你的Controller使用UserService了,如:
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/selectById") public User selectById(Long id) { return userService.select(id); } }
- 于是你的UserService就具备了如下能力:
-
-
逻辑删除(备份方案):
- 通用Service内包含了删除方法,对于删除方法,我们有两种方式处理,一种是直接物理删除,一种是逻辑删除;
- 私认为逻辑删除可能要稳当一些,出了问题好排查一点;
- 于是对于框架的删除方法,我们做了两类,一类是物理删除,一类是逻辑删除,
delete()
为逻辑删除,deleteDanger()
是物理删除 - 对于逻辑删除的处理,有2种方案,一种是记录有一个字段是状态字段,对于删除的记录进行update操作,修改状态字段,把状态改为“删除”状态,另一种是删除的时候进行物理删除,然后把此数据移动到其他地方进行存储,由于前者仅仅是修改状态,有时候会破坏唯一索引(数据一直存在),加之任何查询都得带上类似
state != 1
这种状态行为,而后者就不存在这个问题 - 这里的做法是:将所有删除的数据都放在同一个表里面,于是需要在数据库创建一张备份表用于存放被删除的数据,表名默认是:
logical_delete
,也可以在application.yml里面通过属性指定:mybatis.pro.logical-delete-table = xxx
,当然如果你不使用此方案的逻辑删除,系统里全是物理删除,也可以不创建此表,使用配置mybatis.pro.enable-logical-delete
关闭逻辑删除即可;- 关于将整个库的删除数据都放在同一个表是否合适:微服务时代,每个库的表并不多,另外对于绝大多数据业务系统删除操作都不是很频繁的发生。所以基本上是合适的。
-
逻辑删除(使用update代替delete方案):
- 虽然上述方案更可取,但是部分业务系统对于删除很敏感,业务方由于误操作需要频繁的让开发人员恢复数据,这就导致了使用update方式更为合理。目前框架是兼容此方案的,使用
mybatis.pro.logical-delete-type=update
开启此功能,使用mybatis.pro.logical-delete-column=xxx
(默认是字段是status
)来标记逻辑删除列的列名,使用mybatis.pro.logical-delete-in-activ = xxx
的值表示逻辑删除状态值(默认是0
),使用mybatis.pro.logical-delete-active=xx
的值表示正常数据(也就是未被删除数据,默认是1
),那么,对于基础方法selectById
,selectByIds
,selectAll
和findBy
开头的方法的sql结尾都会加上类似xxx = ${mybatis.pro.logical-delete-active}
的条件。比如:
- 虽然上述方案更可取,但是部分业务系统对于删除很敏感,业务方由于误操作需要频繁的让开发人员恢复数据,这就导致了使用update方式更为合理。目前框架是兼容此方案的,使用
mybatis.pro.enable-logical-delete=true
mybatis.pro.logical-delete-type=update
mybatis.pro.logical-delete-column=status
mybatis.pro.logical-delete-active=1
mybatis.pro.logical-delete-in-active=0
selectById` => `select * from xxx where id = #{id} AND state = 1
selectAll` => `select * from xxx where state = 1
- 如果开启了逻辑删除,但是依然想对某些数据做物理删除,【待开发此功能】
Adaptor
- 1、请求参数:系统中大量存在根据id和id数组来进行查询的请求,于是将id和id数组进行封装,放在
com.github.dreamroute.mybatis.pro.service.adaptor.id
包内; - 2、javax中的参数校验不是很全面,将一些参数校验放在
com.github.dreamroute.mybatis.pro.service.adaptor.validator
包中;
额外功能
- 1、对枚举类型的支持:在开发过程中,对于数据库的
字典
字段,一般我们在数据库使用tinyint
这种类型,Java代码中一般使用枚举类型,我们希望将枚举类型的数字类型的值存入数据库,而不是枚举类型的名称,比如存在如下性别类型的枚举:
@Getter
@AllArgsConstructor
public enum Gender implements EnumMarker {
MALE(1, "男"), FEMALE(2, "女");
private final Integer value;
private final String desc;
}
我们希望存入数据库的是1
或者2
,而不是MALE
和FEMALE
,而在我们进行查询的时候自动将数字类型转换成枚举类型,mybatis的typehandler刚好支持此功能,于是mybatis-pro内置枚举转换typehandler完成此功能,并且,我希望把此功能做得更加通用,业务代码都实现EnumMarker
使用相同的规约,于是定义了EnumMarker
接口和EnumTypeHandler
转换器,枚举类型实现EnumMarker
有2个作用:1、实现此接口的枚举将自动自动转型,不实现此接口则无法享受此待遇;2、接口的两个get
方法分别是获取value
和desc
的值;如下实体就会自动进行转型:
@Getter
@AllArgsConstructor
public enum Gender implements EnumMarker {
MALE(1, "男"), FEMALE(2, "女");
private final Integer value;
private final String desc;
}
@Data
@Table("user")
public class User {
@Id
private Long id;
// 性别(1-男;2-女)
private Gender gender;
}
// 调用:userMapper.insert(User user); // 自动将gender的值1或者2保存到数据
// 调用:User user = userMapper.selectById(Long id); // 自动将gender的值1或者2转换成Gender类型存入user