前言

本文涉及到的平台有 https://gitee.com/renrenio/renren-securityhttps://gitee.com/JavaLionLi/RuoYi-Vue-Plus,探讨的场景主要是需要 join 联查的分页场景,基本上也是这个场景的实现会比较纠结,常见的单表分页,代码生成器中的代码大部分开发平台使用是没有太多问题的

renren-security

我们这次使用的最新版本
image.png
我们以用户的分页查询为例先来看看框架中提供的实现

controller

@RequiresPermissions("sys:user:page")
public Result<PageData<SysUserDTO>> page(@ApiIgnore @RequestParam Map<String, Object> params){
	PageData<SysUserDTO> page = sysUserService.page(params);

	return new Result<PageData<SysUserDTO>>().ok(page);
}
  1. 结果返回使用的是 Result<PageData>
  • Result 这个类比较常规,就是统一返回的封装,里面带一个泛型
@ApiModel(value = "响应")
public class Result<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 编码:0表示成功,其他值表示失败
     */
    @ApiModelProperty(value = "编码:0表示成功,其他值表示失败")
    private int code = 0;
    /**
     * 消息内容
     */
    @ApiModelProperty(value = "消息内容")
    private String msg = "success";
    /**
     * 响应数据
     */
    @ApiModelProperty(value = "响应数据")
    private T data;

    public Result<T> ok(T data) {
        this.setData(data);
        return this;
    }

    public boolean success(){
        return code == 0;
    }

    public Result<T> error() {
        this.code = ErrorCode.INTERNAL_SERVER_ERROR;
        this.msg = MessageUtils.getMessage(this.code);
        return this;
    }

    public Result<T> error(int code) {
        this.code = code;
        this.msg = MessageUtils.getMessage(this.code);
        return this;
    }

    public Result<T> error(int code, String msg) {
        this.code = code;
        this.msg = msg;
        return this;
    }

    public Result<T> error(String msg) {
        this.code = ErrorCode.INTERNAL_SERVER_ERROR;
        this.msg = msg;
        return this;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
  • PageData 是renren中对分页数据的统一封装类,一般开发平台中都会抽象出来这个类,用来在vo中传递
@Data
@ApiModel(value = "分页数据")
public class PageData<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "总记录数")
    private int total;

    @ApiModelProperty(value = "列表数据")
    private List<T> list;

    /**
     * 分页
     * @param list   列表数据
     * @param total  总记录数
     */
    public PageData(List<T> list, long total) {
        this.list = list;
        this.total = (int)total;
    }
}

返回数据后添加了一个 total,方便前端做分页

  1. 形参传递的是一个 map,这一点其实我一直有自己的想法,因为这样做vo层就无法校验了,可以扩展自己的query vo 来替换掉这里的实现

service

public PageData<SysUserDTO> page(Map<String, Object> params) {
	//转换成like
	paramsToLike(params, "username");

	//分页
	IPage<SysUserEntity> page = getPage(params, Constant.CREATE_DATE, false);

	//普通管理员,只能查询所属部门及子部门的数据
	UserDetail user = SecurityUser.getUser();
	if(user.getSuperAdmin() == SuperAdminEnum.NO.value()) {
		params.put("deptIdList", sysDeptService.getSubDeptIdList(user.getDeptId()));
	}

		//查询
	List<SysUserEntity> list = baseDao.getList(params);

	return getPageData(list, page.getTotal(), SysUserDTO.class);
}
  1. 简单提下这里传递用的是 dto 类,这里renren把dto和entity拆分开,dto中其实放的是校验的逻辑,entity相当于do,只做数据库层的映射,不做修改,粘贴下 dto 的代码就知道了
@Data
@ApiModel(value = "用户管理")
public class SysUserDTO implements Serializable {
    private static final long serialVersionUID = 1L;

	@ApiModelProperty(value = "id")
	@Null(message="{id.null}", groups = AddGroup.class)
	@NotNull(message="{id.require}", groups = UpdateGroup.class)
	private Long id;

	@ApiModelProperty(value = "用户名", required = true)
	@NotBlank(message="{sysuser.username.require}", groups = DefaultGroup.class)
	private String username;

	@ApiModelProperty(value = "密码")
	@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
	@NotBlank(message="{sysuser.password.require}", groups = AddGroup.class)
	private String password;

	@ApiModelProperty(value = "姓名", required = true)
	@NotBlank(message="{sysuser.realname.require}", groups = DefaultGroup.class)
	private String realName;

	@ApiModelProperty(value = "头像")
	private String headUrl;

