Caso de Uso: Modelagem de Dados para Locadora de Veículos – N:M vs 1:1

Caso de Uso: Modelagem de Dados para Locadora de Veículos – N:M vs 1:1

Neste caso de uso, abordarei a modelagem de dados simples para uma locadora de veículos, utilizando apenas três tabelas: veículos, contrato e cliente. Além disso, explorarei a comparação entre os relacionamentos 1:1 e M:N.

Modelo N:M (Muitos para Muitos): Um cliente pode alugar muitos veículos, e um veículo pode ser alugado por muitos clientes ao longo do tempo.

Modelo 1:1 de Veículo/ Contrato: Um veículo pode ter no máximo um contrato ativo por vez, mas pode ter vários contratos ao longo do tempo.

Vamos comparar esses dois modelos, focando na questão de Dois Contratos Ativos para o Mesmo Veículo Simultaneamente. Também discutiremos como resolver esse problema no modelo M:N usando triggers e como adaptar o modelo 1:1 Veículo/Contrato para permitir reservas futuras com uma tabela de reservas separada.

Ao final, abordaremos as validações extras necessárias em ambos os modelos. Como a explicação é mais técnica com exemplos e códigos, quem não tiver tempo e paciência pule direto para conclusão.

Modelo 1:1

No modelo Veículo (1,1) ---- (0,1) Contrato, um veículo pode ter no máximo um contrato ativo por vez, mas pode ter vários contratos ao longo do tempo. Isso é garantido por uma restrição UNIQUE na coluna id_veiculo da tabela Contrato.

Estrutura das Tabelas

Tabela Cliente

CREATE TABLE Cliente ( id_cliente INT PRIMARY KEY IDENTITY(1,1), nome VARCHAR(100) NOT NULL, cpf VARCHAR(14) UNIQUE NOT NULL );

Tabela Veículo

CREATE TABLE Veiculo ( id_veiculo INT PRIMARY KEY IDENTITY(1,1), placa VARCHAR(10) UNIQUE NOT NULL, modelo VARCHAR(50) NOT NULL, marca VARCHAR(50) NOT NULL );

Tabela Contrato

CREATE TABLE Contrato ( id_contrato INT PRIMARY KEY IDENTITY(1,1), id_cliente INT NOT NULL, id_veiculo INT NOT NULL UNIQUE, -- Garante que um veículo tenha no máximo um contrato ativo data_inicio DATE NOT NULL, data_fim DATE NOT NULL, valor_total DECIMAL(10, 2) NOT NULL, FOREIGN KEY (id_cliente) REFERENCES Cliente(id_cliente), FOREIGN KEY (id_veiculo) REFERENCES Veiculo(id_veiculo) );

Vantagem: Evita Dois Contratos Ativos Simultaneamente

A restrição UNIQUE na coluna id_veiculo garante que um veículo não possa ter mais de um contrato ativo ao mesmo tempo. Isso resolve automaticamente o problema de dois contratos ativos simultaneamente.

Exemplo:

-- Contrato ativo para o veículo 1 INSERT INTO Contrato (id_cliente, id_veiculo, data_inicio, data_fim, valor_total) VALUES (1, 1, '2023-01-01', '2023-01-10', 500.00); -- Contrato ativo de 01/01/2023 a 10/01/2023

Se tentarmos inserir um segundo contrato para o mesmo veículo, a restrição UNIQUE impedirá a operação:

-- Tentativa de inserir um segundo contrato para o veículo 1 INSERT INTO Contrato (id_cliente, id_veiculo, data_inicio, data_fim, valor_total) VALUES (2, 1, '2023-01-05', '2023-01-15', 600.00); -- Falha devido à restrição UNIQUE

Mas existe uma desvantagem muito grande nesse modelo, o bloqueio para contratos fututos.

Desvantagem: o bloqueio para contratos futuros.

garante que um veículo não possa ter mais de um contrato ativo por vez. No entanto, isso também bloqueia o veículo para contratos futuros enquanto o contrato atual estiver ativo. Isso significa que:

  1. Reservas Antecipadas: Não é possível registrar contratos futuros para o mesmo veículo enquanto o contrato atual estiver ativo.
  2. Flexibilidade Reduzida: O veículo fica "bloqueado" até o término do contrato atual, o que pode ser problemático em cenários onde é necessário reservar com antecedência.

