• 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>
    

    配置

    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}取值, 避免歧义.

    1. ${}会在SQL字符串中直接替换, 非必要不建议使用这种方式
    2. #{}是根据参数类型进行填充, 对于字符串会增加引号包围, 是通常使用的传参方式
    3. #{}支持对象传入, 在引用时通过 #{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);
    

    分页和排序

    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;
        }
    
    }
    

    2. 准备POJO(PO或者DTO)

    如果使用 MyBatis Plus, 常用的三个注解为

    1. @TableName 表名, autoResultMap设置为true, 下面 ext 字段的 typeHandler 才会生效
    2. @TableId 唯一主键, 这个注解在当前类中只允许出现一次. 唯一主键修饰的id字段需要使用包装类, 例如 Integer或Long, 在Insert时这个字段赋值NULL, 才能正确获取生成的自增ID值
    3. @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;
    }
    

    3. 准备 Mapper

    对于 MyBatis Plus, 最常见的 Mapper 初始化方式为扩展 BaseMapper, 这个接口已经实现了 selectById, selectList, insert, delete, deleteById, updateById 等常用方法

    @Repository("userMapper")
    public interface UserMapper extends BaseMapper<UserPO> {
    
    }
    

    注意

    1. 如果 BaseMapper 自带方法不能满足功能需求, 可以通过 @Select, @Update, @Insert, @Delete 等注解添加自定义接口, 实际上就是按原生 MyBatis 的方式处理.
    2. 如果添加了 @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 方法

    1. 三参数格式中, 第一个参数为本条件是否加入查询的条件判断, 便于根据输入进行条件组合
    2. 三参数的第二参数(或二参数的第一参数), 其值为数据库的字段名, 这点与 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);
    
  • 相关阅读:
    整数数组中最大子数组的和的问题(续)
    整数数组中最大子数组的和的问题
    PHP开发环境(Apache+mysql+PHPstorm+php)的搭建
    echart 库 初始
    2.15 学习总结 之 天气预报APP volley(HTTP库)之StringRequest
    2.14 学习总结 之 序列化
    2.13 阶段实战 使用layui重构选课系统
    2.12 学习总结 之 表单校验插件validate
    jsp连接数据库的乱码问题 servlet请求参数编码处理get post
    Rocket
  • 原文地址:https://www.cnblogs.com/milton/p/16100143.html
Copyright © 2020-2023  润新知