DEV Community

Uiratan Cavalcante
Uiratan Cavalcante

Posted on

Protegendo Invariantes com Jakarta Validation

O que são invariantes?

Invariantes são regras ou condições que devem ser sempre verdadeiras para um objeto ou sistema, independentemente das operações realizadas. Por exemplo:

  • Em uma classe Usuario, um invariante pode ser que o nome não seja nulo e tenha entre 2 e 100 caracteres.
  • Outro invariante pode ser que a idade seja maior ou igual a 18.

Proteger as invariantes é essencial para garantir a consistência e a integridade dos dados em um sistema.


Jakarta Validation e Invariantes

O Jakarta Validation (anteriormente Bean Validation) é uma especificação que permite definir regras de validação usando anotações como @NotNull, @Size, @Email, @Min, etc. Ele é amplamente usado para garantir que os dados respeitem as invariantes.

Como o Jakarta Validation ajuda?

  1. Validação declarativa: Você define as regras diretamente nos campos ou métodos da classe usando anotações.
  2. Integração com frameworks: Frameworks como Spring, Quarkus e Jakarta EE integram automaticamente o Jakarta Validation, validando os dados em pontos específicos do ciclo de vida da aplicação (como em APIs REST ou ao persistir dados com JPA).
  3. Customização: Você pode criar validadores personalizados usando a interface ConstraintValidator.

Garantindo que um objeto seja criado em estado consistente

Para garantir que um objeto da classe Usuario seja criado em um estado consistente, você pode adotar as seguintes práticas:


1. Usar um construtor que valide as invariantes

O construtor deve garantir que os valores passados respeitem as invariantes. Se algum valor for inválido, o construtor deve lançar uma exceção. Aqui, podemos usar a classe Assert para simplificar as validações.

Exemplo com Assert:
import org.springframework.util.Assert;

public class Usuario {
    private final String nome;
    private final String email;
    private final int idade;

    public Usuario(String nome, String email, int idade) {
        // Validações usando Assert
        Assert.notNull(nome, "O nome não pode ser nulo.");
        Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
        Assert.notNull(email, "O email não pode ser nulo.");
        Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
        Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");

        this.nome = nome;
        this.email = email;
        this.idade = idade;
    }

    // Getters (sem setters para imutabilidade)
    public String getNome() {
        return nome;
    }

    public String getEmail() {
        return email;
    }

    public int getIdade() {
        return idade;
    }
}
Enter fullscreen mode Exit fullscreen mode
Vantagens:
  • Código mais limpo e expressivo.
  • Lança exceções específicas (IllegalArgumentException) com mensagens claras.
  • Ideal para validações simples e diretas.
Desvantagens:
  • Depende de uma biblioteca externa (como Spring). Se não estiver usando Spring, você pode criar sua própria classe Assert.

2. Usar o padrão Builder com Assert

O padrão Builder é útil quando você tem muitas propriedades para validar ou quer fornecer uma API mais fluente para a criação de objetos. Podemos usar Assert para validar cada propriedade.

Exemplo:
import org.springframework.util.Assert;

public class Usuario {
    private final String nome;
    private final String email;
    private final int idade;

    private Usuario(Builder builder) {
        this.nome = builder.nome;
        this.email = builder.email;
        this.idade = builder.idade;
    }

    // Getters
    public String getNome() {
        return nome;
    }

    public String getEmail() {
        return email;
    }

    public int getIdade() {
        return idade;
    }

    // Builder
    public static class Builder {
        private String nome;
        private String email;
        private int idade;

        public Builder nome(String nome) {
            Assert.notNull(nome, "O nome não pode ser nulo.");
            Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
            this.nome = nome;
            return this;
        }

        public Builder email(String email) {
            Assert.notNull(email, "O email não pode ser nulo.");
            Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
            this.email = email;
            return this;
        }

        public Builder idade(int idade) {
            Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");
            this.idade = idade;
            return this;
        }

