java每日精进 5.15【分页实现】

1. 什么是对象转换和数据翻译?

对象转换

对象转换是指将一种类型的对象(如数据库实体 UserDO)转换为另一种类型的对象(如前端响应对象 UserVO 或服务层 DTO)。例如,一个 UserDO 包含用户 ID、姓名和部门 ID,我们需要将其转换为 UserVO,包含 ID、姓名和部门名称。这种转换在分层架构(如 Controller、Service、DAO)中非常常见。

数据翻译

数据翻译是指将某个字段的值“翻译”为另一个值。例如,UserVO 的 deptId 字段需要读取数据库中 DeptDO 的 name 字段,设置为 UserVO 的 deptName 字段。这种操作通常涉及关联查询或手动拼接。

为什么需要这些操作?

  • 分层隔离:DO(Data Object)用于数据库操作,VO(Value Object)用于前端响应,DTO(Data Transfer Object)用于服务层传递,各自职责不同。
  • 数据格式化:前端需要友好的数据格式(如部门名称而非 ID)。
  • 性能优化:通过翻译减少复杂 SQL 联表查询。

2. 对象转换:MapStruct vs BeanUtils

项目中提供了两种对象转换工具:MapStructBeanUtils。我们先来对比它们的优缺点,然后分别展示使用方法。

2.1 MapStruct

MapStruct 是一个编译时生成映射代码的框架,通过注解(如 @Mapper 和 @Mapping)定义映射规则,生成高效的 getter/setter 调用代码。

  • 优点
    • 高性能:生成纯 Java 代码,避免反射开销。
    • 类型安全:编译时检查映射规则,减少运行时错误。
    • 灵活性:支持复杂映射、自定义逻辑。
  • 缺点
    • 需要编写注解,配置稍复杂。
    • 学习成本稍高,需了解 MapStruct 语法。
  • 适用场景:高性能要求、复杂映射场景。

引入依赖


        
            org.mapstruct
            mapstruct
            ${mapstruct.version}
        
    
public class UserDO {
    private Long id;
    private String name;
    private Long deptId;

    public UserDO() {}
    public UserDO(Long id, String name, Long deptId) {
        this.id = id;
        this.name = name;
        this.deptId = deptId;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Long getDeptId() { return deptId; }
    public void setDeptId(Long deptId) { this.deptId = deptId; }
}


public class DeptDO {
    private Long id;
    private String name;

    public DeptDO() {}
    public DeptDO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}


public class UserVO {
    private Long id;
    private String name;
    private String deptName;

    public UserVO() {}
    public UserVO(Long id, String name, String deptName) {
        this.id = id;
        this.name = name;
        this.deptName = deptName;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDeptName() { return deptName; }
    public void setDeptName(String deptName) { this.deptName = deptName; }
}



@Mapper
public interface UserConvert {
    UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);

    @Mapping(source = "user.id", target = "id")
    @Mapping(source = "user.name", target = "name")
    @Mapping(source = "dept.name", target = "deptName")
    UserVO convert(UserDO user, DeptDO dept);

    default List convertList(List users, Map deptMap) {
        return users.stream()
                .map(user -> convert(user, deptMap.get(user.getDeptId())))
                .toList();
    }
}
  • UserDO:数据访问层的用户对象,包含用户 ID、姓名和部门 ID
  • DeptDO:数据访问层的部门对象,包含部门 ID 和部门名称
  • UserVO:视图层的用户对象,包含用户 ID、姓名和部门名称(直接包含部门名称而非部门 ID)
单个对象转换(convert 方法)
  • @Mapper 注解:告诉 MapStruct 这是一个转换器接口,会自动生成实现类
  • INSTANCE 常量:获取 MapStruct 生成的实现类实例
  • @Mapping 注解:定义字段映射规则
    • source = "user.id", target = "id":将 user 对象的 id 字段映射到 VO 的 id 字段
    • source = "user.name", target = "name":将 user 对象的 name 字段映射到 VO 的 name 字段
    • source = "dept.name", target = "deptName":将 dept 对象的 name 字段映射到 VO 的 deptName 字段
列表转换(convertList 方法)
  • 这是一个默认方法(Java 8 特性),提供了批量转换的实现
  • 使用 Java Stream API 遍历 UserDO 列表
  • 对于每个 UserDO,通过 deptId 从部门映射表中获取对应的 DeptDO
  • 调用单对象转换方法 convert () 完成转换
  • 将转换后的 UserVO 收集到新列表中返回

2.2 BeanUtils

BeanUtils(项目基于 Hutool 的 BeanUtil 封装)通过反射复制字段,适合简单场景。

