mybatisplus数据权限插件学习初探 & 动态表名更换插件 &防止全表更新与删除插件 & 乐观锁插件

学习链接

Mybatis-Plus入门系列(3)- MybatisPlus之数据权限插件DataPermissionInterceptor

Mybatis-Plus入门系列(18) -基于注解的动态数据权限实现方案

SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制

从零搭建开发脚手架 基于Mybatis-Plus的数据权限实现

【RuoYi-Vue-Plus】学习笔记 09 - 数据权限调用流程分析(参照 Mybatis Plus 数据权限插件)

Mybatisplus生成代码配置 & p6spy打印sql & mybatis日志打印 & mybatisplus用法
jsqlparser学习 - 自己收藏的链接

Mysql递归查询子级(父子级结构)&从子级ID查询所有父级(及扩展知识) - 自己的链接
Mysql带层级(父子级)的递归查询案例 - 自己的链接

mybatisplus数据权限插件学习初探

前言

对于系统中的不同用户,对于同一接口,可能都有权限访问此接口,但是由于用户各自的权限大小,看到的数据不一样(数据权限)。

就比如:有一张用户表,每个用户只能属于某一个部门,每个用户都有自己的用户类型(用户类型有老板、部门经理、普通职工),每个员工的订单记录在订单表中,订单属于创建这个订单的用户,订单也属于这个用户的部门。

  • 对于老板来说,能看到所有的订单
  • 对于部门经理来说,能够看到自己部门(可能还有子部门,这里暂不考虑)的订单
  • 对于普通职工来说,只能看到自己的订单

如果对于订单表的查询有多个地方,或者又不仅仅是订单表需要按上面的规则来作数据权限控制,那么在service层的每个地方几乎都要来上面的用户类型判断,然后再写不同的sql来做对应的查询。

所以如果能在dao层能够根据当前用户类型,自动的拼接对应的条件,那样是比较方便的,下面演示的案例仅作为自己入门学习案例记录,对于复杂的sql,需要深入学习下Jsqlparser,参考一些开源项目的做法。

值得思考的问题:

  • 如何进行数据权限的划分?按部门划分是一种方案,但是部门之间有可能会存在上下级,有些人可能属于多个部门(同时存在上下级部门)。有没有其它的划分方式?
  • 复杂sql的处理?

案例

建表

在这里插入图片描述

用户表
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `type` int(1) DEFAULT NULL COMMENT '用户类型',
  `tenant_id` bigint(20) NOT NULL COMMENT '租户ID',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `dept_id` int(11) DEFAULT NULL COMMENT '所属部门id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (1, 0, 1, 'mp', NULL);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (2, 1, 1, 'Jack', 1);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (3, 1, 1, 'Sandy', 2);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (4, 2, 1, 'Billie', 1);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (5, 2, 1, 'Sally', 1);
