Pular para conteúdo

e. Repository

Agora, com o controller implementado, podemos passar para a camada de serviço do microsserviço Account. A camada de serviço é responsável pela lógica de negócio do microsserviço e interage com a camada de repositório para tratar a persistência dos dados da entidade Account. A camada de serviço é implementada na classe AccountService, que contém os métodos para criar, buscar (todas, ou uma pelo id) e excluir contas. A classe AccountService usa o AccountRepository para tratar a persistência dos dados da entidade Account e o AccountParser para tratar a conversão da entrada e da saída dos endpoints da API.

No hands-on anterior, já vimos que este microsserviço usa arquitetura limpa (clean architecture), o que significa que a camada de serviço é independente da camada de controller e da camada de repositório. Isso nos permite testar facilmente a lógica de negócio da camada de serviço sem nos preocuparmos com os detalhes da camada de controller ou da camada de repositório. O diagrama a seguir ilustra a arquitetura do microsserviço Account, mostrando as relações entre as diferentes camadas e classes:

sequenceDiagram
    title Abordagem da arquitetura limpa 
    Actor Request
    participant Controller as Controller<br>Resource
    participant Service@{ "type": "control" }
    participant Repository@{ "type" : "entity" }
    participant Database@{ "type" : "database" }
    Request ->>+ Controller: Rest API (JSON)
    Controller ->>+ Service: parser (AccountIn -> Account)
    Service ->>+ Repository: parser (Account -> AccountModel)
    Repository ->>+ Database: 
    Database ->>- Repository: 
    Repository ->>- Service: parser (Account <- AccountModel)
    Service ->>- Controller: parser (AccountOut <- Account)
    Controller ->>- Request: Rest API (JSON)

A relação entre as diferentes camadas é feita por meio de diferentes DTOs (Data Transfer Objects — objetos de transferência de dados). Para traduzir entre as camadas, usamos parsers, responsáveis por converter os DTOs em entidades e vice-versa.

  • A classe AccountParser é responsável por essa tradução e contém os métodos que convertem o DTO AccountIn na entidade Account e a entidade Account no DTO AccountOut.

  • Além disso, AccountModel é a classe que representa o modelo de banco de dados da entidade Account, usado pelo AccountRepository para tratar a persistência dos dados da entidade Account. A classe AccountModel contém os atributos da entidade Account e é anotada com anotações JPA, que definem o mapeamento entre a classe e a tabela do banco de dados.

1. O mapeamento objeto-relacional (ORM)

Este projeto usa um banco de dados relacional (PostgreSQL) para armazenar os dados da entidade Account. Porém, Java é uma linguagem de programação orientada a objetos, o que significa que precisamos de uma forma de mapear os dados relacionais para dados orientados a objetos.

O mapeamento objeto-relacional (ORM, Object-Relational Mapping) é uma técnica de programação que permite interagir com um banco de dados relacional usando uma linguagem de programação orientada a objetos. Ele mapeia tabelas do banco de dados para classes e registros para objetos, permitindo trabalhar com os dados de forma mais intuitiva e natural. No contexto do microsserviço Account, vamos usar um framework ORM, o JPA, para tratar a persistência dos dados da entidade Account, o que nos permitirá criar, ler, atualizar e excluir contas no banco de dados com facilidade.

Aqui podemos ver o código que resolve as questões de persistência de dados do microsserviço Account, incluindo a classe AccountModel, que representa o modelo de banco de dados da entidade Account, e a interface AccountRepository, responsável pela persistência dos dados da entidade Account usando JPA.

2. Migrações de banco de dados

Com o ORM configurado, precisamos gerenciar as mudanças no esquema do banco de dados. À medida que a aplicação evolui, podemos precisar adicionar novas tabelas, modificar tabelas existentes ou mudar os tipos de dados das colunas. Para gerenciar essas mudanças, vamos usar uma ferramenta de migração de banco de dados chamada Flyway1.

O Flyway é uma ferramenta de migração de banco de dados que permite gerenciar e versionar as mudanças no esquema do banco. Ele oferece uma forma de definir e executar migrações, que são scripts que modificam o esquema do banco de dados, como criar tabelas, adicionar colunas ou mudar tipos de dados. No contexto do microsserviço Account, vamos usar o Flyway para gerenciar as migrações, garantindo que o esquema do banco de dados esteja sempre atualizado e consistente entre os diferentes ambientes.

No código, temos um diretório db/migration, que contém os scripts de migração do microsserviço Account. Cada script de migração segue um formato específico de nome, começando por um número de versão (p. ex., V2026.08.24.001) seguido de uma descrição da migração (p. ex., create_schema). Esses scripts são executados em ordem pelo Flyway, garantindo que o esquema do banco de dados esteja sempre atualizado.