  • 优点
    • 简单易用:字段名一致时无需额外配置。
    • 封装增强:支持 List、Page 转换,允许 Consumer 自定义逻辑。
    • 易替换:封装一层便于切换实现(如换成 Spring 的 BeanUtils)。
  • 缺点
    • 反射导致性能略低于 MapStruct。
    • 不适合复杂字段映射。
  • 适用场景:简单映射、快速开发。
public class UserConvertBeanUtils {

    /**
     * 将用户数据对象(UserDO)和部门数据对象(DeptDO)转换为用户视图对象(UserVO)
     * 

* 转换逻辑: * 1. 先通过 BeanUtil 将 UserDO 自动转换为 UserVO(自动映射 id、name 字段) * 2. 手动将 DeptDO 的 name 字段赋值给 UserVO 的 deptName 字段 * * @param user 用户数据对象(包含 id、name、deptId) * @param dept 部门数据对象(包含 id、name,可为 null) * @return 转换后的用户视图对象(UserVO),若 dept 为 null 则 deptName 为 null */ public static UserVO convert(UserDO user, DeptDO dept) { // 1. 自动转换 UserDO -> UserVO(映射 id、name 字段) UserVO userVO = BeanUtil.toBean(user, UserVO.class); // 2. 手动填充部门名称(若部门对象不为 null) if (dept != null) { userVO.setDeptName(dept.getName()); // 将 DeptDO 的 name 赋值给 UserVO 的 deptName } return userVO; } /** * 扩展转换方法:支持在转换后对 UserVO 进行自定义处理 *

* 转换逻辑: * 1. 继承 convert() 方法的逻辑(自动映射 + 手动填充部门名称) * 2. 通过 Consumer 对转换后的 UserVO 进行额外处理(如属性校验、补充数据等) * * @param user 用户数据对象 * @param dept 部门数据对象(可为 null) * @param consumer 自定义处理器,用于对 UserVO 进行后处理(如 set 其他属性) * @return 处理后的用户视图对象 */ public static UserVO convertWithConsumer(UserDO user, DeptDO dept, Consumer consumer) { // 先执行基础转换(自动映射 + 部门名称填充) UserVO userVO = convert(user, dept); // 调用自定义处理器(若 consumer 不为 null) if (consumer != null) { consumer.accept(userVO); // 将 UserVO 传入处理器进行额外处理 } return userVO; } /** * 将用户数据对象列表转换为用户视图对象列表 *

* 转换逻辑: * 1. 遍历用户列表,逐个调用 convert() 方法进行转换 * 2. 通过部门 ID(deptId)从部门映射表(deptMap)中获取对应的 DeptDO 对象 * * @param users 用户数据对象列表(不可为 null) * @param deptMap 部门映射表(key=部门 ID,value=DeptDO 对象) * @return 转换后的用户视图对象列表 */ public static List convertList(List users, Map deptMap) { return users.stream() // 将列表转换为流 .map(user -> // 对每个用户对象,根据 deptId 从 deptMap 中获取部门对象,再调用 convert() 转换 convert(user, deptMap.get(user.getDeptId())) ) .collect(Collectors.toList()); // 收集结果为列表 } }

输入:
UserDO user = new UserDO(1L, "Alice", 10L);
DeptDO dept = new DeptDO(10L, "工程部");
Map deptMap = new HashMap<>();
deptMap.put(10L, new DeptDO(10L, "工程部"));
List users = Arrays.asList(user);
代码:
// 简单场景
UserVO userVO = UserConvertBeanUtils.convert(user, dept);
// 复杂场景(添加额外字段)
UserVO userVOWithConsumer = UserConvertBeanUtils.convertWithConsumer(user, dept, vo -> vo.setDeptName("自定义-" + vo.getDeptName()));
// 列表转换
List userVOs = UserConvertBeanUtils.convertList(users, deptMap);
输出:
// userVO: {id=1, name="Alice", deptName="工程部"}
// userVOWithConsumer: {id=1, name="Alice", deptName="自定义-工程部"}
// userVOs: [{id=1, name="Alice", deptName="工程部"}]
  • BeanUtil.toBean 使用反射复制字段,适合字段名一致的场景。
  • Consumer 提供灵活性,允许自定义字段处理。
  • convertList 通过 Stream API 实现列表转换。

性能对比:MapStruct 性能优于 BeanUtils,但相比数据库操作的耗时,差距可忽略。因此,简单场景推荐 BeanUtils,复杂场景推荐 MapStruct。

3. 数据翻译:SQL 联表 vs Java 拼接 vs easy-trans

数据翻译是将一个字段(如 deptId)转换为另一个字段(如 deptName)。项目提供三种方案:

  1. SQL 联表查询:通过 MyBatis 的关联查询直接获取目标字段(如 DeptDO.name)。
  2. Java 拼接:多次单表查询(如先查 UserDO,再查 DeptDO),在 Java 代码中拼接。
  3. easy-trans 框架:通过注解(如 @Trans)自动翻译字段。

推荐:优先使用 Java 拼接(方案二)或 easy-trans(方案三),因为:

  • 减少数据库压力:避免复杂的 SQL 联表查询。
  • 易维护:Java 代码逻辑清晰,SQL 改动成本高。
  • 灵活性:easy-trans 提供注解式翻译,简化开发。

3.1easy-trans

public class OperateLogRespVO implements VO {
    private Long id; // 操作日志ID
    private Long userId; // 用户ID,关联AdminUserDO的id字段

    // @Trans注解:声明该字段需要从其他对象转换而来
    @Trans(type = "SIMPLE", target = AdminUserDO.class, fields = "nickname", ref = "userNickname")
    private String userNickname; // 存储从AdminUserDO映射过来的nickname字段

    // getter和setter方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Long getUserId() { return userId; }
    public void setUserId(Long userId) { this.userId = userId; }
    public String getUserNickname() { return userNickname; }
    public void setUserNickname(String userNickname) { this.userNickname = userNickname; }
}

public class AdminUserDO {
    private Long id; // 用户ID
    private String nickname; // 用户昵称

    // 无参构造函数
    public AdminUserDO() {}
    
    // 全参构造函数
    public AdminUserDO(Long id, String nickname) {
        this.id = id;
        this.nickname = nickname;
    }

    // getter和setter方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getNickname() { return nickname; }
    public void setNickname(String nickname) { this.nickname = nickname; }
}
输入:
OperateLogRespVO logVO = new OperateLogRespVO();
logVO.setId(1L);
logVO.setUserId(1L);
// 假设数据库中 AdminUserDO(1L, "Alice") 存在
代码:
// Spring MVC 自动翻译(easy-trans 全局配置)
return logVO;
输出:
// logVO: {id=1, userId=1, userNickname="Alice"}

实现原理

  • OperateLogRespVO 实现 VO 接口,启用 easy-trans 翻译。
  • @Trans(type = "SIMPLE", target = AdminUserDO.class, fields = "nickname", ref = "userNickname"):
    • type = "SIMPLE":使用 MyBatis Plus 查询 AdminUserDO。
    • target:指定目标实体类。
    • fields:读取 nickname 字段。
    • ref:设置到 userNickname 字段。
  • easy-trans 自动根据 userId 查询 AdminUserDO,填充 userNickname。

3.2 场景二:跨模块翻译(easy-trans)

场景:在 yudao-module-crm 模块的 CrmProductRespVO 中,将 ownerUserId 翻译为 AdminUserDO 的 nickname。

/**
 * CRM产品响应视图对象
 * 用于封装产品信息并返回给前端,包含产品基本信息和关联的所有者用户昵称
 */
public class CrmProductRespVO implements VO {
    private Long id; // 产品ID,唯一标识一个产品
    
    private Long ownerUserId; // 产品所有者的用户ID,关联到AdminUserDO的id字段
    
    /**
     * 产品所有者的昵称,通过@Trans注解自动映射
     * 映射规则:
     * - type="SIMPLE":简单类型映射
     * - targetClassName:指定目标数据对象类的全限定名
     * - fields="nickname":从AdminUserDO中获取nickname字段的值
     * - ref="ownerNickname":将获取的值映射到当前类的ownerNickname字段
     */
    @Trans(type = "SIMPLE", targetClassName = "com.example.model.AdminUserDO", fields = "nickname", ref = "ownerNickname")
    private String ownerNickname; // 存储从AdminUserDO映射过来的nickname字段值

    // 以下是各字段的Getter和Setter方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public Long getOwnerUserId() { return ownerUserId; }
    public void setOwnerUserId(Long ownerUserId) { this.ownerUserId = ownerUserId; }
    
    public String getOwnerNickname() { return ownerNickname; }
    public void setOwnerNickname(String ownerNickname) { this.ownerNickname = ownerNickname; }
}
输入:
OperateLogRespVO logVO = new OperateLogRespVO();
logVO.setId(1L);
logVO.setUserId(1L);
// 假设数据库中 AdminUserDO(1L, "Alice") 存在
代码:
// Spring MVC 自动翻译(easy-trans 全局配置)
return logVO;
输出:
// logVO: {id=1, userId=1, userNickname="Alice"}

3.3 场景三:Excel 导出翻译(easy-trans)

场景:导出 UserVO 列表到 Excel,翻译 deptId 为 deptName。

/**
 * 用户数据导出工具类
 * 负责生成用户数据并进行数据转换,用于Excel导出
 */
public class UserExcelExport {
    /**
     * 导出用户列表数据
     * 
     * 1. 创建用户数据
     * 2. 调用TranslateUtils进行数据转换(将部门ID转换为部门名称)
     * 3. 返回转换后的用户列表,用于Excel导出
     * 
     * @return 转换后的用户视图对象列表
     */
    public List exportUsers() {
        // 创建单个用户数据(实际场景可能从数据库查询)
        UserVO user = new UserVO();
        user.setId(1L);         // 设置用户ID
        user.setName("Alice");  // 设置用户名
        user.setDeptId(10L);    // 设置部门ID(关联DeptDO的ID)
        
        // 构建用户列表
        List users = Arrays.asList(user);
        
        // 调用工具类进行数据转换
        // 此方法会根据@Trans注解,将deptId转换为对应的部门名称deptName
        TranslateUtils.translate(users);
        
        // 返回转换后的用户列表,此时列表中的deptName已被填充
        return users;
    }
}
/####################################################################/

/**
 * 用户视图对象
 * 用于前端展示或数据导出,包含部门名称(通过@Trans注解自动映射)
 */
public class UserVO implements VO {
    private Long id;         // 用户ID
    private String name;     // 用户名称
    private Long deptId;     // 部门ID(关联DeptDO的ID)

    /**
     * 部门名称(通过@Trans注解自动映射)
     * 
     * type="SIMPLE": 简单类型转换
     * target=DeptDO.class: 目标数据对象类
     * fields="name": 从DeptDO中获取name字段
     * ref="deptName": 将值映射到当前类的deptName字段
     * 
     * TranslateUtils会根据此注解,
     * 通过deptId查找对应的DeptDO对象,
     * 并将其name字段值赋给当前对象的deptName字段
     */
    @Trans(type = "SIMPLE", target = DeptDO.class, fields = "name", ref = "deptName")
    private String deptName; // 部门名称(通过@Trans自动映射)

    // 以下是各字段的Getter和Setter方法
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public Long getDeptId() { return deptId; }
    public void setDeptId(Long deptId) { this.deptId = deptId; }
    
    public String getDeptName() { return deptName; }
    public void setDeptName(String deptName) { this.deptName = deptName; }
}
/**
 * VO 数据翻译 Utils
 */
public class TranslateUtils {

    private static TransService transService;

    public static void init(TransService transService) {
        TranslateUtils.transService = transService;
    }

    /**
     * 数据翻译
     *
     * 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译
     *
     * @param data 数据
     * @return 翻译结果
     */
    public static  List translate(List data) {
        if (CollUtil.isNotEmpty((data))) {
            transService.transBatch(data);
        }
        return data;
    }

}

4. 扩展:实用技巧与注意事项

4.1 优化性能

  • MapStruct:优先用于高并发场景,生成代码无反射开销。
  • BeanUtils:适合快速开发,但避免在高频接口中使用。
  • easy-trans:全局翻译(easy-trans.is-enable-global=true)方便但可能影响性能,建议在数据量大或树形结构时使用 @IgnoreTrans 注解:
    @IgnoreTrans
    public List getLargeData() {
        // 手动翻译或避免翻译
        return users;
    }

4.2 复杂逻辑处理

  • MapStruct 自定义逻辑:使用 @AfterMapping 或 default 方法处理复杂映射:
    @Mapper
    public interface UserConvert {
        @Mapping(target = "deptName", ignore = true)
        UserVO convert(UserDO user, DeptDO dept);
    
        @AfterMapping
        default void afterConvert(@MappingTarget UserVO userVO, DeptDO dept) {
            if (dept != null) {
                userVO.setDeptName("自定义-" + dept.getName());
            }
        }
    }

  • BeanUtils Consumer:通过 Consumer 添加动态逻辑:

  • UserVO userVO = UserConvertBeanUtils.convertWithConsumer(user, dept, vo -> {
        vo.setDeptName(vo.getDeptName() + "-增强");
    });

5.3 跨模块翻译优化

  • 缓存:跨模块查询(如 AdminUserDO)可能涉及多次数据库访问,建议缓存 DeptDO 或 AdminUserDO:
    Map deptMap = deptService.getDeptMap();
    List userVOs = users.stream()
        .map(user -> UserConvertBeanUtils.convert(user, deptMap.get(user.getDeptId())))
        .collect(Collectors.toList());

5.4 Excel 导出优化

  • 批量翻译:TranslateUtils.translate 支持批量处理,但大数据量时建议分批:
    List> batches = ListUtils.partition(users, 1000);
    batches.forEach(TranslateUtils::translate);

5. 总结

  • 对象转换
    • MapStruct:高性能,适合复杂映射,需配置 @Mapping。
    • BeanUtils:简单易用,适合字段名一致的场景,支持 Consumer 扩展。
  • 数据翻译
    • SQL 联表:适合简单场景,但可能增加数据库压力。
    • Java 拼接:灵活,推荐多次单表查询后拼接。
    • easy-trans:注解式翻译,模块内用 target,跨模块用 targetClassName,Excel 导出用 TranslateUtils。
  • 注意事项
    • 性能敏感场景用 MapStruct 或禁用全局翻译。
    • 复杂逻辑通过 default 方法或 Consumer 处理。
    • 跨模块翻译和 Excel 导出可结合缓存优化性能。

你可能感兴趣的:(Java每日进步,java,开发语言)