自搭框架之MP(MybatisPlus)

1,886 阅读8分钟

最近我在项目中用到了MybatisPlus,自己用它搭了下框架,发现确实挺好用的,写了这篇总结,希望与大家一起进步学习。(本文中如果有错误的地方,欢迎大家指正,记得轻喷,不要打脸就行) 自搭框架采用springBoot+mybatisPlus+mysql

一、环境搭建

  1. 在pom文件中加入mybatisPlus和lombok依赖(自搭框架,省事,搭着玩玩)
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.2</version>
</dependency>
<!--lombok插件-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
  1. application.yml加入mysql数据库连配置
###服务器基本设置
server:
  ###端口号
  port: 8081
  ###项目名
  servlet:
    context-path: /
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/yzwtest?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
  1. 分别创建controller、service、mapper文件夹存放控制层、业务层、数据层代码。
  • (1)首先是数据层代码 User类。其中comm_user是我所创建的User表
@TableName(value="comm_user")
@Data
public class User extends BaseEntity {

    @TableField(value = "name")
    private String name;
    @TableField(value = "password")
    private String password;
}
  • (2)service层,分为UserService和UserServiceImpl.其中IService接口和ServiceImpl是mybatisPlus提供的基类,继承该接口,可以使用mybatisPlus所封装的CRUD方法.具体用法可查看mybatisPlus官网

UserService

public interface UserService extends IService<User> {
}

UserServiceImpl

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
  • (3)controller层。创建UserController,进行CRUD测试。mybatisPlus封装的基本方法基本足够我们平时的开发使用,还有lambda表达式的用法(具体可前往官网查看。)。如果不满足我们的需求,我们可以用自定义XMl或者sql注入器(后续也会介绍如何使用)。

二、逻辑删除字段配置

  1. 在application.yml文件中加入逻辑删除字段配置。其中isValid为所有表字段的逻辑删除字段
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isValid  #全局逻辑删除字段值 3.3.0开始支持,详情看下面。
      logic-delete-value: 0 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 1 # 逻辑未删除值(默认为 0)
  1. 在实体类字段上加入@TableLogic注解
    @TableLogic
    private String isValid;

三、利用自动填充功能,回填相关字段

相信大家在设计表的时候,都会有创建时间(人)、更新时间(人)、逻辑删除的字段。但是如果每次做新增和更新的操作自动回填这些字段,可以减少代码量

  • (1)创建BaseEntity,User类继承BaseEntity(备注:所有表的公共字段提取出来,可以减少代码量)。在实体类字段上添加 @TableField并填写fill注解
描述
DEFAULT 默认不处理
INSERT 插入时填充字段
UPDATE 更新时填充字段
INSERT_UPDATE 插入和更新时填充字段
@Getter
@Setter
public abstract class BaseEntity implements Serializable {
    @TableId(value="id",type = IdType.AUTO)
    private Long id;
    @TableField(value = "user_created",fill = FieldFill.INSERT)
    private Long userCreated;
    @TableField(value = "org_created",fill = FieldFill.INSERT)
    private Long orgCreated;
    @TableField(value = "time_created",fill = FieldFill.INSERT)
    private Timestamp timeCreated;
    @TableField(value = "user_updated",fill = FieldFill.UPDATE)
    private Long userUpdated;
    @TableField(value = "org_updated",fill = FieldFill.UPDATE)
    private Long orgUpdated;
    @TableField(value = "time_updated",fill = FieldFill.UPDATE)
    private Timestamp timeUpdated;
    @TableField(value = "is_valid",fill = FieldFill.INSERT_UPDATE,select = false)
    @TableLogic
    private String isValid;
}
  • (2)自定义AutoFillObjectHandler自动填充处理器类,并实现MetaObjectHandler
@Component
public class AutoFillObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        //判断自动填充的字段,减少性能消耗
        if (metaObject.hasGetter("userCreated") && metaObject.hasGetter("orgCreated")
                && metaObject.hasGetter("timeCreated") && metaObject.hasGetter("isValid")) {
            setFieldValByName("userCreated", Long.parseLong("1"), metaObject);
            setFieldValByName("orgCreated", Long.parseLong("1"), metaObject);
            setFieldValByName("timeCreated", new Timestamp(System.currentTimeMillis()), metaObject);
            setFieldValByName("isValid", "1", metaObject);
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        setFieldValByName("userUpdated", 2L, metaObject);
        setFieldValByName("test", 2L, metaObject);
        setFieldValByName("orgUpdated", 2L, metaObject);
        setFieldValByName("timeUpdated", new Timestamp(System.currentTimeMillis()), metaObject);
    }
}

四、字段类型处理器(以下二选一,也可以都配置)

  • JacksonTypeHandler类型处理。

在pom文件中加入JacksonTypeHandler依赖包

<!--Jackson依赖-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

在实体类字段上添加typeHandler注解

    @TableField(typeHandler = JacksonTypeHandler.class)
    private Book book;
  • FastjsonTypeHandler类型处理 在pom文件中加入JacksonTypeHandler依赖包
<!--fastjson依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>

