НейроАгент

Преобразование DTO в словарь: где делать в архитектуре

Узнайте, где правильно преобразовывать DTO в словарь перед сохранением в БД в многослойной архитектуре. Лучшие практики маппинга DTO и разделения ответственности между слоями.

В каком слое архитектуры (роут, сервис, репо) следует преобразовывать DTO объект в словарь перед сохранением в базу данных? Каковы лучшие практики для маппинга DTO в модели БД в многослойной архитектуре и почему?

НейроАгент

Преобразование DTO в словарь перед сохранением в базу данных должно происходить в сервисном слое, а не в репозитории или роутере. Лучшие практики многослойной архитектуры рекомендуют четкое разделение ответственности, где репозитории отвечают только за операции с базой данных, сервисы - за бизнес-логику и преобразование данных, а роутеры/контроллеры - за обработку запросов и ответов.

Содержание

Преобразование DTO в модели БД: где это делать?

Репозиторий - это антипаттерн для преобразования DTO в объекты сохранения. Репозитории должны быть сфокусированы исключительно на операциях с базой данных, абстрагируя детали реализации persistence-механизмов. Как указано в Microsoft Learn, репозитории разделяют зависимость между доменом и хранением данных, но не должны заниматься преобразованием DTO.

Репозитории должны хранить состояние Агрегатов, а не делиться состоянием Агрегатов с Презентационным слоем - Nick Chamberlain

Сервисный слой является правильным местом для преобразования DTO в модели домена перед сохранением. Сервисы инкапсулируют бизнес-логику и должны отвечать за преобразование данных между различными представлениями. Как объясняют эксперты на StackOverflow, “controller должен знать service, service должен знать repository”, что означает单向ную зависимость вниз по слоям.


Роли слоев в архитектуре

Роутер/Контроллер слой

  • Обрабатывает входящие HTTP запросы
  • Валидирует входные данные
  • Вызывает сервисные методы с DTO
  • Преобразует DTO сервисного слоя в HTTP-ответы
  • Не должен преобразовывать DTO в модели домена

Сервисный слой

  • Содержит бизнес-логику
  • Преобразует DTO в модели домена и обратно
  • Вызывает репозитории для операций с базой данных
  • Управляет транзакциями
  • Ключевая ответственность: преобразование данных между слоями

Репозиторий слой

  • Абстрагирует операции с базой данных
  • Работает напрямую с сущностями домена
  • Предоставляет методы CRUD для агрегатов
  • Не должен знать о существовании DTO
  • Сохраняет только доменные модели, а не словари

Лучшие практики маппинга DTO

1. Четкое разделение ответственности

Каждый слой должен иметь свою область ответственности:

  • DTO - для передачи данных между слоями
  • Domain Models - для бизнес-логики
  • Database Entities - для сохранения в БД

Как указывают эксперты InfoQ, DTO emerged as a versatile tool for seamless data transfer between layers and adaptability to various data models.

2. Использование DTO Assembler

DTO Assembler - это специальный класс/паттерн для преобразования доменных моделей в DTO и обратно:

typescript
class UserAssembler {
  toDTO(user: User): UserDTO {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
      // только необходимые поля для презентации
    };
  }

  fromDTO(dto: UserDTO): User {
    return new User(dto.id, dto.name, dto.email);
  }
}

3. Автоматизированное маппинг

Используйте инструменты автоматизации маппинга:

  • Mapperly для C#
  • MapStruct для Java
  • Automapper для .NET
  • class-transformer для TypeScript

4. Многоуровневое преобразование

В сложных системах может потребоваться несколько уровней преобразования:

HTTP DTO → Service DTO → Domain Model → Database Entity

Паттерны преобразования данных

1. Прямое преобразование в сервисе

typescript
// В сервисе
class UserService {
  createUser(userData: CreateUserDTO): UserDTO {
    const user = new User(
      null, // ID будет сгенерирован БД
      userData.name,
      userData.email
    );
    
    const savedUser = this.userRepository.save(user);
    return this.userAssembler.toDTO(savedUser);
  }
}

2. Использование маппера

typescript
// Отдельный маппер
class UserMapper {
  toEntity(dto: UserDTO): User {
    return new User(dto.id, dto.name, dto.email);
  }
  
  toDomain(entity: UserEntity): User {
    return new User(entity.id, entity.name, entity.email);
  }
}

3. Конвейер преобразования

typescript
// Pipeline для сложных преобразований
class ConversionPipeline {
  async convertToDomain(dto: UserDTO): Promise<User> {
    const intermediate = this.firstStage(dto);
    const processed = this.secondStage(intermediate);
    return this.thirdStage(processed);
  }
}

Примеры реализации

Пример с NestJS (TypeScript)

