既然Entity里什么字段都有,我直接用不就行了,为什么还要费劲写DTO和VO?

张开发
2026/4/21 15:51:17 15 分钟阅读

分享文章

既然Entity里什么字段都有,我直接用不就行了,为什么还要费劲写DTO和VO?
后端开发中最经典、最标准的分层命名规范。简单总结就是Entity数据库表结构一张表对应一个 EntityDTO前端传回来的数据Data Transfer ObjectVO返回给前端的数据View Object1. Entity映射数据库表java// com.sky.entity.Dish.java Data Builder NoArgsConstructor AllArgsConstructor public class Dish { private Long id; // 对应表字段 id private String name; // 对应表字段 name private BigDecimal price; // 对应表字段 price private Long categoryId; // 对应表字段 category_id private Integer status; // 对应表字段 status private LocalDateTime createTime; // 对应表字段 create_time private LocalDateTime updateTime; // 对应表字段 update_time }特点和数据库表字段一一对应包含所有字段。2. DTO接收前端参数前端添加菜品时传过来的数据可能只有部分字段json{ name: 宫保鸡丁, price: 38.00, categoryId: 101, flavors: [微辣, 中辣, 特辣] // 额外字段不在 dish 表中 }java// com.sky.dto.DishDTO.java Data public class DishDTO { private String name; // 菜品名称 private BigDecimal price; // 价格 private Long categoryId; // 分类ID private ListString flavors; // 口味列表dish 表中没有这个字段 }特点可能少于Entity 字段不需要传 id、status、时间等可能多于Entity 字段如 flavors 需要拆分存到其他表字段名、类型可以不完全一致前端传什么就定义什么Controller 接收参数javaPostMapping(/add) public Result addDish(RequestBody DishDTO dishDTO) { // dishDTO 接收前端传过来的数据 }3. VO返回给前端查询菜品详情时返回给前端的数据可能需要额外信息json{ id: 1, name: 宫保鸡丁, price: 38.00, categoryName: 川湘菜, // 关联查询出来的分类名称 status: 1, flavors: [微辣, 中辣] // 从口味表查出来的数据 }java// com.sky.vo.DishVO.java Data public class DishVO { private Long id; private String name; private BigDecimal price; private String categoryName; // 分类名称Entity 中没有 private Integer status; private ListString flavors; // 口味列表Entity 中没有 }特点包含 Entity 的部分字段可以添加额外字段关联查询的数据、计算得出的数据等只返回前端需要的数据不返回敏感字段Service 中构造 VOjavapublic DishVO getDishDetail(Long id) { // 1. 查询 dish 表 Dish dish dishMapper.selectById(id); // 2. 查询 category 表获取分类名称 String categoryName categoryMapper.selectNameById(dish.getCategoryId()); // 3. 查询 flavor 表获取口味列表 ListString flavors flavorMapper.selectByDishId(id); // 4. 组装成 VO DishVO vo new DishVO(); BeanUtils.copyProperties(dish, vo); // 复制相同字段 vo.setCategoryName(categoryName); vo.setFlavors(flavors); return vo; }Entity 数据库表的结构一张表一个 EntityDTO 前端传过来的数据前端→后端VO 返回给前端的数据后端→前端这是目前 Java 后端开发中最成熟、最广泛使用的分层规范遵循这个规范可以让代码更清晰、更易维护很多初学者甚至中级工程师都会有的困惑“既然Entity里什么字段都有我直接用不就行了为什么还要费劲写DTO和VO”答案是技术上完全可以但长期维护上会“埋雷”。打个比方Entity就像你的身份证包含了你的所有信息姓名、身份证号、住址、民族、血型...。DTO/VO就像你去不同地方办业务时填的专用表格。你当然可以走到哪儿都掏出身份证让工作人员自己从上面找他要的信息。但这会带来什么问题呢一、直接用 Entity 会引发的问题1. 接口返回了不该返回的数据安全风险你的User实体里可能有这些字段javaEntity public class User { private Long id; private String username; private String password; // 密码 private String email; private String phone; private BigDecimal balance; // 余额 private LocalDateTime createTime; private LocalDateTime updateTime; }如果查询用户信息的接口直接返回User实体javaGetMapping(/user/{id}) public User getUser(PathVariable Long id) { return userService.getById(id); }前端收到的 JSON 会包含password、balance等敏感信息虽然前端可能不展示但用户打开浏览器控制台就能看到甚至可以通过抓包获取。用 VO 解决只返回需要的字段。javaData public class UserVO { private Long id; private String username; private String email; private String phone; }2. 前端要求的数据格式和数据库不一样数据库里存的是create_time字段值是2024-01-15T10:30:00。但前端想要的是格式化成2024年01月15日多返回一个isToday字段表示是否是今天创建的不返回update_time如果直接用User实体你没法控制这些。只能在 Controller 或 Service 里手动处理代码会很乱。用 VO 解决VO 可以自由定义字段类型和格式。javaData public class UserVO { private Long id; private String username; private String createDate; // 格式化成 2024年01月15日 private Boolean isToday; // 额外计算的字段 // 没有 update_time }3. 前端传的参数和数据库字段对不上前端添加用户时传的参数json{ userName: 张三, // 前端用 userName数据库是 username pwd: 123456, // 前端用 pwd数据库是 password confirmPwd: 123456 // 确认密码数据库根本没有这个字段 }如果直接用User实体接收会发生userName不知道映射到username报错pwd不知道映射到password报错confirmPwd在User中不存在直接报错用 DTO 解决DTO 可以定义前端想要的字段名然后手动映射。javaData public class UserRegisterDTO { private String userName; // 前端字段名 private String pwd; private String confirmPwd; } // 在 Service 中手动转换 User user new User(); user.setUsername(userRegisterDTO.getUserName()); user.setPassword(userRegisterDTO.getPwd()); // confirmPwd 只做校验不存数据库4. 接口变了但你不小心影响了其他接口你有一个User实体被 10 个不同的接口复用。某天接口 A 的需求变了需要在返回的 JSON 里多加一个vipLevel字段。如果你直接在User实体上加这个字段其他 9 个不想要这个字段的接口也会返回vipLevel如果vipLevel需要从其他表查询其他 9 个接口也被迫执行了额外的查询性能下降用 VO 解决只给接口 A 单独建一个UserVipVO加vipLevel字段其他接口完全不受影响。二、什么时候可以直接用 Entity虽然说了这么多问题但有些简单场景下直接用 Entity 是可以接受的场景是否可用说明内部系统、不对外暴露✅ 可用安全风险低追求开发效率增删改操作且参数完全匹配✅ 可用比如修改状态前端只传id和status简单的查询接口字段完全匹配✅ 可用比如根据 ID 查询返回所有字段对外的开放 API❌ 不可用必须严格控制返回字段涉及敏感数据❌ 不可用密码、余额、手机号等必须脱敏或隐藏需要额外关联数据❌ 不可用Entity 里加关联字段很恶心接口会被多个前端调用❌ 不可用Web、App、小程序可能需求不同三、真实项目的折中方案实际开发中没有人会傻到为每个接口都建一个 DTO/VO。通常的做法是方案1继承体系前面提过java// 基础 Entity public class User { private Long id; private String username; private String password; // 敏感 private String email; private String phone; private Integer status; } // 对外返回的 VO继承 Entity但过滤敏感字段 Data public class UserVO extends User { JsonIgnore // 这个字段不返回给前端 private String password; }方案2通用 DTO 专用 DTOjava// 通用分页参数所有分页接口复用 Data public class PageDTO { private Integer page 1; private Integer pageSize 10; } // 专用参数继承通用参数 Data public class OrderQueryDTO extends PageDTO { private Long userId; private Integer status; private String keyword; }方案3使用 Map 但必须加注释不推荐但确实存在有些老项目或超简单的接口直接用MapString, Object接收和返回。但这种方式完全失去了类型安全调试困难不推荐。四、总结对比维度直接用 Entity用 DTO/VO开发速度✅ 快不用建新类❌ 慢一点代码量✅ 少❌ 多安全性❌ 容易泄露敏感数据✅ 可控灵活性❌ 受限于表结构✅ 自由定义可维护性❌ 牵一发动全身✅ 互不影响扩展性❌ 加字段影响所有接口✅ 只影响需要的接口最终建议小型项目、内部工具、Demo直接用 Entity快速开发优先。正式项目、对外 API、多人协作坚持用 DTO/VO用规范换未来的维护成本。一句话记住Entity 是数据库的表结构DTO/VO 是接口的合同。合同一旦签了接口发布了改起来很麻烦所以要用 DTO/VO 把数据库和接口解耦。

更多文章