Skip to content

developframework/mybatis-extension

Repository files navigation

mybatis-extension

对mybatis的功能扩展包,丝滑接入不影响原有mybatis功能

快速使用

基于spring-boot

<dependency>
    <groupId>com.github.developframework</groupId>
    <artifactId>mybatis-extension-spring-boot-starter</artifactId>
</dependency>
mybatis:
  mapperLocations: 'classpath:mybatis/mapper/*.xml'
  typeAliasesPackage: '自己的实体包路径'
  extension:
    enableDDL: true # 开启自动创建表

这里不需要使用mybatis.configLocation参数,因为jar包里已实现了ConfigurationCustomizer覆盖默认配置

SQL日志打印前缀为mybatis.extension

<logger name="mybatis.extension" additivity="false" level="DEBUG">
    <appender-ref ref="console"/>
</logger>

脚本

<dependency>
    <groupId>com.github.developframework</groupId>
    <artifactId>mybatis-extension-launcher</artifactId>
</dependency>
// 数据源信息
DataSourceMetadata metadata = new DataSourceMetadata()
                .setJdbcUrl("jdbc:mysql://")
                .setUsername("")
                .setPassword("");

// 构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = ExtensionMybatisLauncher.open(metadata, new MybatisCustomize() {

    @Override
    public void handleConfiguration(Configuration configuration) {
        // 注册Mapper接口
        configuration.getMapperRegistry().addMapper(GoodsMapper.class);
        // 可以注册转换器
        configuration.getTypeHandlerRegistry().register(GoodsSpecArrayTypeHandler.class);
    }

    @Override
    public boolean enableDDL() {
        // 开启自动建表
        return true;
    }

    @Override
    public List<? extends AutoInjectProvider> customAutoInjectProviders() {
        return List.of(
            // 配置自动注入
        );
    }
});
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
    // 获取Mapper开始脚本处理
    final GoodsMapper mapper = sqlSession.getMapper(GoodsMapper.class);
    
}

BaseMapper 通用Mapper接口

提供通用方法

方法 说明
insert 插入记录
insertAll 批量插入记录
replace 替换记录
replaceAll 批量替换记录
update 更新记录
deleteById 根据id删除记录
existsById 根据id查询存在
selectById 根据id查询记录
selectByIdLock 根据id查询记录(可以锁)
selectByIdArray 根据id数组查询记录
selectByIdArrayLock 根据id数组查询记录(可以锁)
selectByIds 根据id集合查询记录
selectByIdsLock 根据id集合查询记录(可以锁)
selectAll 查询所有记录
exists 根据SqlCriteriaAssembler拼装SQL查询存在
select 根据SqlCriteriaAssembler拼装SQL查询记录
selectPager 根据SqlCriteriaAssembler拼装SQL分页查询记录

示例:

public interface GoodsMapper extends BaseMapper<GoodsPO, Integer> {
    // 其中已包含了上述所有SQL操作
}

实体标注

其中实体类中可以用注解标注字段申明

@Getter
@Setter
@Table("goods") // 标注表名
public class GoodsPO {

    @Id // 标注ID字段
    private Integer id;

    // 商品名称
    private String goodsName;

    // 数量
    private Integer quantity;

    // 创建时间
    @CreateTime
    private LocalDateTime createTime;
    
    // 上架
    private boolean enable;
    
    // 逻辑删除标识
    @LogicDelete
    private boolean delete;

    // 规格 多个值
    @Column(nullable = false, typeHandler = StringArrayTypeHandler.class) // 标注自定义类型处理器
    private String[] specifications;
}

预设注解:

注解 说明
@Table 标注表信息
@Id 标注主键
@Column 标注字段特性(没有特殊指定特性可不标注)
@Transient 排除字段,不属于数据库字段
@Version 乐观锁字段,详见乐观锁章节
@CreateTime 自动注入创建时间,详见自动注入章节
@LastModifyTime 自动注入修改时间,详见自动注入章节
@LogicDelete 逻辑删除标识,详见逻辑删除章节

@Table

@Table(
    value = "goods", // 表名
    indexes = {
        @Index(type = IndexType.UNIQUE, properties = "goodsName") // 标注索引
    },
    comment = "商品表"	// 表注释
) // 标注表名
public class GoodsPO {

}

@Id

单一主键
@Id(
    idGenerator = AutoIncrementIdGenerator.class, // ID生成器
    useGeneratedKey = true // 局部开启自增回填功能
)
private Integer id;

如果实体类没有标注@Id,会把名字叫id的字段作为主键

Optional<GoodsPO> goods = selectById(1);
SELECT * FROM goods WHERE `id` = 1
复合主键

可以标注多个@Id,但不适用自增

@Id(idGenerator = NoIdGenerator.class)
private String name;

