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. Ela é implementada na classe AccountService, que contém os métodos para criar, excluir, buscar e atualizar 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.

Nos hands-on anteriores, já vimos que este microsserviço usa a 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 das camadas de controller ou 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 Clean Architecture
    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)

As relações entre as diferentes camadas são feitas por meio de diferentes DTOs (Data Transfer Objects). 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 para converter o DTO AccountIn na entidade Account, e a entidade Account no DTO AccountOut.

  • Já o 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. 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. Mas 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 (Object-Relational Mapping, ORM) é uma técnica de programação que permite aos desenvolvedores interagir com um banco de dados relacional usando uma linguagem orientada a objetos. Ele oferece uma forma de mapear tabelas do banco para classes e registros do banco para objetos, permitindo trabalhar com os dados de um jeito mais intuitivo e natural. No contexto do microsserviço Account, vamos usar um framework de ORM, o JPA, para tratar a persistência dos dados da entidade Account, o que vai 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, pode ser necessário 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, como criar tabelas, adicionar colunas ou mudar tipos de dados. No contexto do microsserviço Account, vamos usar o Flyway para gerenciar as nossas migrações, garantindo que o esquema do banco 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 é nomeado em um formato específico, começando com um número de versão (por exemplo, V2026.02.27.001) seguido de uma descrição da migração (por exemplo, create_schema). Esses scripts de migração são executados em ordem pelo Flyway, para garantir que o esquema do banco esteja sempre atualizado.

3. Código

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

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

Respeitando a Clean Architecture, 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 e sha256.
AccountModel Esta classe representa o modelo Account, responsável pela lógica de persistência do microsserviço Account. Ela contém os métodos para criar, excluir, buscar e atualizar contas.
AccountParser Esta classe é responsável pela conversão da entrada e da saída dos endpoints da API, convertendo 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.
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-api

services:

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

  account:
    build:
      context: ./account-service
      dockerfile: Dockerfile
    hostname: account
    ports:
      - 8080:8080
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_DB: ${DB_NAME:-store}
      DATABASE_USERNAME: ${DB_USER:-store}
      DATABASE_PASSWORD: ${DB_PASSWORD:-devpass}
    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.0.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>store</groupId>
    <artifactId>account-service</artifactId>
    <version>1.0.0</version>
    <name>account-service</name>

    <properties>
        <java.version>25</java.version>
        <spring-cloud.version>2025.1.0</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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

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

        <dependency>
            <groupId>store</groupId>
            <artifactId>account</artifactId>
            <version>${project.version}</version>
        </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>

    </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.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
server:
  port: 8080

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;

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

    private String id;
    private String name;
    private String email;
    private String password;
    private String passwordSha256;

}
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 = "password_sha256")
    private String passwordSha256;

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

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

}
package store.account;

import java.util.List;

public class AccountParser {

    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> l) {
        return l.stream().map(AccountParser::to).toList();
    }

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

}
1
2
3
4
5
6
7
package store.account;

import org.springframework.data.repository.CrudRepository;

public interface AccountRepository extends CrudRepository<AccountModel, String>  {

}
package store.account;

import java.net.URI;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

@RestController
public class AccountResource implements AccountController {

    @Autowired
    private AccountService accountService;

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

    @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<List<AccountOut>> findAll() {
        return ResponseEntity.ok(
            AccountParser.to(
                accountService.findByAll()
            )
        );
    }

    @Override
    public ResponseEntity<AccountOut> findById(String id) {
        Account out = accountService.findById(id);
        return out == null ?
            ResponseEntity.notFound().build() :
            ResponseEntity.ok(
                AccountParser.to(out) // transform from account to ou
            );
    }

}
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 java.util.stream.StreamSupport;

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 (account.password() == null || account.password().trim().length() == 0) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Password is empty");
        }

        account.passwordSha256(calcHash(account.password()));

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

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

    public Account findById(String id) {
        return accountRepository.findById(id).orElse(null).to();
    }

    public List<Account> findByAll() {
        return StreamSupport.stream(
            accountRepository.findAll().spliterator(),
            false // transform from stream to list
        ).map(AccountModel::to) // parser from Model to Account
        .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
CREATE TABLE accounts.accounts (
    id VARCHAR(36) PRIMARY KEY,
    name VARCHAR(256) NOT NULL,
    email VARCHAR(256) NOT NULL UNIQUE
);
ALTER TABLE accounts.accounts
ADD COLUMN password_sha256 VARCHAR(64) NOT NULL;

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 (por exemplo, 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 fazer o build das imagens Docker antes de iniciar os containers, o que é necessário se você alterou o código ou o Dockerfile.

Também podemos acompanhar 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 container:

    Entering the container
    docker exec -it account-service bash
    
  2. Entrando no banco de dados PostgreSQL:

    Entering the PostgreSQL database
    psql -U store -d store
    
  3. Verificando as migrações:

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

E, se você quiser acompanhar os logs do microsserviço sem entrar no container, pode usar o comando docker logs para ver os logs do container. Isso permite ver os logs em tempo real e acompanhar o processo de inicialização do microsserviço, bem como quaisquer erros ou problemas que possam surgir.

Monitoring the logs
docker logs -f account-service

Pronto!