Exemplo de Problema:

Suponha que o veículo de id_veiculo = 1 está alugado no período de 01/01/2023 a 10/01/2023:

-- Contrato ativo para o veículo 1 INSERT INTO Contrato (id_cliente, id_veiculo, data_inicio, data_fim, valor_total) VALUES (1, 1, '2023-01-01', '2023-01-10', 500.00); -- Contrato ativo de 01/01/2023 a 10/01/2023

Agora, um segundo cliente tenta reservar o mesmo veículo para o período de 15/01/2023 a 20/01/2023:

-- Tentativa de inserir um contrato futuro para o veículo 1 INSERT INTO Contrato (id_cliente, id_veiculo, data_inicio, data_fim, valor_total) VALUES (2, 1, '2023-01-15', '2023-01-20', 600.00); -- Contrato futuro de 15/01/2023 a 20/01/2023

O Que Acontece?

  • A restrição UNIQUE na coluna id_veiculo impedirá a inserção do contrato futuro, mesmo que ele não se sobreponha ao contrato atual.
  • Isso ocorre porque a restrição UNIQUE não leva em consideração as datas dos contratos; ela simplesmente impede que um veículo tenha mais de um contrato em qualquer momento.

Solução: Adicionar um Campo de Status e uma Tabela de Reservas

Para resolver essa limitação, podemos adaptar o modelo 1:1 de duas maneiras:

1. Adicionar um Campo de Status no Contrato

Podemos adicionar um campo status na tabela Contrato para diferenciar entre contratos ativosfuturos e encerrados. Dessa forma, a restrição UNIQUE pode ser aplicada apenas a contratos ativos.

Estrutura da Tabela Contrato com Status

CREATE TABLE Contrato ( id_contrato INT PRIMARY KEY IDENTITY(1,1), id_cliente INT NOT NULL, id_veiculo INT NOT NULL, data_inicio DATE NOT NULL, data_fim DATE NOT NULL, valor_total DECIMAL(10, 2) NOT NULL, status VARCHAR(20) NOT NULL CHECK (status IN ('ATIVO', 'FUTURO', 'ENCERRADO')), -- Status do contrato FOREIGN KEY (id_cliente) REFERENCES Cliente(id_cliente), FOREIGN KEY (id_veiculo) REFERENCES Veiculo(id_veiculo) );

Restrição UNIQUE Apenas para Contratos Ativos

Podemos criar um índice único condicional para garantir que um veículo não tenha mais de um contrato ativo por vez:

CREATE UNIQUE INDEX idx_veiculo_contrato_ativo ON Contrato(id_veiculo) WHERE status = 'ATIVO';

Como Funciona?

  • Um veículo pode ter vários contratos com status FUTURO ou ENCERRADO.
  • Apenas um contrato ativo (status = 'ATIVO') é permitido por veículo.

2. Usar uma Tabela de Reservas

Outra abordagem é criar uma tabela de reservas separada para registrar contratos futuros. A tabela Contrato continuaria a usar a restrição UNIQUE para garantir que um veículo não tenha mais de um contrato ativo por vez, enquanto a tabela Reserva permitiria registrar contratos futuros.

Estrutura da Tabela Reserva

CREATE TABLE Reserva ( id_reserva INT PRIMARY KEY IDENTITY(1,1), id_cliente INT NOT NULL, id_veiculo INT NOT NULL, data_inicio DATE NOT NULL, data_fim DATE NOT NULL, valor_total DECIMAL(10, 2) NOT NULL, FOREIGN KEY (id_cliente) REFERENCES Cliente(id_cliente), FOREIGN KEY (id_veiculo) REFERENCES Veiculo(id_veiculo) );

Como Funciona?

  • A tabela Contrato continua a usar a restrição UNIQUE para garantir que um veículo não tenha mais de um contrato ativo por vez.
  • A tabela Reserva permite registrar contratos futuros sem restrições.
  • Quando um contrato futuro se torna ativo, ele é movido da tabela Reserva para a tabela Contrato.

Exemplo de Uso com Tabela de Reservas