	@ApiModelProperty(value = "性别   0:男   1:女    2:保密", required = true)
	@Range(min=0, max=2, message = "{sysuser.gender.range}", groups = DefaultGroup.class)
	private Integer gender;

	@ApiModelProperty(value = "邮箱")
	@Email(message="{sysuser.email.error}", groups = DefaultGroup.class)
	private String email;

	@ApiModelProperty(value = "手机号")
	private String mobile;

	@ApiModelProperty(value = "部门ID", required = true)
	@NotNull(message="{sysuser.deptId.require}", groups = DefaultGroup.class)
	private Long deptId;

	@ApiModelProperty(value = "状态  0:停用    1:正常", required = true)
	@Range(min=0, max=1, message = "{sysuser.status.range}", groups = DefaultGroup.class)
	private Integer status;

	@ApiModelProperty(value = "创建时间")
	@JsonProperty(access = JsonProperty.Access.READ_ONLY)
	private Date createDate;

	@ApiModelProperty(value = "超级管理员   0:否   1:是")
	@JsonProperty(access = JsonProperty.Access.READ_ONLY)
	private Integer superAdmin;

	@ApiModelProperty(value = "角色ID列表")
	private List<Long> roleIdList;

	@ApiModelProperty(value = "部门名称")
	private String deptName;

}

这里的校验用了分组校验,message提示取出的是 common 模块中 resource 中的 validation.properties

sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
sysuser.email.require=\u90AE\u7BB1\u4E0D\u80FD\u4E3A\u7A7A
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
sysuser.mobile.require=\u624B\u673A\u53F7\u4E0D\u80FD\u4E3A\u7A7A
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
sysuser.superadmin.range=\u8D85\u7EA7\u7BA1\u7406\u5458\u53D6\u503C\u8303\u56F40~1
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A

这里为啥是 这个码???
后面有机会再详细聊聊校验这块的实现

  1. paramsToLike(params, “username”); 来自于继承的 BaseServiceImpl 中的实现
protected void paramsToLike(Map<String, Object> params, String... likes){
    for (String like : likes){
        String val = (String)params.get(like);
        if (StringUtils.isNotBlank(val)){
            params.put(like, "%" + val + "%");
        }else {
            params.put(like, null);
        }
    }
}

功能比较简单,其实就是把这个参数 前后拼接 %% , 添加到查询参数

  1. 构造 Ipage
IPage<SysUserEntity> page = getPage(params, Constant.CREATE_DATE, false);
/**
     * 获取分页对象
     * @param params      分页查询参数
     * @param defaultOrderField  默认排序字段
     * @param isAsc              排序方式
     */
    protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
        // 1. 构造分页参数
        //分页参数
        long curPage = 1;
        long limit = 10;

        // 如果分页参数存在则取出
        if(params.get(Constant.PAGE) != null){
            curPage = Long.parseLong((String)params.get(Constant.PAGE));
        }
        if(params.get(Constant.LIMIT) != null){
            limit = Long.parseLong((String)params.get(Constant.LIMIT));
        }

        //分页对象
        Page<T> page = new Page<>(curPage, limit);

        //分页参数
        params.put(Constant.PAGE, page);

        // 2. 构造排序参数
        //排序字段
        String orderField = (String)params.get(Constant.ORDER_FIELD);
        String order = (String)params.get(Constant.ORDER);

        //前端字段排序
        if(StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)){
            if(Constant.ASC.equalsIgnoreCase(order)) {
                return page.addOrder(OrderItem.asc(orderField));
            }else {
                return page.addOrder(OrderItem.desc(orderField));
            }
        }

        //没有排序字段,则不排序
        if(StringUtils.isBlank(defaultOrderField)){
            return page;
        }

        //默认排序
        if(isAsc) {
            page.addOrder(OrderItem.asc(defaultOrderField));
        }else {
            page.addOrder(OrderItem.desc(defaultOrderField));
        }

        return page;
    }

其实主要就做了两件事情

  • 构造分页参数
  • 构造排序参数
  1. 发起查询
return getPageData(list, page.getTotal(), SysUserDTO.class);

getPageData() 很重要,用来真正构造出分页数据
一共有两个构造方法

protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target){
    List<T> targetList = ConvertUtils.sourceToTarget(list, target);

    return new PageData<>(targetList, total);
}

protected <T> PageData<T> getPageData(IPage page, Class<T> target){
    return getPageData(page.getRecords(), page.getTotal(), target);
}
  1. 注意 这里做的 ConvertUtils.sourceToTarget(list, target); 这是 renren里面的实现,底层就是有判空的 BeanUtils
