野生工程师探索后端开发日志(一)——DTO、VO与数据层级

2 阅读7分钟

涉及知识点

mindmap
      后端结构
          实体类
            DTO
            VO
    

标准数据流层级

在标准的 Java 后端开发(如 Spring Boot 项目)中,从 Controller 接收请求到最终执行 SQL 语句,数据流通常遵循经典的三层架构

完整的数据流层级与调用链路如下:

前端请求 ➡️ Controller层 ➡️ Service层 ➡️ Mapper层 ➡️ SQL/数据库

以下是每一层的具体职责与数据流转细节:

1. Controller 层(控制层 / 表现层)-接收请求参数,返回响应数据

  • 职责:它是整个数据流的“入口”。负责接收前端(或 Postman 等工具)发送的 HTTP 请求,解析请求中的参数(如 URL 路径参数、请求体 JSON 等),并将这些参数传递给 Service 层。
  • 数据流动作:Controller 接收到请求后,不处理具体的业务逻辑,而是直接调用 Service 层的方法。最后,它会将 Service 层返回的最终结果,包装成统一的格式(如包含状态码、提示信息、数据的 JSON 对象)响应给前端。

2. Service 层(业务逻辑层)——处理业务,调用Mapper

  • 职责:由service和对应impl组成,前者是方法声明(Interface),后者是方法实现。一般调用mapper(涉及数据处理),避免调用其他模块mapper(绕开业务逻辑直接从数据库获取是不明智的,容易打破业务逻辑完整性,造成幽灵数据),调用其他service(注意防止循环调用)。它是数据流的“大脑”。负责处理具体的业务逻辑、参数校验、权限判断以及事务控制(确保一连串的数据库操作要么全部成功,要么全部失败)。
  • 数据流动作:Service 层接收 Controller 传来的原始数据,进行业务规则的处理(例如判断用户名是否重复、计算金额等)。处理完毕后,它会调用 Mapper 层的方法,将需要持久化的数据传下去,或者获取数据库中的数据并进行二次加工,最后返回给 Controller。

3.Mapper 层(数据访问层 / 持久层)-执行数据库操作,负责数据存取

  • 职责:由mapper和对应xml组成,前者是方法声明(Interface),后者是方法实现。它是数据流的“搬运工”。专门负责与数据库打交道,不包含任何业务逻辑
  • 数据流动作:这一层通常是一个接口(Interface),它定义了增、删、改、查等抽象方法。Service 层调用这些接口方法时,底层框架(如 MyBatis)会将其映射为具体的数据库操作。

4. SQL 执行(数据库交互)

  • 职责:数据流的“终点”。
  • 数据流动作:在 Mapper 层背后,通常对应着一个 XML 配置文件或注解,里面写有真实的 SQL 语句(如 SELECT * FROM user WHERE id = ?)。框架会将 Service 层传递下来的参数填入 SQL 语句中,发送给数据库执行。数据库执行完毕后,将结果集返回给 Mapper 层,再原路向上层返回。

DTO和VO的应用场景

在数据流转的过程中,通常还会伴随着实体类(Entity/domain) 的传递。Entity 主要负责和数据库打交道, 数据跨层传输采取DTO,前端展示采取VO,DTO(Data Transfer Object)、VO(View Object) 就是来辅助实体类传递的,完成数据怎么存、怎么传、怎么显,简单项目可以直接采取 实体类。

所处文件结构

com/company/project 

├── Application.java # 项目启动类
├── config # 【配置层】存放各种配置类(如数据库、Redis、拦截器配置等) 
├── controller # 【控制层】对外提供 RESTful 接口,接收请求并返回响应 
├── service # 【业务逻辑层】核心业务处理 
│ ├── impl # Service 接口的具体实现类 
├── mapper # 【持久层 / DAO层】直接和数据库交互(MyBatis 接口) 
├── model # 【数据模型层】存放各类数据对象(DTO/VO/Entity等) 
│ ├── entity # 数据库实体类,与数据库表结构一一对应 
│ ├── dto # 数据传输对象,用于接收前端请求参数 
│ ├── vo # 视图展示对象,用于封装返回给前端的数据 
│ ├── bo # 业务对象,用于 Service 层内部复杂业务数据的流转 
│ ├── query # 查询参数对象,专门用于封装分页、筛选等复杂查询条件 
│ └── enums # 全局枚举类(如订单状态、性别等) 
├── exception # 【异常处理层】自定义业务异常类 
├── handler # 全局异常处理器(统一拦截异常并返回友好提示) 
├── constant # 【常量层】存放项目中的静态常量(如 Redis Key、业务常量) 
├── utils # 【工具层】存放各种通用工具类(如日期、加密、字符串工具) 
└── task / job # 【任务层】存放定时任务逻辑(如每日数据统计)

怎么用DTO和VO

在应用示例中,设计一个经典的电商场景: “用户订单详情页” 。在这个场景中,前端需要展示的数据不仅包含用户的基础信息,还包含该用户的历史订单列表以及订单总金额统计

1. 基础实体类 (Entity)

首先,我们需要两个与数据库表一一对应的实体类:UserEntity(用户表)和 OrderEntity(订单表)。

// 用户实体类 
@Data 
@TableName("t_user") 
public class UserEntity { 
private Long id; 
private String username; 
private String password; // 敏感字段,绝对不能暴露给前端 
private String email; 
private LocalDateTime createTime; 
private Integer status; // 数据库存的是数字状态码,如 1-正常, 2-冻结 
}


