分层模型(VO / DTO / QO / Domain / Entity / DAO)

Spring Boot + MyBatis 项目中的标准数据对象分层,每一层职责明确,互不耦合。


各对象一览

对象全称方向核心职责
DTOData Transfer Object前端 → 后端(写)接收新增/修改参数,做校验
QOQuery Object前端 → 后端(读)接收查询/筛选参数
VOView Object后端 → 前端返回给前端的展示数据
EntityPersistent ObjectService ↔ Mapper与数据库表一一对应的纯数据载体
DomainDomain ObjectService 内部携带业务语义和行为(可选)

分层流转图

前端

  写操作: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 按需引入,简单项目省掉也完全正确

相关