Passo 1: Registrar uma Reserva Futura

-- Registrar uma reserva futura para o veículo 1 INSERT INTO Reserva (id_cliente, id_veiculo, data_inicio, data_fim, valor_total) VALUES (2, 1, '2023-01-15', '2023-01-20', 600.00); -- Reserva futura de 15/01/2023 a 20/01/2023

Passo 2: Ativar a Reserva

Quando a data de início da reserva se aproxima, o contrato pode ser movido para a tabela Contrato:

-- Mover a reserva para a tabela Contrato INSERT INTO Contrato (id_cliente, id_veiculo, data_inicio, data_fim, valor_total, status) SELECT id_cliente, id_veiculo, data_inicio, data_fim, valor_total, 'ATIVO' FROM Reserva WHERE id_reserva = 1; -- ID da reserva a ser ativada

-- Excluir a reserva da tabela Reserva DELETE FROM Reserva WHERE id_reserva = 1;

Modelo M:N

No modelo N:M , um cliente pode alugar vários veículos, e um veículo pode ser alugado por vários clientes ao longo do tempo. Para implementar esse relacionamento, usamos uma tabela intermediária chamada Contrato, que conecta Cliente e Veículo.

Estrutura das Tabelas

Tabela Cliente

CREATE TABLE Cliente ( id_cliente INT PRIMARY KEY IDENTITY(1,1), nome VARCHAR(100) NOT NULL, cpf VARCHAR(14) UNIQUE NOT NULL );

Tabela Veículo

CREATE TABLE Veiculo ( id_veiculo INT PRIMARY KEY IDENTITY(1,1), placa VARCHAR(10) UNIQUE NOT NULL, modelo VARCHAR(50) NOT NULL, marca VARCHAR(50) NOT NULL );

Tabela Contrato (Intermediária)

CREATE TABLE Contrato ( id_contrato INT PRIMARY KEY IDENTITY(1,1), id_cliente INT NOT NULL, id_veiculo INT NOT NULL, data_inicio DATE NOT NULL, data_fim DATE NOT NULL, valor_total DECIMAL(10, 2) NOT NULL, FOREIGN KEY (id_cliente) REFERENCES Cliente(id_cliente), FOREIGN KEY (id_veiculo) REFERENCES Veiculo(id_veiculo) );

Problema: Dois Contratos Ativos para o Mesmo Veículo Simultaneamente

No modelo N:M , um veículo pode ter vários contratos ao longo do tempo, mas não pode ter dois contratos ativos simultaneamente. No entanto, sem restrições adicionais, o banco de dados pode permitir que isso aconteça.

Exemplo de Problema:
Suponha que o veículo de id_veiculo = 1 já tenha um contrato ativo:

-- Contrato ativo para o veículo 1 INSERT INTO Contrato (id_cliente, id_veiculo, data_inicio, data_fim, valor_total) VALUES (1, 1, '2023-01-01', '2023-01-10', 500.00); -- Contrato ativo de 01/01/2023 a 10/01/2023

Agora, vamos tentar inserir um novo contrato para o mesmo veículo (id_veiculo = 1) que se sobrepõe ao contrato atual:

-- Tentativa de inserir um novo contrato para o veículo 1 INSERT INTO Contrato (id_cliente, id_veiculo, data_inicio, data_fim, valor_total) VALUES (2, 1, '2023-01-05', '2023-01-15', 600.00); -- Novo contrato de 05/01/2023 a 15/01/2023

O Que Acontece?

  • O novo contrato começa em 05/01/2023, mas o contrato atual termina apenas em 10/01/2023.
  • Isso significa que, entre 05/01/2023 e 10/01/2023, o veículo estaria associado a dois contratos ativos simultaneamente.

Solução: Usar um Trigger para Validar Períodos

Para evitar esse problema, podemos criar um trigger que verifica se há sobreposição de períodos antes de inserir ou atualizar um contrato.

CREATE TRIGGER trg_verifica_contrato_ativo ON Contrato FOR INSERT, UPDATE AS BEGIN IF EXISTS ( SELECT 1 FROM Contrato c JOIN inserted i ON c.id_veiculo = i.id_veiculo WHERE c.id_contrato != i.id_contrato AND c.data_inicio <= i.data_fim AND c.data_fim >= i.data_inicio ) BEGIN RAISERROR('Erro: Veículo já possui um contrato ativo no período especificado.', 16, 1); ROLLBACK TRANSACTION; END END;