        public Usuario build() {
            return new Usuario(this);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Uso:
Usuario usuario = new Usuario.Builder()
    .nome("João Silva")
    .email("joao.silva@example.com")
    .idade(25)
    .build();
Enter fullscreen mode Exit fullscreen mode
Vantagens:
  • Flexibilidade para definir propriedades de forma fluente.
  • Validação feita em cada etapa, garantindo consistência.

3. Usar Jakarta Validation no construtor

Se você estiver usando o Jakarta Validation, pode validar o objeto após a criação no construtor. Aqui, podemos combinar Assert com o Jakarta Validation para garantir consistência.

Exemplo:
import jakarta.validation.*;
import jakarta.validation.constraints.*;
import org.springframework.util.Assert;
import java.util.Set;

public class Usuario {

    @NotNull(message = "O nome não pode ser nulo")
    @Size(min = 2, max = 100, message = "O nome deve ter entre 2 e 100 caracteres")
    private final String nome;

    @NotNull(message = "O email não pode ser nulo")
    @Email(message = "O email deve ser válido")
    private final String email;

    @Min(value = 18, message = "A idade deve ser maior ou igual a 18")
    private final int idade;

    public Usuario(String nome, String email, int idade) {
        // Validações simples com Assert
        Assert.notNull(nome, "O nome não pode ser nulo.");
        Assert.notNull(email, "O email não pode ser nulo.");
        Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");

        this.nome = nome;
        this.email = email;
        this.idade = idade;

        // Validação com Jakarta Validation
        validar();
    }

    private void validar() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<Usuario>> violations = validator.validate(this);

        if (!violations.isEmpty()) {
            StringBuilder sb = new StringBuilder("Erros de validação:\n");
            for (ConstraintViolation<Usuario> violation : violations) {
                sb.append("- ").append(violation.getMessage()).append("\n");
            }
            throw new IllegalArgumentException(sb.toString());
        }
    }

    // Getters
    public String getNome() {
        return nome;
    }

    public String getEmail() {
        return email;
    }

    public int getIdade() {
        return idade;
    }
}
Enter fullscreen mode Exit fullscreen mode
Vantagens:
  • Combina validações simples com Assert e validações complexas com Jakarta Validation.
  • Centraliza a validação em um único método.

4. Imutabilidade

Tornar a classe Usuario imutável (sem métodos setters) garante que, uma vez criado, o objeto não possa ser alterado para um estado inconsistente.

Exemplo:
public class Usuario {
    private final String nome;
    private final String email;
    private final int idade;

    public Usuario(String nome, String email, int idade) {
        // Validações com Assert
        Assert.notNull(nome, "O nome não pode ser nulo.");
        Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
        Assert.notNull(email, "O email não pode ser nulo.");
        Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
        Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");

        this.nome = nome;
        this.email = email;
        this.idade = idade;
    }

    // Getters (sem setters para imutabilidade)
    public String getNome() {
        return nome;
    }

    public String getEmail() {
        return email;
    }

    public int getIdade() {
        return idade;
    }
}
Enter fullscreen mode Exit fullscreen mode
Vantagens:
  • Evita que o objeto seja modificado após a criação.
  • Garante consistência durante todo o ciclo de vida do objeto.

Quando a classe Usuario é uma @Entity (ou seja, uma entidade JPA), há considerações adicionais a serem feitas para garantir que as invariantes sejam protegidas e que a validação seja integrada ao ciclo de vida da persistência. Vou adaptar as práticas anteriores para o contexto de uma entidade JPA, incluindo o uso de Assert, Jakarta Validation, e a integração com o ciclo de vida do JPA.


Protegendo Invariantes em uma Entidade JPA

Considerações Importantes

  1. Validação Automática com JPA:

    • O JPA (Java Persistence API) pode integrar-se automaticamente com o Jakarta Validation para validar entidades antes de persistir no banco de dados.
    • Se uma entidade violar as regras de validação, o JPA lançará uma exceção (ConstraintViolationException).
  2. Ciclo de Vida da Entidade:

    • A validação ocorre automaticamente durante operações como persist, merge, e remove.
    • Você pode usar o @PrePersist e @PreUpdate para adicionar validações personalizadas.
  3. Uso de Assert:

    • A classe Assert pode ser usada para validações programáticas dentro dos métodos da entidade.
  4. Imutabilidade:

    • Em entidades JPA, a imutabilidade total nem sempre é prática, pois o JPA requer métodos setters para atualizar o estado. No entanto, você pode proteger campos críticos (como IDs) para evitar alterações inconsistentes.

Exemplo Completo: Classe Usuario como @Entity

Aqui está um exemplo completo de uma classe Usuario como entidade JPA, com validações usando Jakarta Validation, Assert, e métodos de ciclo de vida.

Código:

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import org.springframework.util.Assert;
import java.util.Objects;

@Entity
public class Usuario {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull(message = "O nome não pode ser nulo")
    @Size(min = 2, max = 100, message = "O nome deve ter entre 2 e 100 caracteres")
    @Column(nullable = false, length = 100)
    private String nome;

    @NotNull(message = "O email não pode ser nulo")
    @Email(message = "O email deve ser válido")
    @Column(nullable = false, unique = true, length = 100)
    private String email;

    @Min(value = 18, message = "A idade deve ser maior ou igual a 18")
    @Column(nullable = false)
    private int idade;

    // Construtor padrão (obrigatório para JPA)
    protected Usuario() {}

    // Construtor principal
    public Usuario(String nome, String email, int idade) {
        this.nome = nome;
        this.email = email;
        this.idade = idade;
        validar(); // Validação manual
    }

