前言
这几天在使用的mybatis-plus的时候,在遇见复杂业务的时候遇见的一些租户过滤问题,面对多表关联查询的时候、自定义sql的时候,或者说一对多的时候,其中一个查询等功能过滤过滤租户的解决方案。
在一个缓存命中率不高的场景中,分页很多时候不能依赖主数据分页查询再遍历查询的方式来组装数据的时候,就会遇见自定义sql 或者是一对多查询。这个时候如果用mybatis-plus的多租户就会很有问题。
自定义sql分页查询方法:
Mapper.xml
<select id="getPageUser" resultMap="userResult">
select * from user ${ew.customSqlSegment}
</select>
这里的SQL很简单,根据自己的业务变动sql。${ew.customSqlSegment} 很多人不了解这个哈,就是:Wrapper<Material> queryWrapper 转化后的sql。还不明白的话,继续看…
Mapper.java
@Mapper
public interface UserMapper extends BaseMapper<User> {
List<User> getPageUser(@Param(Constants.WRAPPER) Wrapper<Material> queryWrapper);
}
看清楚哦,这里返回的是一个list集合。${ew.customSqlSegment} 就是指的这的queryWrapper,ew就是Constants.WRAPPER的值。
ServiceImpl.java
@Override
public PageInfo<User> getPageUser(PageParam<UserQueryParam> param, Wrapper<User> queryWrapper) {
// TODO Auto-generated method stub
PageHelper.startPage(param.getCurrent(), param.getPageSize());
List<User> list = baseMapper.getPageUser(queryWrapper);
PageInfo<User> pageInfo = new PageInfo<User>(list);
return pageInfo;
}
PageParam 就是组装了,当前页码与页行数,UserQueryParam 是查询条件:用于组装在queryWrapper中。
Service.java、Controller.java我就直接省了….
多租户面临的情况:
mybatis-plus 多住户配置:
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
/**
* @author miemie
* @since 2018-08-10
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 租户拦截 PaginationInterceptor
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler()));
return interceptor;
}
/**
* 租戶設置
* @return SQL解析过滤
*/
private TenantLineHandler tenantLineHandler() {
return new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new StringValue(getTenantNo());
}
// 在数据库中租户关联的字段
@Override
public String getTenantIdColumn() {
return "tenant_no";
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
List<String> list = new ArrayList<String>();
list.add("user");
list.add("a_region_city");
return list.contains(tableName);
}
};
}
}
ignoreTable 就是根据表名进行过滤租户,全表所有的sql都不会拼接租户的sql。
那么某个表单独一个sql怎么取消租户过滤呢?
官方方法:就是在Mapper.java的方法上加如下注解
@InterceptorIgnore(tenantLine = "1")
比如加在Mapper.java的getPageUser方法上:
@Mapper
public interface UserMapper extends BaseMapper<User> {
@InterceptorIgnore(tenantLine = "1")
List<User> getPageUser(@Param(Constants.WRAPPER) Wrapper<Material> queryWrapper);
}
执行分页查询发现报错,页数对不上,原因是分页查询有:select count(*) from user 的语句,这个是分页工具的能力,如何解决呢?
能通过传递租户ID就不自动拼接sql吗?
能让增加、修改、删除需要租户,而查询不需要吗?
统一回答:当然可以
具体思路与方法输入下:
通过mybatis-plus 多住户配置MybatisPlusConfig可看出租户拦截器是TenantLineInnerInterceptor,查看源码发现有如下方法:
- processSelect
- processSelectBody
- processInsert
- processUpdate
- processDelete
你可以重写对应的方法,就可以实现sql中某一个方法不加租户拼接举例如下:
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 租户拦截 PaginationInterceptor
interceptor.addInnerInterceptor(tenantLineInnerInterceptor(tenantLineHandler()));
return interceptor;
}
TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantLineHandler tenantLineHandler) {
return new TenantLineInnerInterceptor(tenantLineHandler) {
//重写查询,租户拦截问题
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
if (sql.contains("tenant_no")) {
return;
}
processSelectBody(select.getSelectBody());
List<WithItem> withItemsList = select.getWithItemsList();
if (!CollectionUtils.isEmpty(withItemsList)) {
withItemsList.forEach(this::processSelectBody);
}
}
};
}
/**
* 租戶設置
* @return SQL解析过滤
*/
private TenantLineHandler tenantLineHandler() {
return new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new StringValue(getTenantNo());
}
// 在数据库中租户关联的字段
@Override
public String getTenantIdColumn() {
return "tenant_no";
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
List<String> list = new ArrayList<String>();
list.add("user");
list.add("a_region_city");
return list.contains(tableName);
}
};
}
}
现在再去查询发现分页的统计是ok的了,数量也能对的上。
最后说说一对多sql实现
VO代码:
/**
* @Description: 说明
* @author: kinbug
* @date: 2021年07月22日
*/
@Data
public class MaterialVO {
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
@ApiModelProperty(value = "id")
private Long id;
@ApiModelProperty(value = "编号")
private String no;
@ApiModelProperty(value = "租户编号")
private String tenantNo;
@ApiModelProperty(value = "产品名字")
private String name;
@ApiModelProperty(value = "产品类别")
private String categoryNo;
@ApiModelProperty(value = "商品单位")
private String unit;
@ApiModelProperty(value = "产品编号")
private String serialNo;
@ApiModelProperty(value = "产品型号")
private String model;
@ApiModelProperty(value = "产品规格")
private String standard;
@ApiModelProperty(value = "产品状态(0未启用,1启用)")
private Boolean enabled;
@ApiModelProperty(value = "制造商名")
private String mfrs;
@ApiModelProperty(value = "删除标记(0未删除,1是删除)")
private Boolean deleteFlag;
@ApiModelProperty(value = "创建人")
private String createUno;
@ApiModelProperty(value = "添加时间")
@JsonFormat(pattern = DATE_FORMAT)
private Date createTime;
@ApiModelProperty(value = "修改人")
private String updateUno;
@ApiModelProperty(value = "修改时间")
@JsonFormat(pattern = DATE_FORMAT)
private Date updateTime;
@ApiModelProperty(value = "商品条码价格")
private List<MaterialExtend> materialExtends;
@ApiModelProperty(value = "商品图片")
private List<MaterialImage> materialImages;
public void copyMaterial(Material material) {
BeanUtils.copyProperties(material, this);
}
}
Mapper.XML
<!-- start查询一个商品信息 -->
<resultMap type="com.turtle.eos.domian.vo.material.MaterialVO" id="materialResult">
<result column="id" property="id" />
<result column="no" property="no" />
<result column="name" property="name" />
<result column="category_no" property="categoryNo" />
<result column="unit" property="unit" />
<result column="serial_no" property="serialNo" />
<result column="model" property="model" />
<result column="standard" property="standard" />
<result column="enabled" property="enabled" />
<result column="mfrs" property="mfrs" />
<result column="delete_flag" property="deleteFlag" />
<result column="tenant_no" property="tenantNo" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="update_uno" property="updateUno" />
<result column="create_uno" property="createUno" />
<collection property="materialExtends" ofType="com.turtle.eos.entity.material.MaterialExtend" select="queryMaterialExtends" column="no">
</collection>
<collection property="materialImages" ofType="com.turtle.eos.entity.material.MaterialImage" select="queryMaterialImages" column="no">
</collection>
</resultMap>
<select id="getMaterial" resultMap="materialResult">
select * from m_material ${ew.customSqlSegment}
</select>
<select id="queryMaterialExtends" resultType="com.turtle.eos.entity.material.MaterialExtend">
select * from m_material_extend where delete_flag = 0 and material_no = #{no}
</select>
<select id="queryMaterialImages" resultType="com.turtle.eos.entity.material.MaterialImage">
select * from m_material_image where delete_flag = 0 and material_no = #{no}
</select>
<!-- end查询一个商品信息 -->
值得注意的是collection中的column是给下一个queryMaterialExtends查询传递的值。