INSERT INTO `test`.`user` (`id`, `type`, `tenant_id`, `name`, `dept_id`) VALUES (6, 2, 1, 'Kevin', 2);
订单表
CREATE TABLE `orders` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `orders_no` varchar(10) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `dept_id` int(11) DEFAULT NULL,
  `tenant_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

INSERT INTO `test`.`orders` (`id`, `orders_no`, `user_id`, `dept_id`, `tenant_id`) VALUES (1, '001', 4, 1, 1);
INSERT INTO `test`.`orders` (`id`, `orders_no`, `user_id`, `dept_id`, `tenant_id`) VALUES (2, '002', 5, 1, 1);
INSERT INTO `test`.`orders` (`id`, `orders_no`, `user_id`, `dept_id`, `tenant_id`) VALUES (3, '003', 6, 2, 1);

环境准备

User
@Data
@Accessors(chain = true)
public class User {

    /**
     * 租户 ID
     */
    private Long tenantId;

    @TableId(type = IdType.AUTO)
    private Long id;

    private Integer type;

    private Integer deptId;

    private String name;

}
UserMapper
public interface UserMapper extends BaseMapper<User> {
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.baomidou.mybatisplus.samples.dataPerm.mapper.UserMapper">


</mapper>
Orders
@Data
@Accessors(chain = true)
public class Orders {

    private String id;

    private String ordersNo;

    private Integer userId;

    private String deptId;

}
OrdersMapper
public interface OrdersMapper extends BaseMapper<Orders> {

    List<Orders> selectOrdersList();

}
OrdersMapper.xml

这里起初并没有where条件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.baomidou.mybatisplus.samples.dataPerm.mapper.OrdersMapper">

    <select id="selectOrdersList" resultType="com.baomidou.mybatisplus.samples.dataPerm.entity.Orders">
        SELECT * FROM orders
    </select>

</mapper>

配置

UserTypeEnum
@Getter
public enum UserTypeEnum {
    BOSS(0,"老板"),
    DEPT_MANAGER(1,"部门经理"),
    CLERK(1,"普通职员"),
    DEFAULT(1,"普通职员"),
    ;
    Integer type;
    String desc;

    UserTypeEnum(Integer type, String desc) {
        this.type = type;
        this.desc = desc;
    }

    public static UserTypeEnum type(Integer type) {
        for (UserTypeEnum value : UserTypeEnum.values()) {
            if (Objects.equals(type, value.type)) {
                return value;
            }
        }
        return DEFAULT;
    }

}
UserContextHolder
public class UserContextHolder {

    private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();

    public static void bindUser(User user) {
        USER_THREAD_LOCAL.set(user);
    }

    public static void unBindUser() {
        USER_THREAD_LOCAL.remove();
    }

    public static User getUser() {
        return USER_THREAD_LOCAL.get();
    }

}
CustomizeDataPermissionHandler
@Component
public class CustomizeDataPermissionHandler implements DataPermissionHandler {

    @Override
    public Expression getSqlSegment(Expression where, String mappedStatementId) {

        User user = UserContextHolder.getUser();

        if (user == null) {
            return where;
        }

        UserTypeEnum type = UserTypeEnum.type(user.getType());
        if (UserTypeEnum.BOSS == type) {
            return where;
        } else if (UserTypeEnum.DEPT_MANAGER == type) {

            // 部门下可能存在子部门(需要查询当前部门的所有子部门(包括当前部门),可以使用sql递归), 这里暂时就认为只有1个
            StringValue deptIdStringValue = new StringValue(String.valueOf(user.getDeptId()));
            ExpressionList expressionList = new ExpressionList(deptIdStringValue);

            InExpression inExpression = new InExpression(new Column("orders.dept_id"), expressionList);

            if (where == null) {
                // 如果原来没有where条件, 就添加一个where条件
                return inExpression;
            } else {
                return new AndExpression(where, inExpression);
            }


        } else {

            EqualsTo equalsTo = new EqualsTo(new Column("orders.user_id"), new StringValue(String.valueOf(user.getUserId())));

            if (where == null) {
                // 如果原来没有where条件, 就添加一个where条件
                return equalsTo;
            } else {
                return new AndExpression(where, equalsTo);
            }

        }
    }

}

MybatisPlusConfig
@Configuration
@MapperScan("com.baomidou.mybatisplus.samples.dataPerm.mapper")
public class MybatisPlusConfig {

    @Autowired
    private CustomizeDataPermissionHandler dataPermissionHandler;

    /**
     * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        interceptor.addInnerInterceptor(new DataPermissionInterceptor(dataPermissionHandler));
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                User user = UserContextHolder.getUser();
                if (user != null) {
                    return new LongValue(user.getTenantId());
                } else {
                    return null;
                }
            }

            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            @Override
            public boolean ignoreTable(String tableName) {
                return Objects.equals("user", tableName);
            }
        }));

        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        paginationInnerInterceptor.setDialect(DialectFactory.getDialect(DbType.MYSQL));
        interceptor.addInnerInterceptor(paginationInnerInterceptor);

        // interceptor.addInnerInterceptor();
        // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
        // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
//        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }

//    @Bean
//    public ConfigurationCustomizer configurationCustomizer() {
//        return configuration -> configuration.setUseDeprecatedExecutor(false);
//    }
}

测试

测试类
@Slf4j
@SpringBootTest(classes = TenantApplication.class)
public class TenantTest {

    @Resource
    private UserMapper userMapper;

    @Resource
    private OrdersMapper ordersMapper;

    @Test
    public void test001() {

        // User user = userMapper.selectById(1);
        // User user = userMapper.selectById(2);
        User user = userMapper.selectById(4);
        UserContextHolder.bindUser(user);

        List<Orders> orders = ordersMapper.selectOrdersList();
        for (Orders order : orders) {
            log.info("{}", order);
        }

        Page<Orders> ordersPage = new Page<>(1, 1);
        ordersMapper.selectPage(ordersPage, null);
        log.info("total:{},pages:{},data:{}",ordersPage.getTotal(),ordersPage.getPages(), ordersPage.getRecords());

        UserContextHolder.unBindUser();
    }

}
boss

老板可以看到所有的数据。可以看到下面,仅拼接了租户id

SELECT id, tenant_id, type, dept_id, name FROM user WHERE id = 1

SELECT * FROM orders WHERE orders.tenant_id = 1

SELECT COUNT(*) AS total FROM orders WHERE orders.tenant_id = 1
SELECT id, orders_no, user_id, dept_id FROM orders WHERE orders.tenant_id = 1 LIMIT 1
deptManager

部门经理可以看到本部门及子部门的数据。可以看到下面拼接了in的条件

SELECT id, tenant_id, type, dept_id, name FROM user WHERE id = 2

SELECT * FROM orders WHERE orders.dept_id IN ('1') AND orders.tenant_id = 1

SELECT COUNT(*) AS total FROM orders WHERE orders.dept_id IN ('1') AND orders.tenant_id = 1
SELECT id, orders_no, user_id, dept_id FROM orders WHERE orders.dept_id IN ('1') AND orders.tenant_id = 1 LIMIT 1
clerk

普通职员只能看到自己的数据。可以看到拼接的条件使用user_id

SELECT id, tenant_id, type, dept_id, name FROM user WHERE id = 4

SELECT * FROM orders WHERE orders.user_id = '4' AND orders.tenant_id = 1

SELECT COUNT(*) AS total FROM orders WHERE orders.user_id = '4' AND orders.tenant_id = 1
SELECT id, orders_no, user_id, dept_id FROM orders WHERE orders.user_id = '4' AND orders.tenant_id = 1 LIMIT 1

动态表名更换插件

这个插件比较简单,就是在sql中遇到表名,会把这个表名传递给TableNameHandler处理器,然后在TableNameHandler处理器的dynamicTableName方法中获取到表名,然后返回一个新的表名替换掉sql中的原表名。

案例

代码

MyBatisplusConfig
@Slf4j
@Configuration
public class MyBatisplusConfig {

    @Bean
    public MybatisPlusInterceptor interceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        dynamicTableNameInnerInterceptor.setTableNameHandler(new TableNameHandler() {
            @Override
            public String dynamicTableName(String sql, String tableName) {
                log.info("sql: {}", sql);
                log.info("tableName: {}", tableName);
                return tableName + "_1";
            }
        });
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        return interceptor;
    }

}
AccountController
@RestController
@RequestMapping("/account")
public class AccountController {

    @RequestMapping("getAccounts")
    public List<Account> getAccounts() {
    
    	// 调用mybatisplus的方法
        return accountService.list();
    }

    @RequestMapping("getAccounts2")
    public List<Account> getAccounts2() {
    	
    	// 调用自己写的mapper方法 
        return accountMapper.find();
    }
}
AccountMapper
public interface AccountMapper extends BaseMapper<Account> {

    List<Account> find();
}
AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzhua.mapper.AccountMapper">

    <select id="find" resultMap="BaseResultMap">
        SELECT * FROM account a INNER JOIN user u on a.user_id = u.id
    </select>

</mapper>

测试

访问:http://localhost:8080/account/getAccounts,可以看到表名替换了

在这里插入图片描述
访问:http://localhost:8080/account/getAccounts2,可以看到2个表名都会经过TableNameHandler
在这里插入图片描述

防止全表更新与删除插件

这个插件可以防止全表更新与删除插件,就是如果不带where条件DML的sql会抛出异常,但是有的时候,就是需要全表删除呢?可以在mapper方法上使用@InterceptorIgnore(blockAttack = “true”)注解标注此方法即可。

案例

代码

MyBatisplusConfig

直接加就完了

@Slf4j
@Configuration
public class MyBatisplusConfig {

    @Bean
    public MybatisPlusInterceptor interceptor() {
    
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }

}
AccountController
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("deleteAll")
    public Object deleteAll() {
        accountMapper.deleteAll();
        return "删除成功";
    }

    @GetMapping("updateAll")
    public Object updateAll() {
        LambdaUpdateWrapper<Account> updateWrapper = new LambdaUpdateWrapper<Account>().set(Account::getNickName, "005");
        accountMapper.update(null, updateWrapper);
        return "修改成功";
    }

}
AccountMapper
public interface AccountMapper extends BaseMapper<Account> {

    @InterceptorIgnore(blockAttack = "true") // true表示不启用插件
    void deleteAll();
}
AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzhua.mapper.AccountMapper">

    <delete id="deleteAll">
        delete from account
    </delete>

</mapper>

乐观锁插件

给需要加乐观锁的表,添加一个version字段,然后给这个version字段来个默认值1(如果这个version字段为null的话,不会拼接乐观锁的sql),然后给实体类的version属性添加@Version注解,即可

其实就是当版本一致时,才会更新到数据库中,根据数据库的返回结果(受影响的行数),就能知道有没有更新成功

案例

代码

MyBatisplusConfig
@Slf4j
@Configuration
public class MyBatisplusConfig {

    @Bean
    public MybatisPlusInterceptor interceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        return interceptor;
    }

}
Account
@Getter
@Setter
@ApiModel(value = "Account对象", description = "")
public class Account implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String nickName;

    @Version
    private Long version;
}

AccountController
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("updateAccount")
    public Object updateAccount() {
        Account account = accountMapper.selectById(2);
        account.setNickName("m2");
        int update = accountMapper.update(account, null);
        System.out.println("更新结果: " + update);
        return "ok";
    }

}

测试

访问:http://localhost:8080/account/updateAccount

在这里插入图片描述

逻辑删除

在表中添加一个字段,用来表示该条数据的删除状态。当添加时,需要自己手动设置未删除状态;当删除时,并不会执行delete语句,而是执行update语句,更改删除状态字段为已删除;当更新时,也会带上未删除状态作为条件;当查询时,也会带上未删除状态作为条件。
(当然,这只会在mybatisplus自动注入的方法中才会生效,自己手动写的不会这样搞)

注意事项

  • 只对自动注入的 sql 起效:

    • 插入: 不作限制
    • 查找: 追加 where 条件过滤掉已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
    • 更新: 追加 where 条件防止更新到已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动追加该字段
    • 删除: 转变为 更新
      • 例如:

        • 删除: update user set deleted=1 where id = 1 and deleted=0

        • 查找: select id,name,deleted from user where deleted=0

  • 字段类型支持说明:

    • 支持所有数据类型(推荐使用 Integer,Boolean,LocalDateTime)
    • 如果数据库字段使用datetime,逻辑未删除值和已删除值支持配置为字符串null,另一个值支持配置为函数来获取值如now()
  • 附录:

    • 逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。
    • 如果你需要频繁查出来看就不应使用逻辑删除,而是以一个状态去表示。

案例

application.yml

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isDeleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

Account

@Getter
@Setter
@ApiModel(value = "Account对象", description = "")
public class Account implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String nickName;

    @TableLogic
    private Integer isDeleted;
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值