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
endEssa 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 constraintExemplo 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
endNote 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
endCom essa técnica em duas etapas:
- Novos dados já são validados imediatamente
- Você tem tempo para corrigir dados antigos
- 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
endVantagens 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:
- Idade mínima: Usuários devem ter pelo menos 18 anos
- Percentual válido: Taxa de desconto entre 0 e 100
- 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'
endExecute:
bundle exec rake active_record_doctor
bundle exec database_consistencyConclusã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
Nenhum comentário:
Postar um comentário