@Id(idGenerator = NoIdGenerator.class)
private String mobile;

接口上可以使用CompositeId代表复合主键

public interface PersonMapper extends BaseMapper<PersonPO, CompositeId> {

}
Optional<PersonPO> goods = selectById(
    new CompositeId()
        .id("name", "张三")
        .id("mobile", "18888888888")
);

最终执行的SQL效果

SELECT * FROM person WHERE `name` = '张三' AND `mobile` = '18888888888'
ID生成器

可以实现IdGenerator接口定义自己的ID生成器

public interface IdGenerator {

    Object generate(Object entity);
}

默认自带的实现有两个:

  • AutoIncrementIdGenerator 数据库自增实现
  • NoIdGenerator 无ID生成器

@Column

@Column(
    name = "specifications",    // 重定义字段名 很少用 DDL相关
    customizeType = "VARCHAR(100)", // 自定义类型申明
    javaType = void.class,  // 定义mybatis的javaType 很少用
    jdbcType = JdbcType.UNDEFINED,  // 定义mybatis的jdbcType 很少用
    typeHandler = StringArrayTypeHandler.class, // // 标注自定义类型处理器
    nullable = false  // 该字段是否能null DDL相关
    length = 50, // 长度 DDL相关
    scale = 2, // 精度 DDL相关
    unsigned = true, // 是否无符号 DDL相关
    defaultValue = "0", // null时的默认值 DDL相关
    comment = "" // 字段注释 DDL相关
) 
private String[] specifications;
  • nullable 会影响使用update的策略
goodsMapper.update(goods);

nullable=true时对象内的null值字段会被修改成null

nullable=false时对象内的null值会被跳过不修改

逻辑删除

提供@LogicDelete注解标注逻辑删除字段

自动注入

提供@AutoInject注解来自动注入预设值,需要实现AutoInjectProvider接口来申明值的来源

public interface AutoInjectProvider {

    /**
     * 哪些SQL操作类型需要注入
     * <p>
     * INSERT or UPDATE
     */
    SqlCommandType[] needInject();

    /**
     * 提供注入值
     */
    Object provide(Type fieldType);
}

已设置两个审计常用注入值@CreateTime@LastModifyTime,支持字段类型:

  • LocalDateTime
  • ZonedDateTime
  • LocalDate
  • LocalTime
  • Instant
  • java.util.Date
  • java.sql.Date
  • java.sql.Timestamp
@CreateTime  // 等价于内置@AutoInject(AuditCreateTimeAutoInjectProvider.class)
private LocalDateTime createTime;

@LastModifyTime  // 等价于内置@AutoInject(AuditModifyTimeAutoInjectProvider.class)
private LocalDateTime modifyTime;

@AutoInject(OtherValueAutoInjectProvider.class)    // 自定义注入值
private Object otherValue;

多租户功能

multipleTenant = true 开启多租户功能

@AutoInject(
    value = DomainIdAutoInjectProvider.class, 
    multipleTenant = true
)
private Integer domainId;
public class DomainIdAutoInjectProvider implements AutoInjectProvider {
    @Override
    public SqlCommandType[] needInject() {
        return SqlCommandType.values();
    }

    @Override
    public Object provide(Type fieldType) {
        return 1;
    }
}
// SELECT * FROM `order` WHERE `id` = 1 AND `domain_id` = 1 LIMIT 1
mapper.selectById(1);
// SELECT * FROM `order` WHERE `id` IN (1) AND `domain_id` = 1
mapper.selectByIds(List.of(1));
// UPDATE `order` SET ... WHERE `id` = 1 AND `domain_id` = 1
mapper.update(order);
// DELETE FROM `order` WHERE `id` = 1 AND `domain_id` = 1 LIMIT 1
mapper.deleteById(1);
// SELECT * FROM `order` WHERE `domain_id` = 1
mapper.selectAll();

多样查询

分页查询功能

在Mapper接口的方法参数里加入Pager,返回值类型为Page就能实现分页查询

public interface GoodsMapper extends BaseMapper<GoodsPO, Integer> {

    // @CountStatement("pagerCount")
    @Select("SELECT * FROM goods")
    Page<GoodsPO> selectGoods(Pager pager, @Param("goodsName") String goodsName);

    // long pagerCount(@Param("goodsName") String goodsName);
}
Pager pager = new Pager(0, 20); // 页码从0开始
Page<GoodsPO> page = goodsMapper.selectGoods(pager, "面包");
long recordTotal = page.getRecordTotal(); // 获取记录总数
int pageTotal = page.getPageTotal(); // 获取分页总数
page.forEach(item -> {}); // page对象实际上是List,可以迭代处理本页数据
  • 方法多参数时,Pager可以任意位置放置
  • @CountStatement注解以及查询数量的statement不是必须的,如果主查询列表的语句是简单SELECT语句,可以略写该注解采用自动生成的查询总条数的SELECT COUNT语句;如果主查询列表语句是一个JOIN或嵌套子查询,@CountStatement可以重定义一个简单的SELECT COUNT语句