3. Código

Vamos codificar a implementação do microsserviço Account, que é composta por várias classes. A estrutura de diretórios resultante ficará assim:

📁 api/
├── 📁 account/
├── 📁 account-service/
│   ├── 📁 src/
│   │   └── 📁 main/
│   │       ├── 📁 java/
│   │       │   └── 📁 store/
│   │       │       └── 📁 account/
│   │       │           ├──  Account.java
│   │       │           ├──  AccountServiceApplication.java
│   │       │           ├──  AccountModel.java
│   │       │           ├──  AccountParser.java
│   │       │           ├──  AccountRepository.java
│   │       │           ├──  AccountResource.java
│   │       │           └──  AccountService.java
│   │       └── 📁 resources/
│   │           ├── 📁 db/
│   │           │   └── 📁 migration/
│   │           │       ├──  V2026.08.24.001__create_schema.sql
│   │           │       └──  V2026.08.24.002__create_table.sql
│   │           └── YAML application.yaml
│   └──  pom.xml
└── YAML compose.yaml

Respeitando a arquitetura limpa, temos as seguintes classes:

Classe Descrição
Account Esta classe representa a entidade Account, a entidade principal do microsserviço Account. Ela contém os atributos da entidade, como id, name, email, password (o valor original, só na entrada) e hashPassword.
AccountModel Esta classe é a entidade JPA mapeada para a tabela accounts.accounts (id, name, email, hash_password). Seu construtor AccountModel(Account) e seu método to() fazem a conversão entre a entidade e o modelo.
AccountParser Esta classe é responsável por converter a entrada e a saída dos endpoints da API, transformando os DTOs AccountIn e AccountOut na entidade Account e vice-versa.
AccountRepository Esta interface é responsável pela persistência dos dados da entidade Account, usando um framework de mapeamento objeto-relacional (ORM) para interagir com o banco de dados. Ela estende CrudRepository e redeclara findAll() para devolver uma List em vez de um Iterable.
AccountResource Esta classe é responsável pelos endpoints da API do microsserviço Account: ela implementa a interface AccountController definida no módulo account e usa o AccountService para tratar a lógica de negócio dos endpoints.
AccountService Esta classe é responsável pela lógica de negócio do microsserviço Account, usando o AccountRepository para tratar a persistência dos dados da entidade Account e o AccountParser para tratar a conversão da entrada e da saída dos endpoints da API.

Código-fonte

name: store

services:

  db:
    image: postgres:17
    volumes:
      - ${VOLUME_DB}:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER:-store}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-store}
      POSTGRES_DB: ${DB_NAME:-store}

  account:
    build:
      context: ./account-service
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_DB: ${DB_NAME:-store}
      DATABASE_USERNAME: ${DB_USER:-store}
      DATABASE_PASSWORD: ${DB_PASSWORD:-store}
    depends_on:
      - db
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
    </parent>
    <groupId>store</groupId>
    <artifactId>account-service</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>25</java.version>
        <spring-cloud.version>2025.1.3</spring-cloud.version>
        <maven.compiler.proc>full</maven.compiler.proc>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-flyway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-database-postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>store</groupId>
            <artifactId>account</artifactId>
            <version>1.0.0</version>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>
spring:
  application:
    name: account

  datasource:
    url: jdbc:postgresql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_DB}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
    driver-class-name: org.postgresql.Driver

  flyway:
    baseline-on-migrate: true
    schemas: accounts

  jpa:
    properties:
      hibernate:
        default_schema: accounts
package store.account;

import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;

@Builder @Data
@Accessors(fluent = true)
public class Account {

    private String id;
    private String name;
    private String email;
    private String password;
    private String hashPassword;

}
package store.account;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

@Entity
@Table(name = "accounts")
@Setter @Accessors(chain = true, fluent = true)
@NoArgsConstructor @AllArgsConstructor
public class AccountModel {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    @Column(name = "id")
    private String id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    @Column(name = "hash_password")
    private String hashPassword;

    public AccountModel(Account a) {
        this.id = a.id();
        this.name = a.name();
        this.email = a.email();
        this.hashPassword = a.hashPassword();
    }

    public Account to() {
        return Account.builder()
            .id(this.id)
            .name(this.name)
            .email(this.email)
            .hashPassword(this.hashPassword)
            .build();
    }

}
package store.account;

import java.util.List;

public final class AccountParser {

    public static Account to(AccountIn in) {
        return in == null ? null :
            Account.builder()
                .name(in.name())
                .email(in.email())
                .password(in.password())
                .build();
    }

    public static AccountOut to(Account a) {
        return a == null ? null :
            AccountOut.builder()
                .id(a.id())
                .name(a.name())
                .email(a.email())
                .build();
    }