在实体类字段上添加typeHandler注解

    @TableField(typeHandler = FastjsonTypeHandler.class)
    private Book book;

五、sql注入器

MybatisPlus封装了CRUD的许多方法,若不符合自己的需求,可以通过sql注入器,添加自己所需的自定义方法。接下来上代码,自定义方法为findOne

  • (1)创建method文件夹,自定义方法FindOne,继承AbstractMethod
public class FindOne extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        /* 执行 SQL ,动态 SQL 参考类 SqlMethod */
        String sql = "select * from " + tableInfo.getTableName()
                + " where " + tableInfo.getKeyColumn() + "=#{" + tableInfo.getKeyProperty() + "}";
        /* mapper 接口方法名一致 */
        String method = "findOne";
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return addSelectMappedStatementForTable(mapperClass, method, sqlSource, tableInfo);
    }
}
  • (2)创建自定义sql注入器,继承DefaultSqlInjector
public class MySqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //增加自定义方法
        methodList.add(new FindOne());
        return methodList;
    }
}
  • (3)创建MyBaseMapper,继承BaseMapper;UserMapper继承MyBaseMapper

UserMapper

@Mapper
public interface UserMapper extends MyBaseMapper<User> {

}

MyBaseMapper

public interface MyBaseMapper<T> extends BaseMapper<T> {
    /**
     *自定义findOne方法
     */
    T findOne(Serializable id);
    /**
     *自定义deleteAll方法
     */
    int deleteAll();
}
  • (4)创建MP配置文件MybatisPlusConfig,放在config目录下。
@Configuration
public class MybatisPlusConfig {
    /**
     * 自定义sql注入器
     */
    @Bean
    public MySqlInjector mySqlInjector (){
        return new MySqlInjector();
    }
}

做完以上配置,就完成自定义方法的注入。但是讲到sql注入器,不得不讲下mapper层选装件。mapper层选装件需要sql注入器配合使用。

mapper层选装件的使用

选装件 效果备注
alwaysUpdateSomeColumnById(T entity) 根据ID更新固定的某些字段
insertBatchSomeColumn(List entityList) 批量新增数据,自选字段新增
deleteByIdWithFill(T entity) 根据ID删除逻辑删除字段,并带字段回填功能
  1. 在MyBaseMapper上添加选装件的方法
public interface MyBaseMapper<T> extends BaseMapper<T> {
    /**
     *自定义findOne方法
     */
    T findOne(Serializable id);
    //选装件方法
    int alwaysUpdateSomeColumnById(@Param(Constants.ENTITY)T entity);

    int insertBatchSomeColumn(List<T> entityList);

    int deleteByIdWithFill(T entity);
}
  1. 在自定义注入器MySqlInjector上添加选装件方法
public class MySqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        //增加自定义方法
        methodList.add(new DeleteAll());
        methodList.add(new FindOne());
        //predicate判断不需要更新的字段
        methodList.add(new AlwaysUpdateSomeColumnById(predicate ->!predicate.getColumn().equals("sex")));
        //predicate判断不需要新增的字段
        methodList.add(new InsertBatchSomeColumn(predicate ->!predicate.getColumn().equals("sex")));
        methodList.add(new LogicDeleteByIdWithFill());
        return methodList;
    }
}
  1. deleteByIdWithFill选装件使用时,所需要数据回填的字段添加@TableField(fill = FieldFill.UPDATE)注解。因为是逻辑删除,执行的是update操作
    @TableField(value = "name",fill = FieldFill.UPDATE)
    private String name;
    @TableField(value = "password",fill = FieldFill.UPDATE)
    private String password;

六、MP通用枚举

  1. 自定义枚举类SexEnum,实现MP提供的IEnum
public enum SexEnum implements IEnum<String> {
    MAN("1","男"),
    WOMAN("0","女"),
    NONE("2","未知");
    private String value;
    private String desc;

    SexEnum(String value , String desc) {
        this.value = value;
        this.desc = desc;
    }

    @Override
    public String getValue() {
        return this.value;
    }
}
  1. 在application.yml文件中加入枚举路径配置
mybatis-plus:
  type-enums-package: com.yuzw.cusproject.mybatisplus.enums
  1. 在MybatisPlusConfig配置Jackson序列化
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer(){
        return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
    }   
  1. 在User类用枚举定义
    @TableField(value = "sex",fill = FieldFill.UPDATE)
    private SexEnum sexEnum;

随后进行CRUD操作,可以进行正常的操作。但是在控制层(即controller)有个问题,在接收JSON的User类时,枚举是按照ordinal(下标)接收的。举个例子,传的是0(女),但是后台接收到的是1(男)。因为1(男)在SexEnum是第一个定义的。如果还不理解,上图:

图1postman传的是0(女),但是控制层接收的是1(男),请注意,控制层就是根据ordinal来接收的(具体源码没看到)。为了解决这个问题,我弄了一个自定义枚举,没用mybatisPlus提供的枚举。

七、自定义枚举

  1. 自定义BaseEnum枚举类接口
