MyBatis 及 MyBatis Plus 纯注解方式配置(Spring Boot + Postgresql)
说明
当前的版本为
- MyBatis 3.5.9
- MyBatis Plus 3.5.1
- Spring Boot 2.6.4
- Postgresql 42.3.3
与 Spring Boot 结合使用 MyBatis
以下说明Spring Boot下完全以注解方式进行的配置, 覆盖大部分功能场景
项目依赖
需要以下的依赖, 版本由Spring Boot指定, 或者参考上面的版本号
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency>
配置
项目的配置文件中, 需要配置 datasource
spring: #... datasource: url: jdbc:postgresql://123.123.123.123:5432/mydb?stringtype=unspecified username: 123123 password: 123123 driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 200 minimum-idle: 20 max-lifetime: 20000 connection-test-query: select 1 leak-detection-threshold: 30000 allow-pool-suspension: true
其中
- stringtype=unspecified 必须, 否则在insert和update jsonb类型字段时, 会报类型不匹配
- leak-detection-threshold 这个用于记录执行时间超长的SQL, 会抛出一个异常
代码
1.增加 MapperScan 注解
这个注解可以和@SpringBootApplication放在一起, 也可以放在单独的一个 @Configuration
@SpringBootApplication @MapperScan(basePackages = "com.yourdomain.demo.commons.impl.mapper") public class CommonsBoot { public static void main(String[] args) { SpringApplication.run(CommonsBoot.class, args); } }
2. 准备POJO(PO或者DTO)
public class RoleItemDTO { private Integer id; private int roleId; private int itemId; private Date createTime; // getters and setters // ... }
3. 准备Mapper
Mapper必须位于 @MapperScan 指定的包路径, 其中 @Repository 用于指定 Bean 名称, 也可以使用 @Mapper 注解. 内部用 @Select @Update @Delete @Insert 等编写对应操作的SQL
@Repository("roleItemMapper") public interface RoleItemMapper { @Select("SELECT * FROM test_role_item WHERE role_id = #{roleId}") List<RoleItemDTO> listByRoleId(@Param("roleId") int roleId); }
4. 调用
测试用例
@ExtendWith(SpringExtension.class) @SpringBootTest @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class RoleItemTest { private static final Logger log = LoggerFactory.getLogger(RoleItemTest.class); @Resource private RoleItemMapper roleItemMapper; @Test @Order(1) void insert() { RoleItemDTO rc = new RoleItemDTO(); rc.setItemId(1); rc.setRoleId(2); rc.setCreateTime(new Date()); Assertions.assertThat(roleItemMapper.insert(rc)).isEqualTo(1); List<RoleItemDTO> dummies = roleItemMapper.listByRoleId(2); log.info(JacksonUtil.compress(dummies)); Assertions.assertThat(dummies).isNotNull(); } }
注解使用
典型用例
public interface VillageMapper { @Results({ @Result(property = "vid", column = "id"), @Result(property = "villageName", column = "name"), @Result(property = "district", column = "district") }) @Select("SELECT id, name, district from village WHERE id = #{id}") Village selectVillage(int id); @Insert("INSERT into village(name,district) VALUES(#{villageName}, #{district})") void insertVillage(Village village); @Update("UPDATE village SET name=#{villageName}, district =#{district} WHERE id =#{vid}") void updateVillage(Village village); @Delete("DELETE FROM village WHERE id =#{id}") void deleteVillage(int id); }
传参注解@Param
和${}
,#{}
在Mapper接口中, 使用@Param("paramName")
可以对传入的参数进行命名, 在SQL中通过${paramName}
和#{paramName}
取值, 避免歧义.
${}
会在SQL字符串中直接替换, 非必要不建议使用这种方式#{}
是根据参数类型进行填充, 对于字符串会增加引号包围, 是通常使用的传参方式#{}
支持对象传入, 在引用时通过#{obj.variable}
的方式取值, 参考下面 Insert 的例子
Select和结果集映射
对于普通类型例如primitive类型, 数值(Integer, Long, Double, BigDecimal等), 字符串String, 日期Date, MyBatis都已经做了自动转换不需要手工设定. 对于特殊类型, 例如对应Postgresql的 Array, Jsonb, 需要使用注解 @Results 进行指定, 在注解中, 对于已经有默认handler的字段可以省略, 只需要添加特殊指定hanler的字段.
Array类型字段的例子, 对templates字段的转换进行指定
// POJO定义 public class ViewPO implements Serializable { private Integer id; private String name; private Integer[] templates; } // Mapper方法 @Results({ @Result(property = "templates", column = "template_ids", typeHandler = ArrayTypeHandler.class) }) @Select("select * from test_view where id = #{id}") ViewPO select(@Param("id") int id);
JSONB类型字段的例子, 对ext字段的转换进行指定
// POJO定义 public class AccountDTO { private Long id; private String name; private int val; private BigDecimal amount; private Ext ext; public static class Ext { private Integer id; private String name; private BigDecimal level; // getters and setters } // getters and setters } // Mapper方法 @Results({ @Result(property = "ext", column = "ext", typeHandler = JacksonTypeHandler.class) }) @Select("select * from test_account where amount > #{amountGt}") List<AccountDTO> list(@Param("amountGt") BigDecimal amountGt);
复用结果集映射
通过@ResultMap
复用@Results
@Select({"select id,user_name from u_user where id = #{id}"}) @Results(id="userMap", value={ @Result(column="id", property="id", id=true), @Result(column="user_name", property="userName")}) User select(Long id); @Select({"select * from u_user"}) @ResultMap("userMap") List<User> selectUsers();
分页和排序
MyBatis 并没有提供内建的分页和排序支持, 可以通过自定义一个Pager实现, 最底下 getSql()
方法生成的SQL格式是给PostgreSQL使用的, 如果用 MySQL 需要自己修改一下.
public class Pager implements Serializable { public static final Pager DEFAULT = new Pager(); private static final int UNLIMITED = 0; private int offset = 0; private int limit = UNLIMITED; private final List<Sort> sorts = new ArrayList<>(); public Pager() {} public Pager(int limit) { this.limit = limit; } public Pager(int offset, int limit) { this.offset = offset; this.limit = limit; } public Pager orderByAsc(String field) { sorts.add(new Sort(field, Order.ASC)); return this; } public Pager orderByDesc(String field) { sorts.add(new Sort(field, Order.DESC)); return this; } public enum Order { /** */ ASC, DESC } public static class Sort { public String field; public Order order; public Sort(String field, Order order) { this.field = field; this.order = order; } } public String getSql() { StringBuilder sb = new StringBuilder(); if (sorts.size() > 0) { for (Sort sort : sorts) { sb.append((sb.length() == 0)? "ORDER BY " : ", ") .append(sort.field).append(' ').append(sort.order); } } if (limit > UNLIMITED) { sb.append(" LIMIT ").append(limit).append(" OFFSET ").append(offset); } return sb.toString(); } }
在使用时, 注解中使用 ${}
引用, 注意:如果使用了<if>
判断, 前后要加上<script>
String PARAMS = "<if test='param != null'>" + "<if test='param.itemType != null'>AND item_type = #{param.itemType}</if>" + "<if test='param.itemName != null'>AND item_name = #{param.itemName}</if>" + "</if>"; @Select("<script>SELECT item_type, item_name, create_time" + " FROM ${tableName} <where>" + PARAMS + "</where>" + " <if test='pager != null'>${pager.sql}</if>" + "</script>") List<Item> list( @Param("tableName") String tableName, @Param("param") Map<String, Object> param, @Param("pager") Pager pager);
调用时, 如果不排序分页就直接给 null, 如果排序分页, 就带上参数.
Pager pager = new Pager(10, 20).orderByAsc("item_name").orderByAsc("item_type"); list = itemMapper.list(TBL_NAME, map, pager);
Insert和自增主键
@Insert("INSERT INTO role_item (role_id, item_id, create_time) VALUES (#{po.roleId}, #{po.itemId}, #{po.createTime})") @Options(useGeneratedKeys=true, keyProperty="id") int insert(@Param("po") RoleItemPO po);
Delete
@Delete("DELETE FROM role_item WHERE item_id = #{itemId}") int deleteByItemId(@Param("itemId") int itemId);
Update
@Update("update widget set name=#{po.name}, manufacturer=#{po.manufacturer} where id=#{id}") public void updateById(@Param("id") int id, @Param("po") Widget po);
在注解内使用 foreach, if test 等标签
如果使用复杂标签, 需要加上 <script>
包围
@Mapper public interface LizzMapper { @Select({"<script> select id,name " + " from t_lizz" + " where id in" + " <foreach collection=\"ids\" index=\"index\" item=\"item\" open=\"(\" separator=\",\" close=\")\">" + " #{item}" + " </foreach>" + "</script>"}) List<Lizz> selectByIds(List<Integer> ids); @Insert({"<script> insert into t_lizz(id,name)" + " values" + " <foreach collection=\"datas\" item=\"item\" separator=\",\" >(#{item.id},#{item.name})</foreach>" + "</script>"}) int batchInsert(List<Lizz> datas); @Select({"<script> select id,name " + " from t_lizz" + " where id in" + " <foreach collection=\"ids.split(',')\" index=\"index\" item=\"item\" open=\"(\" separator=\",\" close=\")\">#{item}</foreach>" + "<if test='state != null'>and state= #{state}</if> " + "</script>"}) List<AlertRule> listByIdsAndState(String ids, Integer state); }
与 Spring Boot 结合使用 MyBatis Plus
MyBatis Plus 相对于原生 MyBatis, 增加了对常用CRUD方法的包装, 减小了手工编写SQL的工作量. MyBatis Plus 对多主键的表支持不太好, 如果数据库中的某个数据表使用了联合主键, 建议使用原生的 MyBatis 注解对这个数据表进行操作
项目依赖
将 MyBatis 的依赖替换为MyBatis Plus, 参考上面的版本号, 本文使用的是3.5.1
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency>
配置
1.增加 MyBatisPlus 的配置类
因为除了设置 MapperScan, 还要设置分页插件, 因此放到了单独的 @Configuration
@Configuration @MapperScan(basePackages = "com.yourdomain.demo.common.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL)); return interceptor; } }
相应的, 在pom.xml中需要增加
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.3</version> <exclusions> <exclusion> <artifactId>jsqlparser</artifactId> <groupId>com.github.jsqlparser</groupId> </exclusion> </exclusions> </dependency>
因为较低版本的jsqlparser在启动时会出现下面的错误
An attempt was made to call a method that does not exist. The attempt was made from the following location: com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor.<clinit>(PaginationInnerInterceptor.java:70) The following method did not exist: net.sf.jsqlparser.schema.Column.withColumnName(Ljava/lang/String;)Lnet/sf/jsqlparser/schema/Column; The method's class, net.sf.jsqlparser.schema.Column, is available from the following locations: jar:file:/C:/Users/laimin/.m2/repository/com/github/jsqlparser/jsqlparser/2.0/jsqlparser-2.0.jar!/net/sf/jsqlparser/schema/Column.class
2. 准备POJO(PO或者DTO)
如果使用 MyBatis Plus, 常用的三个注解为
- @TableName 表名, autoResultMap设置为true, 下面 ext 字段的 typeHandler 才会生效
- @TableId 唯一主键, 这个注解在当前类中只允许出现一次. 唯一主键修饰的id字段需要使用包装类, 例如 Integer或Long, 在Insert时这个字段赋值NULL, 才能正确获取生成的自增ID值
- @TableField 字段映射关系, 当字段名与变量名不能直接对应时使用, 当类型转换需要特殊处理时也需要使用这个注解
@TableName(value = "test_users", autoResultMap = true) public class UserPO implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Long id; private String userName; private String password; @TableField("creat_time") private Date createdAt; @TableField("update_time") private Date updatedAt; @TableField(typeHandler = JacksonTypeHandler.class) private JsonNode ext; @TableField(exist = false) private String fieldNotColumn; }
3. 准备 Mapper
对于 MyBatis Plus, 最常见的 Mapper 初始化方式为扩展 BaseMapper, 这个接口已经实现了 selectById, selectList, insert, delete, deleteById, updateById 等常用方法
@Repository("userMapper") public interface UserMapper extends BaseMapper<UserPO> { }
注意
- 如果 BaseMapper 自带方法不能满足功能需求, 可以通过 @Select, @Update, @Insert, @Delete 等注解添加自定义接口, 实际上就是按原生 MyBatis 的方式处理.
- 如果添加了 @Select 查询并且结果也是标准POJO, 结果中的 JSONB 类型字段并不会自动赋值(输出为null). 如果需要转换, 需要按原生 MyBatis 一样在方法上添加 @Results 注解.
4. 调用
调用方式与原生 MyBatis 一致
注解使用
MyBatis Plus 的常用注解主要有两个, 一个是 Wrapper(有多个实现类), 一个是 Page, 分别用于查询条件和分页条件.
QueryWrapper 用于条件,排序和聚合
QueryWrapper 自带了常用的逻辑方法 eq(相等), ne(不相等), lt/gt(小于/大于), lte/gte(小于等于/大于等于), like(两边%), likeLeft(左%), likeRiht(右%), exists(条件子查询), in(数组中包含), 以及 groupBy, orderBy 方法
- 三参数格式中, 第一个参数为本条件是否加入查询的条件判断, 便于根据输入进行条件组合
- 三参数的第二参数(或二参数的第一参数), 其值为数据库的字段名, 这点与 LambdaQueryWrapper 不同, 需要注意
List<FieldPO> fields = fieldMapper.selectList(new QueryWrapper<FieldPO>().eq("t_id", tId).orderByAsc("id"));
一个复杂一点的例子, 注意最后一个exists例子中的传参方式
QueryWrapper<ItemPO> wrapper = new QueryWrapper<ItemPO>() .eq(name != null, "item_name", name) .like(nameLike != null, "item_name", nameLike) .like(descLike != null, "desc", descLike) .eq(type > 0, "item_type", type) .eq(state > 0, "item_state", state) .eq(creatorId > 0, "creator_id", creatorId) .in(idIn != null && idIn.size() > 0, "id", idIn) .exists(roleId > 0, "select 1 from role_item where role_item.item_id=item.id and role_item.role_id = {0}", roleId);
LambdaQueryWrapper POJO字段方式
LambdaQueryWrapper 与 QueryWrapper 的区别在与条件参数为POJO的取值方法名而不是数据库字段, 从设计上看这样可以完全隔离数据库层的信息, 是更好的一种实现, 例子
LambdaQueryWrapper<AccountDTO> userLambdaQueryWrapper = new LambdaQueryWrapper<>(); userLambdaQueryWrapper.likeRight(AccountDTO::getName , "acc").lt(AccountDTO::getVal , 40).last("limit 5");
但是当前 MyBatis Plus 的实现在高版本JDK上会提示警告, 在 JDK9+ 上运行时会提示An illegal reflective access operation has occurred
#issue. 建议还是继续用 QueryWrapper, 待这部分的实现修复后再迁移.
Page 分页
列表的排序和聚合都是通过Wrapper指定的, Page 用于列表中的分页. 前提: 在 Configuration 中, 通过 MybatisPlusInterceptor 添加 PaginationInnerInterceptor, 参考前面的配置部分, 需要配置正确的 DbType.
通过页数和每页记录数创建Page对象
Page page = new Page<>(vo.getPageNum(), vo.getPageSize());
结合Wrapper进行查询
@Override public List<ItemPO> listByArgs( Page<ItemPO> page, String name, int type) { QueryWrapper<ItemPO> wrapper = new QueryWrapper<ItemPO>() .eq(name != null, "item_name", name) .eq(type > 0, "item_type", type); return itemMapper.selectPage(page, wrapper).getRecords(); }
如果需要总数, 可以直接返回selectPage(page, wrapper)
的结果.
混合使用 MyBatis 原生方式和 MyBatis Plus
因为 MyBatis Plus 是对 MyBatis 功能的包装, 底层依然是 MyBatis, 所以在使用 MyBatis Plus 的过程中, MyBatis 的原生方法依然可用.
例如, 可以在原生方法中, 使用 MyBatis Plus 的 Wrapper
@Repository("userMapper") public interface UserMapper extends BaseMapper<UserPO> { @Select("select * from u_users ${ew.customSqlSegment}") List<UserPO> selectAll(@Param(Constants.WRAPPER) Wrapper<UserPO> wrapper); }
其它
批量执行
批量执行可以通过两种方式,
MyBatis 原生环境
在 MyBatis 原生环境下, 可以通过获取 sqlSession 后手工发起
@Resource(name = "sqlSessionFactory") private SqlSessionFactory sqlSessionFactory; public void addItems(List<ItemPO> items) { SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false); try { for (ItemPO item : items) { itemMapper.insert(item); } session.commit(); session.clearCache(); } catch (Exception e) { log.error("Exception in batch update", e); session.rollback(); } finally { session.close(); } }
MyBatis Plus 的 saveBatch
在 MyBatis Plus 中, 可以通过扩展 ServiceImpl<Mapper, POJO> 类, 直接使用其中的 saveBatch() 方法
public class FieldServiceImpl extends ServiceImpl<FieldMapper, FieldPO> implements FieldService { @Override public Result saveSomething(List<FieldPO> pos) { // 在方法中使用 saveBatch saveBatch(pos); } }
动态表名
如果需要在查询中指定表名, 最简单的方式是通过${}
方式传参, 例子如下
@Select("SELECT * FROM ${tableName} WHERE item_type=#{itemType} " + "AND item_name=#{itemName} AND item_id=#{itemId} AND label=#{label}") LabelMapDTO selectFromTable( @Param("tableName") String tableName, @Param("itemType") int itemType, @Param("itemName") String itemName, @Param("itemId") String itemId, @Param("label") String label); @Select("SELECT * FROM ${tableName} WHERE item_type=#{itemType} " + "AND item_name=#{itemName} AND item_id=#{itemId}") List<LabelMapDTO> selectListFromTable( @Param("tableName") String tableName, @Param("itemType") int itemType, @Param("itemName") String itemName, @Param("itemId") String itemId);
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
2016-04-04 JVM监测&工具[转]
2016-04-04 Java运行环境JVM GC和线程堆栈查询
2015-04-04 zookeeper单节点和多节点配置
2015-04-04 设置root用户不保存终端历史记录到.bash_history
2015-04-04 Jenkins 安装
2015-04-04 本地 Maven项目部署到Nexus Repository
2015-04-04 Sonatype Nexus Maven仓库搭建和管理