// 订单实体类
@Data
@TableName("t_order")
public class OrderEntity {
private Long orderId;
private Long userId;
private BigDecimal amount; // 订单金额
private String orderNo;
private LocalDateTime createTime;
}

2. 数据传输对象 (DTO)

DTO 的核心作用是跨层传输。在 Service 层,我们需要把从不同表排查出来的UserEntityList<OrderEntity>聚合在一起,打包传给 Controller 层。

// 用户订单聚合 DTO(Service层内部流转使用) 
@Data 
public class UserOrderDTO {
// 来自 UserEntity 的数据 
private Long userId;
private String username; 
private String email; // 在内部流转时,可以保留原始数据
private Integer userStatus; // 原始状态码 

// 来自 OrderEntity 的聚合数据 
private List<OrderEntity> orderList;// 直接携带订单实体列表进行内部传递

// 业务统计字段 
private BigDecimal totalOrderAmount; // 订单总金额(业务层计算得出)
private Integer totalOrderCount; // 订单总数
}

3. 视图展示对象 (VO)

VO 的核心作用是适配前端展示。它必须对敏感数据进行脱敏,对格式进行美化,并且只保留前端页面真正需要的字段。

// 订单简要信息 VO(嵌套在用户VO中) 
@Data 
public class OrderBriefVO {
private String orderNo; 
private String formattedAmount; // 格式化后的金额,如 "¥199.00" 
private String orderDate; // 格式化后的日期,如 "2026-06-01" 
} 

// 用户订单详情 VO(最终返回给前端) 
@Data
public class UserOrderVO {
private Long userId;
private String username; // 【数据脱敏】邮箱中间部分打码,如 "a****@gmail.com" 
private String maskedEmail; // 【状态转换】将数字状态码转为前端直接展示的文本 
private String userStatusText; // 如 "正常" 或 "已冻结" 

// 【格式化数据】前端直接渲染,无需再次处理 
private String formattedTotalAmount; // 如 "总计消费:¥5,999.00" 
private String totalOrderCountDesc; // 如 "共 12 笔订单" 

// 订单列表(使用专门定制的VO,而不是直接把OrderEntity扔给前端)
private List<OrderBriefVO> orders; 
}

4. Service 层与 Controller 层调用示例

// ================= Service 层 ================= 
@Service
public class UserOrderService {
    @Autowired private UserMapper userMapper;
    @Autowired private OrderMapper orderMapper;


    // 返回 DTO,负责把多个实体类的数据聚合起来 
    public UserOrderDTO getUserOrderDetail(Long userId) { 
    // 1. 分别查询用户和订单的 Entity
    UserEntity user = userMapper.selectById(userId);
    List<OrderEntity> orders = orderMapper.selectList(new QueryWrapper<OrderEntity>().eq("user_id", userId)); 
    
    // 2. 组装 DTO
    UserOrderDTO dto = new UserOrderDTO();
    dto.setUserId(user.getId());
    dto.setUsername(user.getUsername());
    dto.setEmail(user.getEmail());
    dto.setUserStatus(user.getStatus());
    dto.setOrderList(orders); 
    
    // 3. 计算业务统计数据
    dto.setTotalOrderCount(orders.size());
    BigDecimal totalAmount = orders.stream() .map(OrderEntity::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add);
    dto.setTotalOrderAmount(totalAmount);
    
    return dto;  // 将打包好的 DTO 交给 Controller
    } 
} 

// ================= Controller 层 ================= 
@RestController
@RequestMapping("/api/users")
public class UserOrderController {
    @Autowired private UserOrderService userOrderService;
    
    @GetMapping("/{userId}/orders") 
    
    public Result<UserOrderVO> getUserOrders(@PathVariable Long userId) {
        // 1. 调用 Service 获取聚合的 DTO
        UserOrderDTO dto = userOrderService.getUserOrderDetail(userId);
        
        // 2. 将 DTO 转换为适配前端的 VO 
        UserOrderVO vo = new UserOrderVO();
        vo.setUserId(dto.getUserId());
        vo.setUsername(dto.getUsername());
        
        // 【脱敏处理】 
        vo.setMaskedEmail(maskEmail(dto.getEmail())); 
        
        // 【格式化与状态转换】 
        vo.setUserStatusText(dto.getUserStatus() == 1 ? "正常" : "已冻结"); 
        vo.setFormattedTotalAmount("总计消费:¥" + dto.getTotalOrderAmount().toPlainString()); 
        vo.setTotalOrderCountDesc("共 " + dto.getTotalOrderCount() + " 笔订单"); 
        
        // 转换订单列表 
        List<OrderBriefVO> orderVOList = dto.getOrderList().stream().map(order -> { 
        OrderBriefVO briefVO = new OrderBriefVO(); briefVO.setOrderNo(order.getOrderNo()); 
        briefVO.setFormattedAmount("¥" + order.getAmount().toPlainString()); 
        briefVO.setOrderDate(order.getCreateTime().toLocalDate().toString());
        return briefVO;
        }).collect(Collectors.toList());
        vo.setOrders(orderVOList);
        
        // 3. 返回最终给前端的 VO
        return Result.success(vo);
        } 
        
        // 简单的邮箱脱敏工具方法 
        private String maskEmail(String email) {
            if (email == null || !email.contains("@")) return email; String[] parts = 
            email.split("@");
            return parts[0].charAt(0) + "****@" + parts[1];
            }
            
    }