    public static List<AccountOut> to(List<Account> la) {
        return la.stream()
            .map(AccountParser::to)
            .toList();
    }

}
package store.account;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface AccountRepository extends CrudRepository<AccountModel, String> {

    List<AccountModel> findAll();

}
package store.account;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

@Controller
public class AccountResource implements AccountController {

    @Autowired
    private AccountService accountService;

    @Override
    public ResponseEntity<Void> create(AccountIn in) {
        final String id = accountService.create(
            AccountParser.to(in)
        ).id();
        return ResponseEntity.created(
            ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(id)
                .toUri()
        ).build();
    }

    @Override
    public ResponseEntity<List<AccountOut>> findAll() {
        return ResponseEntity
            .ok(AccountParser.to(accountService.findAll()));
    }

    @Override
    public ResponseEntity<Void> delete(String id) {
        accountService.delete(id);
        return ResponseEntity.noContent().build();
    }

    @Override
    public ResponseEntity<Void> healthCheck() {
        return ResponseEntity
            .ok()
            .build();
    }

    @Override
    public ResponseEntity<AccountOut> findById(String id) {
        final Account a = accountService.findById(id);
        if (a == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        return ResponseEntity.ok(AccountParser.to(a));
    }

}
package store.account;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    public Account create(Account account) {

        if (null == account.password()) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST, "Password is empty"
            );            
        }

        final String pass = account.password().trim();
        if (pass.length() < 6) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST, "Password must have at least 6 characters"
            );
        }

        if (account.email() == null || account.email().trim().isEmpty()) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST, "Email is empty"
            );
        }

        // agora calcula o hash
        account.hashPassword(calcHash(pass));

        return accountRepository.save(
            new AccountModel(account)
        ).to();
    }

    public Account findById(String id) {
        // null when the id does not exist: AccountResource answers 404
        return accountRepository.findById(id)
            .map(AccountModel::to)
            .orElse(null);
    }

    public void delete(String id) {
        accountRepository.deleteById(id);
    }

    public List<Account> findAll() {
        return accountRepository.findAll().stream()
            .map(AccountModel::to)
            .toList();
    }

    private String calcHash(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(text.getBytes(StandardCharsets.UTF_8));
            byte[] digest = md.digest();
            return Base64.getEncoder().encodeToString(digest);
        } catch (NoSuchAlgorithmException e) {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }

}
CREATE SCHEMA IF NOT EXISTS accounts;
1
2
3
4
5
6
CREATE TABLE accounts.accounts (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(256) NOT NULL,
    email VARCHAR(256) NOT NULL UNIQUE,
    hash_password VARCHAR(256) NOT NULL
);

As regras de negócio ficam no AccountService:

  • create responde 400 Bad Request quando a senha está ausente ou tem menos de 6 caracteres, ou quando o email está vazio; caso contrário, guarda apenas o hash SHA-256 (em Base64) da senha, em hash_password — nunca a própria senha.
  • findById devolve null para um id inexistente, e o AccountResource transforma isso em 404 Not Found.

4. Execução

Para executar o microsserviço Account, podemos usar o comando docker-compose para iniciar o microsserviço e as suas dependências (p. ex., o banco de dados PostgreSQL). Certifique-se de estar no diretório raiz do projeto, onde fica o arquivo compose.yaml, e execute o seguinte comando:

docker compose up --build

Observe que a flag --build é usada para gerar as imagens Docker antes de iniciar os containers, o que é necessário se você fez mudanças no código ou no Dockerfile.

Também podemos monitorar os logs do microsserviço para ver se ele está rodando corretamente. Observe que as migrações do Flyway estão sendo executadas e que o microsserviço está subindo sem erros. Você deve ver logs indicando que o microsserviço está no ar e conectado ao banco de dados. Além disso, você também pode verificar o banco de dados para conferir se as migrações foram executadas corretamente e se as tabelas foram criadas como esperado.

  1. Entrando no banco de dados PostgreSQL. O psql fica no container db (a imagem postgres), não no do account; por isso, abra-o lá, com o nome do serviço do compose.yaml e o usuário/banco definidos por DB_USER e DB_NAME (ambos store):

    Entering the PostgreSQL database
    docker compose exec db psql -U store -d store
    
  2. Verificando as migrações:

    List the schemas
    \dn
    
    List the tables
    \dt *.*
    

E, se você quiser monitorar os logs do microsserviço sem entrar no container, pode usar o comando docker compose logs com o nome do serviço (account) para ver os logs do container. Assim você acompanha os logs em tempo real e pode monitorar a inicialização do microsserviço, bem como quaisquer erros ou problemas que surjam.

Monitoring the logs
docker compose logs -f account

Pronto!