Check Constraints: O Constraint Mais Versátil do PostgreSQL

Se você desenvolve aplicações Rails há algum tempo, provavelmente já usou validações do Active Record como validates :preco, numericality: { greater_than: 0 }. Mas e se eu te contar que existe uma forma mais robusta de garantir a qualidade dos seus dados, especialmente em cenários de alta concorrência?

Neste tutorial, você vai aprender a usar check constraints do PostgreSQL — o tipo de constraint mais flexível e poderoso que todo desenvolvedor Rails intermediário deveria dominar.

Por Que Check Constraints São Importantes?

Imagine que você tem uma aplicação de reservas de veículos. Duas validações críticas:

  • A data de término deve ser posterior à data de início
  • A reserva deve ter no mínimo 30 minutos de duração

Você pode validar isso no Active Record, mas em cenários de alta concorrência, condições de corrida podem permitir que dados inválidos entrem no banco. Check constraints garantem isso no nível do banco de dados, onde a consistência é inquebrável.

O Que São Check Constraints?

Check constraints são regras que você define usando qualquer expressão SQL que retorne um valor booleano. Se a expressão retornar true, a operação é permitida. Se retornar false, PostgreSQL rejeita a mudança com um erro.

A melhor parte? Qualquer condição que você pode expressar em SQL pode ser um check constraint.

Exemplo 1: Consistência Cronológica

Vamos criar uma migration para garantir que viagens só podem ser concluídas depois de serem criadas:

# db/migrate/20250102_add_check_constraint_viagens_concluidas.rb
class AdicionarCheckConstraintViagensConcluidas < ActiveRecord::Migration[7.1]
  def change
    add_check_constraint :viagens,
      "concluida_em > criada_em",
      name: "viagens_concluidas_apos_criacao"
  end
end

Essa migration gera o seguinte SQL:

ALTER TABLE viagens
  ADD CONSTRAINT viagens_concluidas_apos_criacao
  CHECK (concluida_em > criada_em);

Agora tente inserir dados inválidos:

viagem = Viagem.new(
  criada_em: Time.current,
  concluida_em: 1.hour.ago  # Inválido!
)

viagem.save
# => ActiveRecord::StatementInvalid: 
#    PG::CheckViolation: ERROR: new row violates check constraint

Exemplo 2: Duração Mínima de Reserva

Agora vamos garantir que reservas tenham no mínimo 30 minutos:

# db/migrate/20250102_add_duracao_minima_reservas.rb
class AdicionarDuracaoMinimaReservas < ActiveRecord::Migration[7.1]
  def change
    add_check_constraint :reservas_veiculos,
      "termina_em >= (inicia_em + INTERVAL '30 minutes')",
      name: "reserva_duracao_minima"
  end
end

Note o uso de INTERVAL '30 minutes' — uma funcionalidade poderosa do PostgreSQL para trabalhar com datas e horários.

Exemplo 3: Validações Complexas de Negócio

Check constraints podem expressar lógica de negócio complexa. Imagine que um desconto nunca pode ser maior que o preço original:

add_check_constraint :produtos,
  "desconto <= preco_original AND desconto >= 0",
  name: "desconto_valido"

Check Constraints em Tabelas Grandes: A Técnica NOT VALID

Aqui está um truque avançado: quando você adiciona um check constraint em uma tabela com milhões de linhas, PostgreSQL trava a tabela para validar todas as linhas existentes. Em produção, isso pode ser desastroso.

A solução? Use validate: false (que gera NOT VALID no SQL):

# Migration 1: Adiciona constraint SEM validar dados existentes
class AdicionarConstraintSemValidar < ActiveRecord::Migration[7.1]
  def change
    add_check_constraint :viagens,
      "preco > 0",
      name: "preco_positivo",
      validate: false  # Não valida linhas existentes!
  end
end

# Migration 2: Depois de corrigir dados inválidos, valida
class ValidarConstraintPreco < ActiveRecord::Migration[7.1]
  def change
    validate_check_constraint :viagens, name: "preco_positivo"
  end
end

Com essa técnica em duas etapas:

  1. Novos dados já são validados imediatamente
  2. Você tem tempo para corrigir dados antigos
  3. A validação final usa um lock mais leve

Check Constraints vs Active Record Validations

Você deve estar pensando: "Mas eu já valido isso no model!" Aqui está a diferença:

# app/models/reserva_veiculo.rb
class ReservaVeiculo < ApplicationRecord
  # Validação no Active Record (camada de aplicação)
  validate :termina_apos_inicio
  
  private
  
  def termina_apos_inicio
    if termina_em && inicia_em && termina_em <= inicia_em
      errors.add(:termina_em, "deve ser depois do início")
    end
  end
end

Vantagens da validação no Active Record:

  • Mensagens de erro customizadas e traduzidas
  • Lógica condicional complexa em Ruby
  • Feedback imediato no formulário

Vantagens do Check Constraint:

  • Proteção contra condições de corrida
  • Funciona mesmo com SQL direto ou rake tasks
  • Documenta regras no schema do banco
  • Garantia absoluta de consistência

Recomendação: Use os dois! Active Record para UX e check constraints para segurança.

Exercício Prático: Seu Turno

Tente criar check constraints para estes cenários:

  1. Idade mínima: Usuários devem ter pelo menos 18 anos
  2. Percentual válido: Taxa de desconto entre 0 e 100
  3. Horário comercial: Reservas só entre 8h e 20h

Dica para o exercício 3: use EXTRACT(HOUR FROM horario) no PostgreSQL!

Ferramentas Úteis

Adicione essas gems ao seu Gemfile para verificar se suas validações e constraints estão sincronizados:

# Gemfile
group :development do
  gem 'active_record_doctor'
  gem 'database_consistency'
end

Execute:

bundle exec rake active_record_doctor
bundle exec database_consistency

Conclusão

Check constraints são uma ferramenta poderosa que todo desenvolvedor Rails intermediário deveria ter no seu arsenal. Eles oferecem:

  • ✅ Garantias de consistência mais fortes que validações de aplicação
  • ✅ Proteção contra condições de corrida
  • ✅ Documentação viva das regras de negócio
  • ✅ Flexibilidade para expressar qualquer lógica SQL

Comentários (0)

Nenhum comentário:

Postar um comentário