mybatis 也想要类似 spring-data-jpa 那样只需要写接口就能查询的功能怎么办?
前言
spring-data-jpa 的只写接口便可以 CRUD 的能力真的是好爽,然而 mybatis 写 sql 的灵活又让我欲罢不能。
我总是希望二者能够调和一下,为此我曾想过在同一个项目中同时使用这两个框架,当然跑起来是可行的,但是总会担心会出现什么问题,想要彻底整合却没那个实力。。。
在我的不懈努力搜索之下,终于让我找到一个项目,它可以在 spring-data-jpa 中以 freemarker 模板的形式编写动态 sql 去执行。看起来很完美的样子,就是还需要去学习 freemarker 而已。
但是,受此启发,既然他拓展了 spring-data-jpa,我为什么不能拓展一下 mybatis 呢?于是我就编写了一个扩展包:mybatis-auto-mapper.jar
详细功能介绍请移步我的个人博客(点击这里)查看,这里就不重复写一遍了,只介绍如何使用了。
下面说一下使用(创建项目就不贴图了,直接贴关键代码了):
首先是 pom.xml
在项目中添加以下依赖即可,无需新增任何配置(需要自己下载源码安装到本地仓库。。。)
<dependency>
<groupId>com.kfyty</groupId>
<artifactId>mybatis-auto-mapper</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后是实体类
package com.kfyty.mybatis.auto.mapper.entity;
import lombok.Data;
import java.util.Date;
/**
* 功能描述: 实体类
*
* @author kfyty725@hotmail.com
* @date 2019/11/16 16:41
* @since JDK 1.8
*/
@Data
public class TestUser {
private Integer id;
private String name;
private Integer age;
private Date createTime;
private int sortIndex;
public TestUser() {
}
public TestUser(String name, Integer age) {
this.name = name;
this.age = age;
this.sortIndex = age;
}
}
Mapper 接口
package com.kfyty.mybatis.auto.mapper.mapper;
import com.github.pagehelper.Page;
import com.kfyty.mybatis.auto.mapper.BaseMapper;
import com.kfyty.mybatis.auto.mapper.annotation.AutoMapper;
import com.kfyty.mybatis.auto.mapper.entity.TestUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 功能描述: mapper 接口
*
* @author kfyty725@hotmail.com
* @date 2019/11/16 16:39:41
* @since JDK 1.8
*/
@Mapper
@AutoMapper(entity = TestUserMapper.class)
public interface TestUserMapper extends BaseMapper<Integer, TestUserMapper> {
}
需要提一下的是,里面集成了 mybatis-page-helper 分页插件(在此表示感谢),可以直接使用,当然如果想自己配置一个 Bean 的话也不会冲突。如果只想进行配置属性的话,可以添加如下代码进行配置:
@Bean("pageInterceptorProperties")
public Properties pageProperties() {
Properties properties = new Properties();
properties.setProperty("supportMethodsArguments", "true");
return properties;
}
启动类
package com.kfyty.mybatis.auto.mapper;
import com.github.pagehelper.PageInfo;
import com.kfyty.mybatis.auto.mapper.entity.TestUser;
import com.kfyty.mybatis.auto.mapper.mapper.TestUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* 功能描述: 启动类
*
* @author kfyty725@hotmail.com
* @date 2019/11/16 16:35
* @since JDK 1.8
*/
@RestController
@SpringBootApplication
public class MybatisAutoMapperDemoApplication {
@Autowired
private TestUserMapper testUserMapper;
@RequestMapping("demo/insert/{name}/{age}")
public int insert(@PathVariable("name") String name, @PathVariable("age") Integer age) {
return testUserMapper.insert(new TestUser(name, age));
}
@RequestMapping("demo/insert/all/{startIndex}/{count}")
public int insertAll(@PathVariable("startIndex") Integer startIndex, @PathVariable("count") int count) {
List<TestUser> list = new ArrayList<>();
for (int i = 0; i < count; i++, startIndex++) {
list.add(new TestUser("test-" + startIndex, startIndex));
}
return testUserMapper.insertAll(list);
}
@RequestMapping("demo/page/{pageNum}/{pageSize}")
public PageInfo page(@PathVariable("pageNum") int pageNum, @PathVariable("pageSize") int pageSize) {
return PageInfo.of(testUserMapper.pageByNameNotNull(pageNum, pageSize));
}
@RequestMapping("demo/find/age-between/{start}/{end}/{pageNum}/{pageSize}")
public List<TestUser> findByAgeBetweenOrderByCreateTimeDesc(@PathVariable("start") int start, @PathVariable("end") int end, @PathVariable("pageNum") int pageNum, @PathVariable("pageSize") int pageSize) {
return testUserMapper.findByAgeBetweenOrderByCreateTimeDesc(start, end, pageNum, pageSize);
}
@Bean("pageInterceptorProperties")
public Properties pageProperties() {
Properties properties = new Properties();
properties.setProperty("supportMethodsArguments", "true");
return properties;
}
public static void main(String[] args) {
SpringApplication.run(MybatisAutoMapperDemoApplication.class, args);
}
}
由于这里只涉及到了单表操作,所以不需要 Mapper.xml 文件
下面就可以启动测试了:
-- 测试之前
mysql> select * from test_user;
Empty set (0.00 sec)
-- 测试接口: http://localhost:8080/demo/insert/1/1
mysql> select * from test_user;
+----+------+------+---------------------+------------+
| id | name | age | create_time | sort_index |
+----+------+------+---------------------+------------+
| 15 | 1 | 1 | 2019-12-08 14:38:01 | 1 |
+----+------+------+---------------------+------------+
1 row in set (0.00 sec)
-- 测试接口: http://localhost:8080/demo/insert/all/2/10
mysql> select * from test_user;
+----+---------+------+---------------------+------------+
| id | name | age | create_time | sort_index |
+----+---------+------+---------------------+------------+
| 15 | 1 | 1 | 2019-12-08 14:38:01 | 1 |
| 16 | test-2 | 2 | 2019-12-08 14:39:08 | 2 |
| 17 | test-3 | 3 | 2019-12-08 14:39:08 | 3 |
| 18 | test-4 | 4 | 2019-12-08 14:39:08 | 4 |
| 19 | test-5 | 5 | 2019-12-08 14:39:08 | 5 |
| 20 | test-6 | 6 | 2019-12-08 14:39:08 | 6 |
| 21 | test-7 | 7 | 2019-12-08 14:39:08 | 7 |
| 22 | test-8 | 8 | 2019-12-08 14:39:08 | 8 |
| 23 | test-9 | 9 | 2019-12-08 14:39:08 | 9 |
| 24 | test-10 | 10 | 2019-12-08 14:39:08 | 10 |
| 25 | test-11 | 11 | 2019-12-08 14:39:08 | 11 |
+----+---------+------+---------------------+------------+
11 rows in set (0.00 sec)
下面测试分页接口:
--接口: http://localhost:8080/demo/find/name-not-null/1/3
-- 日志如下:
2019-12-08 14:42:01.418 DEBUG 6524 --- [nio-8080-exec-2] c.k.m.j.s.m.T.findByNameNotNull_COUNT : ==> Preparing: SELECT count(0) FROM test_user WHERE (name IS NOT NULL)
2019-12-08 14:42:01.418 DEBUG 6524 --- [nio-8080-exec-2] c.k.m.j.s.m.T.findByNameNotNull_COUNT : ==> Parameters:
2019-12-08 14:42:01.419 DEBUG 6524 --- [nio-8080-exec-2] c.k.m.j.s.m.T.findByNameNotNull_COUNT : <== Total: 1
2019-12-08 14:42:01.421 DEBUG 6524 --- [nio-8080-exec-2] c.k.m.j.s.m.T.findByNameNotNull : ==> Preparing: select * from test_user where ( name is not null ) LIMIT ?
2019-12-08 14:42:01.421 DEBUG 6524 --- [nio-8080-exec-2] c.k.m.j.s.m.T.findByNameNotNull : ==> Parameters: 3(Integer)
2019-12-08 14:42:01.426 DEBUG 6524 --- [nio-8080-exec-2] c.k.m.j.s.m.T.findByNameNotNull : <== Total: 3
--接口: http://localhost:8080/demo/find/age-between/5/10/1/10
-- 日志如下:
2019-12-08 15:07:41.874 DEBUG 5104 --- [nio-8080-exec-3] dByAgeBetweenOrderByCreateTimeDesc_COUNT : ==> Preparing: SELECT count(0) FROM test_user WHERE age BETWEEN ? AND ?
2019-12-08 15:07:41.874 DEBUG 5104 --- [nio-8080-exec-3] dByAgeBetweenOrderByCreateTimeDesc_COUNT : ==> Parameters: 5(Integer), 10(Integer)
2019-12-08 15:07:41.876 DEBUG 5104 --- [nio-8080-exec-3] dByAgeBetweenOrderByCreateTimeDesc_COUNT : <== Total: 1
2019-12-08 15:07:41.876 DEBUG 5104 --- [nio-8080-exec-3] .T.findByAgeBetweenOrderByCreateTimeDesc : ==> Preparing: select * from test_user where age between ? and ? order by create_time desc LIMIT ?
2019-12-08 15:07:41.877 DEBUG 5104 --- [nio-8080-exec-3] .T.findByAgeBetweenOrderByCreateTimeDesc : ==> Parameters: 5(Integer), 10(Integer), 10(Integer)
2019-12-08 15:07:41.882 DEBUG 5104 --- [nio-8080-exec-3] .T.findByAgeBetweenOrderByCreateTimeDesc : <== Total: 6
这里介绍一下预设接口 BaseMapper<PrimaryKey, T>,该接口提供了一些基本的方法,但是如果继承它的话,需要在子接口上使用 @AutoMapper 注解并指定实体类型,之所以需要这个配置,是因为这个包不是在运行时起作用,而是在启动时起作用,因此需要提前配置实体类型,其代码如下:
package com.kfyty.mybatis.auto.mapper;
import com.kfyty.mybatis.auto.mapper.annotation.AutoMapper;
import com.kfyty.mybatis.auto.mapper.struct.TableFieldStruct;
import com.kfyty.mybatis.auto.mapper.struct.TableStruct;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
public interface BaseMapper<PrimaryKey, T> {
/**
* 插入单条数据
* @param entity 实体数据
* @return 受影响的行数
*/
@AutoMapper
int insert(@Param("entity") T entity);
/**
* 批量插入数据
* @param entities 实体数据
* @return 受影响的行数
*/
@AutoMapper
int insertAll(@Param("entities") List<T> entities);
/**
* 更新单条数据,只更新值不为空的字段
* @param entity 实体数据
* @return 受影响的行数
*/
@AutoMapper
int update(@Param("entity") T entity);
/**
* 更新单条数据,允许更新值为空的字段
* @param entity 实体数据
* @return 受影响的行数
*/
@AutoMapper(allowNull = true)
int updateDeep(@Param("entity") T entity);
/**
* 批量更新数据,只更新值不为空的字段
* @param entities 实体数据
* @return 受影响的行数
*/
@AutoMapper
int updateAll(@Param("entities") List<T> entities);
/**
* 批量更新数据,允许更新值为空的字段
* @param entities 实体数据
* @return 受影响的行数
*/
@AutoMapper(allowNull = true)
int updateAllDeep(@Param("entities") List<T> entities);
/**
* 查询所有的数据
* @return 所有数据
*/
@AutoMapper
List<T> findAll();
/**
* 查询记录总条数
* @return 记录总条数
*/
@AutoMapper
int countAll();
/**
* 分页查询数据
* @param pageNum 起始页码
* @param pageSize 每页大小
* @return 分页数据
*/
@AutoMapper
List<T> pageAll(@Param("pageNum") int pageNum, @Param("pageSize") int pageSize);
/**
* 删除所有数据
* @return 受影响的行数
*/
@AutoMapper
int deleteAll();
/**
* 根据主键查询数据,支持复合主键
* @param pk 主键数据
* @return 实体
*/
@AutoMapper
T findByPk(@Param("pk") PrimaryKey ... pk);
/**
* 根据主键查询数据,支持复合主键
* @param pk 主键数据
* @return 直接以 Map 形式返回
*/
@AutoMapper(parseColumn = false)
Map<String, Object> findMapByPk(@Param("pk") PrimaryKey ... pk);
/**
* 根据主键删除数据,支持复合主键
* @param pk 主键数据
* @return 受影响的行数
*/
@AutoMapper
int deleteByPk(@Param("pk") PrimaryKey ... pk);
/**
* 查询当前表结构
* @param database 表所在的数据库名称
* @return 表结构
*/
@AutoMapper
TableStruct findTableStruct(@Param("database") String database);
/**
* 查询当前表的字段结构
* @param database 表所在的数据库名称
* @return 表字段结构
*/
@AutoMapper
List<TableFieldStruct> findTableFieldStruct(@Param("database") String database);
}
最后,插入单个数据时,如果需要返回主键值的话,可以使用 @SelectKey 注解,SelectKey.java 文件如下:
package com.kfyty.mybatis.auto.mapper.annotation;
import com.kfyty.mybatis.auto.mapper.enums.SelectKeyOrder;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 功能描述: 用于 mapper 接口或方法,插入数据时生成 <selectKey/> 标签
* 方法注解优先级高于类注解
* insertAll 方法无效
*
* @author kfyty725@hotmail.com
* @date 2019/12/20 19:37
* @since JDK 1.8
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SelectKey {
/**
* 查询主键值 SQL 语句
* @return 默认值为 MySQL 查询自增主键
*/
String value() default "select last_insert_id()";
/**
* <selectKey/> 标签执行顺序
* @return 默认值为 SelectKeyOrder.AFTER
*/
SelectKeyOrder order() default SelectKeyOrder.AFTER;
}
最后放上 AutoMapper.java 文件,大致可以了解支持哪些功能:
package com.kfyty.mybatis.auto.mapper.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 功能描述: 用于 mapper 接口或方法,即可自动映射方法
*
* @author kfyty725@hotmail.com
* @date 2019/11/6 13:37
* @since JDK 1.8
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AutoMapper {
/**
* 更新时指定主键属性,可用于接口配置
* @return 默认值为 id
*/
String[] primaryKey() default "";
/**
* 实体类后缀,可用于接口配置
* @return 默认值为 Pojo
*/
String suffix() default "";
/**
* 实体类/Mapper 接口命名不规范时需指定表名,可用于接口配置
* @return 默认值为 ""
*/
String table() default "";
/**
* 继承自 BaseMapper 的方法需在子接口上声明实体类型
* @return 默认值为 Object.class
*/
Class<?> entity() default Object.class;
/**
* 查询时添加额外的条件,可用于接口配置
* @return 默认值为 ""
*/
String where() default "";
/**
* where 条件分隔符,可用于接口配置
* @return 默认值为 "and"
*/
String separator() default "and";
/**
* 指定需要查询的列,仅用于方法配置
* @return 默认值为 "*"
*/
String columns() default "*";
/**
* 符合 find*By** 风格命名时,是否从方法解析需查询的列,仅用于方法配置
* @return 默认值为 true
*/
boolean parseColumn() default true;
/**
* 插入/更新对象时,遇到 null 是否转换为插入数据库默认值,仅用于方法配置
* @return 默认值为 false
*/
boolean useDefault() default false;
/**
* 更新对象时,是否允许更新为 null 值,仅用于方法配置
* @return 默认值为 false
*/
boolean allowNull() default false;
/**
* 是否继承类注解 where 配置,仅用于方法配置
* @return 默认值为 true
*/
boolean extend() default true;
}
看起来是不是挺像那么回事的呢?
最后的最后,如果需要查看生成的 mapper 标签的话,在 application.yml 中添加如下配置即可:
logging:
level:
com.kfyty.mybatis.auto.mapper.handle: debug
感兴趣的可以查看 github:https://github.com/kfyty/mybatis-auto-mapper