typescript
// Controller
@Controller('users')
export class UsersController {
  constructor(private readonly userService: UserService) {}

  @Post()
  async createUser(@Body() createUserDto: CreateUserDto): Promise<UserDto> {
    return this.userService.createUser(createUserDto);
  }
}

// Service
@Injectable()
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly userMapper: UserMapper
  ) {}

  async createUser(createUserDto: CreateUserDto): Promise<UserDto> {
    const user = this.userMapper.toDomain(createUserDto);
    const savedUser = await this.userRepository.save(user);
    return this.userMapper.toDto(savedUser);
  }
}

// Repository
@Injectable()
export class UserRepository {
  constructor(private readonly dataSource: DataSource) {}

  async save(user: User): Promise<User> {
    const userEntity = this.userMapper.toEntity(user);
    const saved = await this.userRepository.save(userEntity);
    return this.userMapper.toDomain(saved);
  }
}

Пример с .NET (C#)

csharp
// Controller
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;
    
    public UsersController(IUserService userService)
    {
        _userService = userService;
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateUser([FromBody] CreateUserDto dto)
    {
        var user = await _userService.CreateUserAsync(dto);
        return Ok(user);
    }
}

// Service
public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;
    private readonly IMapper _mapper;
    
    public UserService(IUserRepository userRepository, IMapper mapper)
    {
        _userRepository = userRepository;
        _mapper = mapper;
    }
    
    public async Task<UserDto> CreateUserAsync(CreateUserDto dto)
    {
        var user = _mapper.Map<User>(dto);
        await _userRepository.SaveAsync(user);
        return _mapper.Map<UserDto>(user);
    }
}

// Repository
public class UserRepository : IUserRepository
{
    private readonly DbContext _context;
    
    public UserRepository(DbContext context)
    {
        _context = context;
    }
    
    public async Task SaveAsync(User user)
    {
        var entity = _mapper.Map<UserEntity>(user);
        _context.Users.Add(entity);
        await _context.SaveChangesAsync();
    }
}

Частые ошибки и как их избежать

1. Преобразование в репозитории

typescript
// Плохо: Репозиторий знает о DTO
class UserRepository {
  async save(dto: UserDTO): Promise<void> {
    const user = this.mapper.fromDTO(dto); // Нарушение SRP
    // Сохранение...
  }
}

2. Прямое сохранение DTO

typescript
// Плохо: Сохранение DTO в базу
class UserService {
  async save(dto: UserDTO): Promise<void> {
    await this.repository.save(dto); // Нарушение инкапсуляции
  }
}

3. Смешение слоев

typescript
// Плохо: Контроллер работает с сущностями
class UserController {
  async create(): Promise<void> {
    const user = new User(); // Прямое создание сущности
    await this.service.save(user);
  }
}

4. Отсутствие преобразования

typescript
// Плохо: Прямое использование DTO во всех слоях
class UserService {
  async save(dto: UserDTO): Promise<UserDTO> {
    return this.repository.save(dto); // Смешение слоев
  }
}

Заключение

  1. Преобразование DTO должно происходить в сервисном слое, а не в репозитории или контроллере, чтобы обеспечить четкое разделение ответственности.

  2. Репозитории должны оставаться чистыми - они должны работать только с доменными сущностями и абстрагировать детали реализации базы данных.

  3. Используйте специализированные инструменты для автоматизации маппинга (Mapperly, MapStruct, Automapper) чтобы уменьшить количество шаблонного кода.

  4. DTO Assembler - мощный паттерн для централизации логики преобразования между доменными моделями и DTO.

  5. Соблюдайте单向ную зависимость между слоями: контроллер → сервис → репозиторий, чтобы избежать циклических зависимостей и обеспечить тестируемость системы.

Следуя этим практикам, вы создадите чистую, поддерживаемую и легко тестируемую многослойную архитектуру, где каждый слой выполняет свою специфическую роль, что критически важно для долгосрочного развития корпоративных приложений.

Источники

  1. A Better Way to Project Domain Entities into DTOs · Nick Chamberlain
  2. Designing the infrastructure persistence layer - .NET | Microsoft Learn
  3. Which layer should be used for conversion to DTO from Domain Object - Stack Overflow
  4. Architecting with Java Persistence: Patterns and Strategies - InfoQ
  5. Building a Layered Architecture in NestJS & Typescript: Repository Pattern, DTOs, and Validators | Medium
  6. Repository Pattern with Layered Architecture, dotnet | Medium
  7. architectural patterns - Three layer architecture and using DTOs to transfer data between layers - Software Engineering Stack Exchange
  8. Implementing DTOs, Mappers & the Repository Pattern using the Sequelize ORM [with Examples] - DDD w/ TypeScript | Khalil Stemmler