分层模型(VO / DTO / QO / Domain / Entity / DAO)
Spring Boot + MyBatis 项目中的标准数据对象分层,每一层职责明确,互不耦合。
各对象一览
| 对象 | 全称 | 方向 | 核心职责 |
|---|---|---|---|
| DTO | Data Transfer Object | 前端 → 后端(写) | 接收新增/修改参数,做校验 |
| QO | Query Object | 前端 → 后端(读) | 接收查询/筛选参数 |
| VO | View Object | 后端 → 前端 | 返回给前端的展示数据 |
| Entity | Persistent Object | Service ↔ Mapper | 与数据库表一一对应的纯数据载体 |
| Domain | Domain Object | Service 内部 | 携带业务语义和行为(可选) |
分层流转图
前端
写操作:POST/PUT → DTO → Service → Entity → DB
查询操作:GET → QO → Service → Entity → DB
返回:DB → Entity → (→ Domain →) → VO → 前端
可选
DTO — 写操作参数
接收新增、修改等写操作的前端参数,重点是参数校验。
// 新增用户
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20)
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6)
private String password;
}
// 修改用户
public class UserUpdateDTO {
@NotNull
private Long id;
@Size(min = 3, max = 20)
private String username; // null 表示不修改
}QO — 查询参数
接收查询、搜索、分页等读操作的前端参数。
与 DTO 分开命名的原因:查询参数字段大多可选(不需要 @NotBlank),语义上也是”描述查什么”而非”提交什么数据”。
public class UserQO {
private String username; // 模糊搜索,可为 null
private Integer status; // 状态筛选,可为 null
@Min(1)
private Integer page = 1;
@Min(1) @Max(100)
private Integer pageSize = 20;
}Controller 中用 @Valid + 直接绑定(不需要 @RequestBody,GET 请求用 query string):
@GetMapping
public Result<PageResult<UserVO>> list(@Valid UserQO qo) {
return Result.ok(userService.listUsers(qo));
}
@PostMapping
public Result<Void> create(@Valid @RequestBody UserCreateDTO dto) {
userService.createUser(dto);
return Result.ok();
}Entity — 数据库映射对象
Entity 只做一件事:描述数据库表的结构。
MyBatis 下就是普通 POJO,字段与表列名对应,不包含任何业务逻辑:
// 对应数据库 user 表
public class UserEntity {
private Long id;
private String username;
private String password; // 密文,不暴露给前端
private Integer status; // 0=禁用 1=启用,存数字
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}Entity 的边界:
- 只在 Mapper ↔ Service 之间流动
- 字段完全跟着数据库走,不做格式转换
- 不暴露给 Controller
DAO / Mapper — 数据访问层
只执行 SQL,返回 Entity,不含任何业务判断。
Mapper 接口
@Mapper
public interface UserMapper {
UserEntity selectById(Long id);
UserEntity selectByUsername(String username);
List<UserEntity> selectList(UserQO qo); // QO 直接传入也可以,简单项目常见
long countByCondition(UserQO qo);
int insert(UserEntity user);
int updateById(UserEntity user);
int deleteById(Long id);
}查询条件直接复用 QO 还是另建内部 Query 对象,看团队约定。 简单项目直接传 QO;若 Mapper 的查询字段与前端参数差异较大(如需要 offset 换算),可另建。
Mapper XML
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.example.entity.UserEntity">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="status" property="status"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="selectList" resultMap="BaseResultMap">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
LIMIT #{pageSize} OFFSET #{(page - 1) * pageSize}
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(username, password, status, created_at)
VALUES(#{username}, #{password}, #{status}, #{createdAt})
</insert>
<update id="updateById">
UPDATE user
<set>
<if test="username != null">username = #{username},</if>
<if test="status != null">status = #{status},</if>
updated_at = NOW()
</set>
WHERE id = #{id}
</update>
</mapper>Domain — 业务对象(可选)
Domain 不是必须的,只在业务逻辑较复杂时引入。
何时需要 Domain:
- Entity 字段是数字/原始类型(
status=1),业务需要枚举(UserStatus.ACTIVE) - 业务对象聚合了多张表(如订单 = 订单表 + 订单项表)
- 需要在对象上定义业务方法(
user.canLogin())
何时可以省略:
- 简单 CRUD,Service 直接操作 Entity 即可
- 字段少、无复杂状态流转
// 引入 Domain 的例子:枚举 + 业务方法
public class UserDomain {
private Long id;
private String username;
private UserStatus status; // 枚举,不是数字
public boolean canLogin() {
return UserStatus.ACTIVE.equals(this.status);
}
}
public enum UserStatus {
ACTIVE(1, "启用"),
DISABLED(0, "禁用");
private final int code;
private final String label;
public static UserStatus fromCode(int code) {
for (UserStatus s : values()) {
if (s.code == code) return s;
}
throw new IllegalArgumentException("未知状态: " + code);
}
}VO — 返回给前端
按前端需要裁剪字段,做格式化,隐藏敏感信息。
public class UserVO {
private Long id;
private String username;
private String status; // 枚举/数字 → 展示文字 "启用"
private String createdAt; // LocalDateTime → "2024-01-01 12:00"
}完整示例:查询用户列表
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
// 查询列表(不使用 Domain,简单项目直接 Entity → VO)
public PageResult<UserVO> listUsers(UserQO qo) {
List<UserEntity> entities = userMapper.selectList(qo);
long total = userMapper.countByCondition(qo);
List<UserVO> vos = entities.stream()
.map(this::toVO)
.toList();
return new PageResult<>(vos, total, qo.getPage(), qo.getPageSize());
}
// 新增用户
public void createUser(UserCreateDTO dto) {
if (userMapper.selectByUsername(dto.getUsername()) != null) {
throw new BusinessException("用户名已存在");
}
UserEntity entity = new UserEntity();
entity.setUsername(dto.getUsername());
entity.setPassword(passwordEncoder.encode(dto.getPassword()));
entity.setStatus(1); // 默认启用
entity.setCreatedAt(LocalDateTime.now());
userMapper.insert(entity);
}
// Entity → VO(无 Domain 的简单转换)
private UserVO toVO(UserEntity entity) {
UserVO vo = new UserVO();
vo.setId(entity.getId());
vo.setUsername(entity.getUsername());
vo.setStatus(entity.getStatus() == 1 ? "启用" : "禁用");
vo.setCreatedAt(entity.getCreatedAt()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
return vo;
}
}转换链总结
写:DTO/QO → Entity → DB
读:DB → Entity → VO
读(复杂业务):DB → Entity → Domain → VO
核心原则:
- DTO 负责写,QO 负责查,职责分离
- Entity 只跟 Mapper 打交道,不出 Service 层
- VO 只跟 Controller 打交道,不进 Service 内部逻辑
- Domain 按需引入,简单项目省掉也完全正确