public interface BaseEnum<T> {
    /**
     * 真正与数据库进行映射的值
     *
     * @return value
     */
    @JsonValue
    T getValue();

    /**
     * 显示的名称
     *
     * @return displayName
     */
    String getDisplayName();

    /**
     * 按枚举的value获取枚举实例
     *
     * @param enumType 枚举类型
     * @param value    值
     * @return T
     */
    static <E extends BaseEnum> E fromValue(Class<E> enumType, Object value) {
        if (value == null) {
            return null;
        }
        for (E e : enumType.getEnumConstants()) {
            if ((value instanceof String && value.equals(e.getValue())) ||
                    (value instanceof Integer && value == e.getValue()) ||
                    (value instanceof BigDecimal && Integer.valueOf(value.toString()) == e.getValue())) {
                return e;
            }
        }
        return null;
    }

    /**
     * 按枚举的value获取枚举显示名称
     *
     * @param enumType 枚举类型
     * @param value    值
     * @return String
     */
    static <E extends BaseEnum> String getDisplayNameByValue(Class<E> enumType, Object value) {
        E e = fromValue(enumType, value);
        if (e == null) {
            return null;
        } else {
            return e.getDisplayName();
        }
    }

    /**
     * 获取显示名称
     *
     * @param e 枚举
     * @return String
     */
    static <E extends BaseEnum> String getDisplayName(E e) {
        if (e == null) {
            return null;
        } else {
            return e.getDisplayName();
        }
    }

    /**
     * 批量-按枚举的value获取枚举显示名称
     *
     * @param enumType 枚举类型
     * @param value    值:多值逗号分隔
     * @return String
     */
    static <E extends BaseEnum> String getDisplayNameByValues(Class<E> enumType, String value) {
        String[] values = value.split(",");
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < values.length; i++) {
            for (E e : enumType.getEnumConstants()) {
                if (values[i].equals(e.getValue().toString())) {
                    if (result.toString().equals("")) {
                        result.append(e.getDisplayName());
                    } else {
                        result.append(",").append(e.getDisplayName());
                    }
                }
            }
        }
        return result.toString();
    }
}
  1. 自定义枚举类型转换器UniversalEnumTypeHandler
public class UniversalEnumTypeHandler<E extends BaseEnum> extends BaseTypeHandler<E> {
    /**
     * 枚举类型
     */
    private Class<E> type;

    /**
     * 枚举数组
     */
    private E[] enums;

    /**
     * Constructor
     *
     * @param type 枚举类型
     */
    public UniversalEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        this.enums = this.type.getEnumConstants();
        if (this.enums == null) {
            throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        //BaseTypeHandler 进行非空校验
        if (jdbcType == null) {
            ps.setObject(i, parameter.getValue());
        } else {
            ps.setObject(i, parameter.getValue(), jdbcType.TYPE_CODE);
        }
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        Object code = rs.getObject(columnName);
        if (rs.wasNull()) {
            return null;
        }
        return getEnumByValue(code);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        Object code = rs.getObject(columnIndex);
        if (rs.wasNull()) {
            return null;
        }
        return getEnumByValue(code);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        Object code = cs.getObject(columnIndex);
        if (cs.wasNull()) {
            return null;
        }
        return getEnumByValue(code);
    }

    /**
     * 值获取枚举对象
     *
     * @param value 值
     * @return 枚举
     * @author Frode Fan
     */
    private E getEnumByValue(Object value) {
        if (value instanceof Integer) {
            for (E e : enums) {
                if (e.getValue() == value) {
                    return e;
                }
            }
            throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code :  " +
                    value);
        }

        if (value instanceof BigDecimal) {
            for (E e : enums) {
                if (e.getValue() == Integer.valueOf(value.toString())) {
                    return e;
                }
            }
            throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code :  " +
                    value);
        }

        if (value instanceof String) {
            for (E e : enums) {
                if (value.equals(e.getValue())) {
                    return e;
                }
            }
            throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code :  " +
                    value);
        }
        throw new IllegalArgumentException("Unknown enumeration type:" + enums.getClass() + ", value:" + value);
    }
}

3.在MybatisPlusConfig中配置默认的枚举类型处理器

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return new MybatisPlusCustomizers();
    }

    /**
     * 自定义配置
     */
    class MybatisPlusCustomizers implements ConfigurationCustomizer {

        @Override
        public void customize(org.apache.ibatis.session.Configuration configuration) {
            configuration.setDefaultEnumTypeHandler(UniversalEnumTypeHandler.class);
        }
    }
  1. 自定义枚举SexEnum实现BaseEnum
public enum SexEnum implements BaseEnum<String> {
    MAN("1","男"),
    WOMAN("0","女"),
    NONE("2","未知");
    private String value;
    private String desc;

    SexEnum(String value , String desc) {
        this.value = value;
        this.desc = desc;
    }

    @Override
    public String getValue() {
        return this.value;
    }

    @Override
    public String getDisplayName() {
        return desc;
    }
   
}

最后上下目录结构图,方便大家参考

感言

这是我第一次写博客,写的不正确的地方欢迎大家指正