Como Funciona o Trigger?

  1. Quando um novo contrato é inserido ou atualizado, o trigger verifica se já existe um contrato para o mesmo veículo (id_veiculo).
  2. Se existir, ele verifica se o novo contrato começa antes do término do contrato atual (c.data_fim > i.data_inicio).
  3. Se a condição for verdadeira, o trigger lança um erro e reverte a transação.

Validações Adicionais Necessárias

Ambos os modelos precisam de validações adicionais para garantir a integridade dos dados.

  1. Contratos com Datas Inválidas:
    • Garantir que data_inicio não seja posterior a data_fim.
  2. Contratos com Sobreposição de Períodos:
    • No modelo M:N, é necessário verificar se um novo contrato não se sobrepõe a um contrato existente.
    • No modelo Veículo (1,1) ---- (0,1) Contrato, a restrição UNIQUE já resolve esse problema.

Independentemente do modelo escolhido, é crucial implementar validações adicionais para garantir a integridade dos dados e evitar problemas como contratos com datas inválidas ou sobreposição de períodos. Essas validações podem ser feitas com triggers no banco de dados ou com regras no backend/frontend.

Conclusão

Acredito que o modelo 1:1 (veículo/contrato) seja mais restritivo, burocrático e trabalhoso, já que limita bastante a flexibilidade. Às vezes, é melhor ter um pouco mais de liberdade operacional e controlar as restrições por meio de validações feitas tanto no banco de dados com triggers quanto no backend/frontend.

No modelo 1:1, há a vantagem clara de garantir automaticamente que um veículo possua apenas um contrato ativo de cada vez—algo que não acontece naturalmente no modelo M:N. Porém, a grande desvantagem é que o veículo fica bloqueado até que o contrato vigente seja formalmente encerrado, dificultando ou impossibilitando reservas futuras, ou antecipadas.

Uma alternativa para flexibilizar o modelo 1:1 seria incluir um campo de status no contrato (por exemplo, ATIVO, FUTURO, ENCERRADO) para diminuir essa rigidez. No entanto, ainda seria necessária a criação de uma tabela adicional para reservas futuras, migrando os dados dessas reservas para a tabela principal assim que o contrato estivesse liberado.

Por esses motivos, minha escolha pessoal seria pelo modelo M:N, devido à sua maior flexibilidade na gestão das reservas futuras. Usando o modelo M:N com triggers para evitar conflitos, você consegue garantir integridade sem perder controle operacional.

O relacionamento principal ocorre entre Veículo e Cliente. Com o modelo M:N, cria-se uma tabela intermediária (que podemos chamar de Contrato) onde é possível incluir atributos extras relevantes ao negócio.

As validações são essenciais em ambos os modelos para evitar problemas sistêmicos, como contratos com datas inválidas (por exemplo, data de entrega anterior à data de locação) ou períodos que se sobrepõem (especialmente no M:N). A complexidade adicional gerada pelas triggers é administrável e compensa muito pelo ganho operacional a longo prazo, podendo também ser complementada por regras de negócio aplicadas no backend ou frontend.

Todos os códigos estão em formato SQL formatados para o SQL Server da Microsoft

Se esse conteúdo foi útil para você, deixe um comentário com suas opiniões ou sugestões.

Veja mais

Entendendo a Diferença entre Modelo Entidade-Relacionamento (MER) e Diagrama Entidade-Relacionamento (DER)

Entendendo a Diferença entre Modelo Entidade-Relacionamento (MER) e Diagrama Entidade-Relacionamento (DER)

A modelagem de dados é uma etapa crucial no desenvolvimento de sistemas de informação, e dois conceitos fundamentais nesse processo são o MER (Modelo Entidade-Relacionamento) e o DER (Diagrama Entidade-Relacionamento). Embora esses termos sejam frequentemente usados de forma intercambiável, eles representam etapas distintas na modelagem de dados. Este artigo tem

Por Andrei Schievelbein