    // Getters e Setters (obrigatórios para JPA)
    public Long getId() {
        return id;
    }

    protected void setId(Long id) { // Setter protegido para evitar alterações externas
        this.id = id;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        Assert.notNull(nome, "O nome não pode ser nulo.");
        Assert.isTrue(nome.length() >= 2 && nome.length() <= 100, "O nome deve ter entre 2 e 100 caracteres.");
        this.nome = nome;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        Assert.notNull(email, "O email não pode ser nulo.");
        Assert.isTrue(email.matches("^[A-Za-z0-9+_.-]+@(.+)$"), "O email deve ser válido.");
        this.email = email;
    }

    public int getIdade() {
        return idade;
    }

    public void setIdade(int idade) {
        Assert.isTrue(idade >= 18, "A idade deve ser maior ou igual a 18.");
        this.idade = idade;
    }

    // Método de validação manual
    private void validar() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        var violations = validator.validate(this);

        if (!violations.isEmpty()) {
            StringBuilder sb = new StringBuilder("Erros de validação:\n");
            for (var violation : violations) {
                sb.append("- ").append(violation.getMessage()).append("\n");
            }
            throw new IllegalArgumentException(sb.toString());
        }
    }

    // Métodos de ciclo de vida (opcional)
    @PrePersist
    @PreUpdate
    private void antesDePersistirOuAtualizar() {
        validar(); // Validação automática antes de persistir ou atualizar
    }

    // Equals e HashCode (opcional, mas recomendado para entidades)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Usuario usuario = (Usuario) o;
        return Objects.equals(id, usuario.id); // Compara apenas o ID
    }

    @Override
    public int hashCode() {
        return Objects.hash(id); // Usa apenas o ID
    }

    @Override
    public String toString() {
        return "Usuario{" +
                "id=" + id +
                ", nome='" + nome + '\'' +
                ", email='" + email + '\'' +
                ", idade=" + idade +
                '}';
    }
}
Enter fullscreen mode Exit fullscreen mode

Explicação dos Pontos-Chave

  1. Validação com Jakarta Validation:

    • As anotações como @NotNull, @Size, @Email, e @Min são usadas para definir as regras de validação.
    • O JPA valida automaticamente a entidade antes de persistir ou atualizar no banco de dados.
  2. Uso de Assert:

    • A classe Assert é usada nos métodos setters para garantir que os valores sejam válidos antes de atribuí-los aos campos.
  3. Métodos de Ciclo de Vida:

    • O método @PrePersist e @PreUpdate é usado para chamar a validação manual antes de persistir ou atualizar a entidade.
  4. Imutabilidade Parcial:

    • O campo id tem um setter protegido para evitar alterações externas.
    • Os setters públicos validam os dados antes de atribuí-los.
  5. Validação Manual:

    • O método validar() usa o Validator do Jakarta Validation para verificar as anotações e lançar exceções em caso de violações.
  6. Equals e HashCode:

    • São implementados com base no campo id, que é a chave primária da entidade.

Como Funciona na Prática

Criando um Usuário Válido:

Usuario usuario = new Usuario("João Silva", "joao.silva@example.com", 25);
// O objeto é criado e validado com sucesso.
Enter fullscreen mode Exit fullscreen mode

Tentando Criar um Usuário Inválido:

try {
    Usuario usuario = new Usuario(null, "email-invalido", 15);
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage());
}
// Saída:
// Erros de validação:
// - O nome não pode ser nulo
// - O email deve ser válido
// - A idade deve ser maior ou igual a 18
Enter fullscreen mode Exit fullscreen mode

Persistindo com JPA:

EntityManager em = ...; // Obtém o EntityManager
em.getTransaction().begin();
em.persist(usuario); // A validação ocorre automaticamente antes de persistir
em.getTransaction().commit();
Enter fullscreen mode Exit fullscreen mode

Conclusão

Para garantir que um objeto seja criado em um estado consistente:

  1. Use Assert para validações simples e diretas.
  2. Use o padrão Builder para objetos complexos.
  3. Combine Jakarta Validation com Assert para validações avançadas.
  4. Torne a classe imutável para evitar alterações inconsistentes após a criação.

Essas práticas garantem que as invariantes sejam respeitadas, mantendo a consistência e a integridade dos dados em seu sistema.

Ao trabalhar com uma entidade JPA:

  1. Use Jakarta Validation para definir regras de validação declarativas.
  2. Use Assert para validações programáticas em métodos setters.
  3. Integre a validação ao ciclo de vida da entidade com @PrePersist e @PreUpdate.
  4. Proteja campos críticos (como o ID) para evitar alterações inconsistentes.

Essas práticas garantem que a entidade esteja sempre em um estado consistente, tanto no domínio da aplicação quanto no banco de dados.

Top comments (0)