在 Spring 开发中,将 DTO(数据传输对象)映射到 Entity(实体类)是常见需求,特别是在分层架构中(如 Controller、Service、Repository)。以下是基于 UserEntity
, RoleEntity
, UserDTO
, UserController
, UserService
, 和 UserRepository
的示例,展示几种常用的 DTO 到 Entity 映射方式,包括手动映射、Spring 的 BeanUtils
、以及 MapStruct 等工具的使用场景和实现。
示例背景
假设我们有以下类:
- UserEntity:用户实体类
- RoleEntity:角色实体类
- UserDTO:用户数据传输对象
- UserController:控制器
- UserService:服务层
- UserRepository:数据访问层
1. 数据模型定义
数据模型是关系数据库表到Java代码的映射。
UserEntity
import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
@Data
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
private String email;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<RoleEntity> roles = new HashSet<>();
private LocalDateTime createdAt;
}
RoleEntity
import lombok.Data;
import javax.persistence.*;
@Entity
@Table(name = "roles")
@Data
public class RoleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
private String description;
}
2. 传输数据模型定义
传输数据模型是来自前端界面用户输入的数据到Java代码的映射。
UserDTO
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.util.Set;
@Data
public class UserDTO {
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
private String email;
private Set<String> roles; // 角色名称集合
}
3. 常用映射方式
方法 1:手动映射
手动映射是最简单直接的方式,适合字段较少或需要自定义逻辑的场景。
UserService 示例
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public UserEntity createUser(UserDTO userDTO) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername(userDTO.getUsername());
userEntity.setPassword(passwordEncoder.encode(userDTO.getPassword())); // 加密密码
userEntity.setEmail(userDTO.getEmail());
userEntity.setCreatedAt(LocalDateTime.now());
// 处理角色(假设角色已存在数据库)
if (userDTO.getRoles() != null && !userDTO.getRoles().isEmpty()) {
Set<RoleEntity> roles = new HashSet<>();
for (String roleName : userDTO.getRoles()) {
RoleEntity role = new RoleEntity();
role.setName(roleName);
roles.add(role); // 这里简化,实际应从数据库查询
}
userEntity.setRoles(roles);
}
return userRepository.save(userEntity);
}
}
优点:
- 完全控制映射逻辑。
- 适合复杂业务逻辑。
缺点:
- 代码量多,容易出错,维护成本高。
方法 2:使用 Spring 的 BeanUtils
Spring 的 BeanUtils
可以快速拷贝相同字段名的属性,减少手动赋值的工作量。
UserService 示例
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public UserEntity createUser(UserDTO userDTO) {
UserEntity userEntity = new UserEntity();
BeanUtils.copyProperties(userDTO, userEntity); // 拷贝相同字段名的属性
// 处理密码加密和额外字段
userEntity.setPassword(passwordEncoder.encode(userDTO.getPassword()));
userEntity.setCreatedAt(LocalDateTime.now());
// 处理角色
if (userDTO.getRoles() != null && !userDTO.getRoles().isEmpty()) {
Set<RoleEntity> roles = new HashSet<>();
for (String roleName : userDTO.getRoles()) {
RoleEntity role = new RoleEntity();
role.setName(roleName);
roles.add(role); // 实际应查询数据库
}
userEntity.setRoles(roles);
}
return userRepository.save(userEntity);
}
}
优点:
- 减少手动赋值的代码量。
- 简单易用。
缺点:
- 仅支持字段名完全匹配的拷贝。
- 无法处理复杂映射(如
Set<String>
到Set<RoleEntity>
)。
方法 3:使用 MapStruct
MapStruct 是目前最推荐的方式,它通过注解生成高效的映射代码,支持复杂映射和自定义逻辑。
3.1 添加 MapStruct 依赖
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
3.2 定义映射接口 (UserMapper.java)
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.stream.Collectors;
@Mapper(componentModel = "spring")
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(target = "password", source = "password") // 密码需额外处理
@Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())")
@Mapping(target = "roles", source = "roles", qualifiedByName = "mapRoles")
UserEntity toEntity(UserDTO userDTO);
@Mapping(target = "roles", source = "roles", qualifiedByName = "mapRoleNames")
UserDTO toDto(UserEntity userEntity);
@Named("mapRoles")
default Set<RoleEntity> mapRoles(Set<String> roleNames) {
if (roleNames == null) return null;
return roleNames.stream()
.map(name -> {
RoleEntity role = new RoleEntity();
role.setName(name);
return role; // 实际应从数据库查询
})
.collect(Collectors.toSet());
}
@Named("mapRoleNames")
default Set<String> mapRoleNames(Set<RoleEntity> roles) {
if (roles == null) return null;
return roles.stream()
.map(RoleEntity::getName)
.collect(Collectors.toSet());
}
}
3.3 UserService 示例
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
public UserDTO createUser(UserDTO userDTO) {
UserEntity userEntity = userMapper.toEntity(userDTO);
userEntity.setPassword(passwordEncoder.encode(userDTO.getPassword())); // 加密密码
UserEntity savedEntity = userRepository.save(userEntity);
return userMapper.toDto(savedEntity);
}
}
优点:
- 自动生成映射代码,减少手动工作。
- 支持复杂映射和自定义逻辑。
- 与 Spring 集成良好,可注入依赖。
缺点:
- 需额外引入依赖和学习成本。
4. 完整示例
UserRepository
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity, Long> {
}
UserController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
UserDTO createdUser = userService.createUser(userDTO);
return ResponseEntity.ok(createdUser);
}
}
注意事项
- 角色处理:上述示例中角色直接创建为新对象,实际开发中应从数据库查询已有角色(如通过
RoleRepository
)。 - 密码加密:始终在服务层对密码进行加密处理。
- 双向映射:MapStruct 支持 Entity 到 DTO 的反向映射,方便返回数据。
5. 选择建议
方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
手动映射 | 小型项目或复杂逻辑 | 灵活,易调试 | 代码冗余,维护成本高 |
BeanUtils | 字段名一致的简单映射 | 简单,代码少 | 不支持复杂映射 |
MapStruct | 中大型项目,复杂映射 | 高效,自动生成,支持扩展 | 需学习和配置依赖 |
推荐:对于中小型项目,BeanUtils
结合少量手动逻辑即可;对于大型项目或需要长期维护的系统,强烈推荐使用 MapStruct,因其扩展性和可维护性更强。