public static <T> T sourceToTarget(Object source, Class<T> target){
        if(source == null){
            return null;
        }
        T targetObject = null;
        try {
            targetObject = target.newInstance();
            BeanUtils.copyProperties(source, targetObject);
        } catch (Exception e) {
            logger.error("convert error ", e);
        }

        return targetObject;
    }

    public static <T> List<T> sourceToTarget(Collection<?> sourceList, Class<T> target){
        if(sourceList == null){
            return null;
        }

        List targetList = new ArrayList<>(sourceList.size());
        try {
            for(Object source : sourceList){
                T targetObject = target.newInstance();
                BeanUtils.copyProperties(source, targetObject);
                targetList.add(targetObject);
            }
        }catch (Exception e){
            logger.error("convert error ", e);
        }

        return targetList;
    }

dao

<select id="getList" resultType="io.renren.modules.sys.entity.SysUserEntity">
	select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName from sys_user t1
	where t1.super_admin = 0
	<if test="username != null and username.trim() != ''">
		and t1.username like #{username}
	</if>
	<if test="deptId != null and deptId.trim() != ''">
		and t1.dept_id = #{deptId}
	</if>
	<if test="gender != null and gender.trim() != ''">
		and t1.gender = #{gender}
	</if>
	<if test="deptIdList != null">
		and t1.dept_id in
		<foreach item="id" collection="deptIdList" open="(" separator="," close=")">
			#{id}
		</foreach>
	</if>
</select>

这块使用的子查询实现的联查

ruoyi-vue-plus

我们看下对应的

controller

@SaCheckPermission("system:user:list")
@GetMapping("/list")
public TableDataInfo<SysUser> list(SysUser user, PageQuery pageQuery) {
    return userService.selectPageUserList(user, pageQuery);
}

service

这边对应的类是 TableDataInfo

@Override
public TableDataInfo<SysUser> selectPageUserList(SysUser user, PageQuery pageQuery) {
    Page<SysUser> page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapper(user));
    return TableDataInfo.build(page);
}

TableDataInfo

/**
 * 表格分页数据对象
 *
 * @author Lion Li
 */
@Data
@NoArgsConstructor
public class TableDataInfo<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 总记录数
     */
    private long total;

    /**
     * 列表数据
     */
    private List<T> rows;

    /**
     * 消息状态码
     */
    private int code;

    /**
     * 消息内容
     */
    private String msg;

    /**
     * 分页
     *
     * @param list  列表数据
     * @param total 总记录数
     */
    public TableDataInfo(List<T> list, long total) {
        this.rows = list;
        this.total = total;
    }

    public static <T> TableDataInfo<T> build(IPage<T> page) {
        TableDataInfo<T> rspData = new TableDataInfo<>();
        rspData.setCode(HttpStatus.HTTP_OK);
        rspData.setMsg("查询成功");
        rspData.setRows(page.getRecords());
        rspData.setTotal(page.getTotal());
        return rspData;
    }

    public static <T> TableDataInfo<T> build(List<T> list) {
        TableDataInfo<T> rspData = new TableDataInfo<>();
        rspData.setCode(HttpStatus.HTTP_OK);
        rspData.setMsg("查询成功");
        rspData.setRows(list);
        rspData.setTotal(list.size());
        return rspData;
    }

    public static <T> TableDataInfo<T> build() {
        TableDataInfo<T> rspData = new TableDataInfo<>();
        rspData.setCode(HttpStatus.HTTP_OK);
        rspData.setMsg("查询成功");
        return rspData;
    }

}

可以看出 plus 中对于分页返回参数的封装较深,直接可以通过 Page 构造出,感觉这种方式比较方便啊

dao

看下 mapper 层的写法

@DataPermission({
    @DataColumn(key = "deptName", value = "d.dept_id"),
    @DataColumn(key = "userName", value = "u.user_id")
})
Page<SysUser> selectPageUserList(@Param("page") Page<SysUser> page, @Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);

在dao层做了可以添加数据权限
xml

<select id="selectPageUserList" resultMap="SysUserResult">
  select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex,
  u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from
  sys_user u
  left join sys_dept d on u.dept_id = d.dept_id
  ${ew.getCustomSqlSegment}
</select>

通过 {ew.getCustomSqlSegment} 可以传入 自定义 Wrapper

小结

renren 使用的是先一次性拼接好出来分页结果,使用子查询实现
plus 中使用的是left join,看起来比较常见的方式,mapper 直接拿到 Page

Logo

快速构建 Web 应用程序

更多推荐