示例:

简单分页查询:

-- 主列表查询
SELECT * FROM goods WHERE goods_name = #{goodsName}
-- 自动生成的查询总数语句
SELECT COUNT(*) FROM (SELECT * FROM goods WHERE goods_name = #{goodsName}) _count

复杂分页查询:

-- 主列表查询
SELECT * FROM goods g LEFT JOIN xxx x ON g.id = x.goods_id WHERE g.goods_name = #{goodsName})
-- 自动生成的查询总数语句
SELECT COUNT(*) FROM (SELECT * FROM goods g LEFT JOIN xxx x ON g.id = x.goods_id WHERE g.goods_name = #{goodsName}) _count
-- 其实在查询总数时没必要去执行LEFT JOIN 可以采用@CountStatement来重定义查总数语句
SELECT COUNT(*) FROM goods WHERE goods_name = #{goodsName}

根据方法名自动生成简单的SQL语句

支持的方法语法开头:

语法 方法开头 等价SQL
插入 insertinsertIgnorereplace INSERT INTO ... INSERT IGNORE INTO ... REPLACE INTO ...
修改 update UPDATE ...
删除 deleteByremoveBy DELETE FROM ...
查询 selectByfindByqueryBy SELECT * FROM ...
查询数量、查询存在 existsByhasBycountBy SELECT COUNT(*) FROM ...
int insertGoodsNameQuantity(Goods goods);
int updateGoodsNameQuantity(Goods goods);

查询支持的关键字:

关键字 示例 等价SQL语句
EQ selectByGoodsName(String GoodsName) 或 selectByGoodsNameEq(String GoodsName) WHERE goods_name= #{param1}
EQ_TRUE selectByEnableTrue() WHERE enable = 1
EQ_FALSE selectByEnableFalse() WHERE enable = 0
ISNULL selectByGoodsNameIsNull() WHERE goods_name IS NULL
NOTNULL selectByGoodsNameNotNull() WHERE goods_name IS NOT NULL
GT selectByQuantityGt(int quantity) WHERE quantity > #{param1}
GTE selectByQuantityGte(int quantity) WHERE quantity >= #{param1}
LT selectByQuantityLt(int quantity) WHERE quantity < #{param1}
LTE selectByQuantityLte(int quantity) WHERE quantity <= #{param1}
BETWEEN selectByQuantityBetween(Integer quantityStart, Integer quantityEnd) WHERE quantity BETWEEN #{param1} AND #{param2}
LIKE selectByGoodsNameLike(String GoodsName) WHERE goods_name LIKE CONCAT('%', #{param1}, '%')
LIKE_HEAD selectByGoodsNameLikeHead(String GoodsName) WHERE goods_name LIKE CONCAT(#{param1}, '%')
LIKE_TAIL selectByGoodsNameLikeTail(String GoodsName) WHERE goods_name LIKE CONCAT('%', #{param1})
IN selectByGoodsNameIn(String[] GoodsNames) WHERE goods_name IN(...)
NOT IN selectByGoodsNameNotIn(String[] GoodsNames) WHERE goods_name NOT IN(...)
AND selectByGoodsNameAndQuantityGt(String GoodsName, Integer quantity) WHERE goods_name= #{param1} AND quantity > #{param2}
OR selectByGoodsNameORQuantityGt(String GoodsName, Integer quantity) WHERE goods_name= #{param1} OR quantity > #{param2}
  • 可以使用@Dynamic注解,如果入参值为空则会忽略该条件,实现动态拼接SQL
  • BETWEEN 如果开始值或结束值为空则会转变为GTE或LTE
  • 该方式只支持简单条件拼接,不支持带括号的OR多条件查询
  • 方法参数的顺序必须严格按照方法名描述的顺序申明BETWEEN可以占用两个参数,内部是以mybatis的参数命名方式paramN取值的,所以不必使用@Param注解
public interface GoodsMapper extends BaseMapper<GoodsPO, Integer> {

  	@Dynamic
    Page<GoodsPO> selectByCreateTime(Pager pager, @SqlCustomized(ColumnFunction.DATE) LocalDate date);
}
  • @SqlCustomized可以申明字段使用的函数

等价于如下SQL

SELECT * FROM goods WHERE DATE(create_time) = #{param1}

需要注意的是,该种方式生成SQL的时机是在启动程序后初始化Mybatis时去解析的,本质上是修改了Mybatis默认生成的MappedStatement内的SqlSource,不会影响查询的性能问题

SqlCriteriaAssembler查询SQL装配器

以代码方式动态拼装SQL,实现SqlCriteriaAssembler接口描述查询SQL如何拼接(只能拼接单表非聚合查询语句)

@FunctionalInterface
public interface SqlCriteriaAssembler {

    SqlCriteria assemble(SqlRoot root, SqlCriteriaBuilder builder);
}

示例:

mapper.select(
    (root, builder) ->
    builder.or(
        builder.between(root.function("YEAR", root.get(Goods.Fields.createTime)), 2021, 2023),
        builder.and(
            builder.in(root.get(Goods.Fields.goodsName), "雪碧", "可乐"),
            builder.gte(root.get(Goods.Fields.quantity), 1)
        )
    ),
    Sort.by(
        Sort.desc(Goods.Fields.quantity),
        Sort.asc(Goods.Fields.goodsName)
    )
);

如上代码最终将会拼成SQL

SELECT * FROM `goods` WHERE ( 
	YEAR(`create_time`) BETWEEN ? AND ? 
	OR ( 
		`goods_name` IN (?,?) AND `quantity` >= ? 
	) 
) 
ORDER BY `quantity` DESC, `goods_name` ASC

需要注意的是,该种方式生成SQL的时机是在Mybatis执行查询时,使用插件拦截了Executor 的query方法,用新的MappedStatment替换掉本次查询引用的MappedStatment,和原生mybatis相比,每次查询都会去动态拼装一遍SQL,稍微有点影响查询性能。

数据库锁

乐观锁

提供@Version标注乐观锁版本字段(单实体类仅能设置一个),支持标注的字段类型:

  • int
  • Integer
  • long
  • Long
@Getter
@Setter
@Table("goods")
public class GoodsPO {

    @Id
    private Integer id;

    // 商品名称
    private String goodsName;

    @Version
    private int version;
}
goodsMapper
    .selectById(1)
    .ifPresent(goods -> {
        try {
            mapper.update(goods);
        } catch (OptimisticLockException e) {
            // 触发乐观锁异常
        }
    });

生成实际SQL:

UPDATE `goods` SET `goods_name` = ?, `version` = `version` + 1 WHERE `id` = ? AND `version` = ?

悲观锁

提供@Lock标注在需要开启悲观锁的查询语句上,BaseMapper也提供了相应带LockType参数的查询语句

排它锁
@Lock // 或等价于@Lock(LockType.WRITE)
List<Goods> selectByName(String name);

等价于SQL:

SELECT * FROM `goods` WHERE name = #{name} FOR UPDATE
共享锁
@Lock(LockType.READ)
@Select(SQL_BY_NAMING)
List<Goods> selectByName(String name);

等价于SQL:

SELECT * FROM `goods` WHERE name = #{name} LOCK IN SHARE MODE

自动建表 (DDL)

结合模块mybatis-extension-spring-boot-starter使用,有个开关可以开启(默认关闭):

mybatis:
  extension:
    enableDDL: true # 开启自动创建表

创建/修改表

系统启动时会自动创建实体对应的数据表,字段的申明来源于@Column的属性配置,与DDL相关的属性:

@Column(
    name = "specifications",    // 重定义字段名 很少用 DDL相关
    customizeType = "VARCHAR(100)", // 自定义类型申明
    nullable = false  // 该字段是否能null DDL相关
    length = 50, // 长度 DDL相关
    scale = 2, // 精度 DDL相关
    unsigned = true, // 是否无符号 DDL相关
    defaultValue = "0", // null时的默认值 DDL相关
    comment = "规格" // 字段注释 DDL相关
) 

如果没有定义customizeType属性值,则字段类型会自动按照实体属性类型匹配,匹配规则如下表:

属性类型 默认字段类型 说明
String 或 其它类型 VARCHAR(100) 长度按length
Integer int INT
Long long BIGINT
Boolean boolean BIT(1)
Float float FLOAT(6,2)
Double double DOUBLE(12,2) 长度按length,精度按scale
BigDecimal DECIMAL(10,2) 长度按length,精度按scale
LocalDateTime ZonedDateTime java.util.Date java.util.Calendar DATETIME
LocalDate java.sql.Date DATE
LocalTime TIME
java.sql.Timestamp TIMESTAMP
枚举类型 ENUM('value1', 'value2') 会自动识别枚举值

日志

【Mybatis DDL】 goods: ADD COLUMN `goods_name` varchar(100) NOT NULL

创建/修改索引

系统启动时会自动维护索引,索引的申明来源于@Table的属性indexes

@Table(
        value = "goods",
        indexes = {
                @Index(type = IndexType.UNIQUE, properties = {"goodsName"})
        }
)

日志

【Mybatis DDL】 goods: ADD UNIQUE `UKgoods_name`(`goods_name`) USING BTREE

About

mybatis扩展包

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages