Versão 1.0
Triggers e Stored Procedures
Conceito:
Triggers e Stored Procedures são objetos de banco de dados que estendem o poder do SQL, permitindo que a lógica de negócio seja executada diretamente no servidor. Eles automatizam tarefas, garantem a integridade dos dados e otimizam a performance, funcionando como blocos de código reutilizáveis que operam sobre as tabelas.
Uma Stored Procedure (Procedimento Armazenado) é um conjunto de comandos SQL que pode ser salvo e executado sob demanda. Ela pode aceitar parâmetros de entrada e retornar valores, agindo como uma função em uma linguagem de programação. O uso de procedures centraliza a lógica, reduz o tráfego de rede e melhora a segurança, pois as aplicações podem interagir com o banco de dados através de uma interface bem definida, sem precisar de acesso direto às tabelas.
Já um Trigger (Gatilho) é um tipo especial de procedimento que é "disparado" automaticamente em resposta a eventos específicos de manipulação de dados (DML), como INSERT
, UPDATE
ou DELETE
. Triggers são ideais para impor regras de negócio complexas, auditar modificações ou manter a consistência entre tabelas relacionadas de forma automática. Diferente de uma Stored Procedure, um Trigger não pode ser chamado diretamente; sua execução é uma consequência de uma ação sobre os dados.
Implementações no Software:
Visão Geral dos Objetos Programáveis
Este script implementa a lógica de negócio dinâmica do jogo de RPG através de triggers e stored procedures. Esses objetos automatizam regras (como a validação de peso em inventários e a concessão de experiência) e encapsulam ações (como a execução de uma batalha completa), garantindo que as mecânicas do jogo funcionem de forma consistente e segura diretamente no banco de dados.
A seguir, apresento a criação de cada objeto programável:
Declarações de DML no Software:
Atenção!
O conteúdo deste tópico poderá sofrer alterações ao longo da Disciplina de Sistema de Banco de Dados 1. Portanto, à medida que novas inserções forem necessárias, o arquivo atual sempre sempre se manterá atualizado com a nova versão.
1. Triggers
Estes são os procedimentos que o banco de dados executará automaticamente para manter as regras do jogo.
1.1. Trigger de Verificação de Peso do Inventário (trg_check_inventory_weight
)
Este trigger garante que um jogador não possa carregar mais itens do que a sua capacidade (pods
) permite. Ele é disparado antes de qualquer item ser adicionado ou movido, calculando o peso total e cancelando a operação se o limite for excedido.
-- PASSO 1: A Função do Trigger
CREATE OR REPLACE FUNCTION func_check_inventory_weight()
RETURNS TRIGGER AS $$
DECLARE
v_total_peso NUMERIC;
v_max_pods INT;
BEGIN
-- Se um item está sendo adicionado a um inventário
IF NEW.id_inventario IS NOT NULL THEN
-- Pega a capacidade máxima do inventário de destino
SELECT pods INTO v_max_pods FROM inventario WHERE id_inventario = NEW.id_inventario;
-- Calcula o peso total dos itens que já estão no inventário
SELECT COALESCE(SUM(peso), 0) INTO v_total_peso FROM item WHERE id_inventario = NEW.id_inventario;
-- Se o peso total + peso do novo item exceder a capacidade, lança um erro.
IF (v_total_peso + NEW.peso) > v_max_pods THEN
RAISE EXCEPTION 'Capacidade de peso (Pods) do inventário excedida! Operação cancelada.';
END IF;
END IF;
-- Se a verificação passar, permite a operação
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- PASSO 2: A Declaração do Trigger na Tabela 'item'
CREATE TRIGGER trg_check_inventory_weight
BEFORE INSERT OR UPDATE ON item
FOR EACH ROW
EXECUTE FUNCTION func_check_inventory_weight();
1.2. Trigger de Concessão de Experiência (trg_grant_xp_on_victory
)
Essencial para a progressão no jogo, este trigger é disparado após um confronto ser registrado. Se o resultado for uma vitória do jogador (vencedor = TRUE
), o trigger automaticamente busca o valor de XP
da criatura derrotada e atualiza o nível do personagem do jogador.
-- PASSO 1: A Função do Trigger
CREATE OR REPLACE FUNCTION func_grant_xp_on_victory()
RETURNS TRIGGER AS $$
DECLARE
v_xp_ganho INT;
v_jogador_level_atual INT;
BEGIN
-- Verifica se o confronto foi uma vitória para o jogador
IF NEW.vencedor = TRUE THEN
-- Pega o XP da criatura derrotada
SELECT XP INTO v_xp_ganho FROM criatura WHERE ID_personagem = NEW.criatura_id;
-- Pega o level atual do jogador
SELECT level INTO v_jogador_level_atual FROM personagem WHERE ID_personagem = NEW.jogador_id;
-- Atualiza o personagem do jogador com um novo level (lógica de exemplo: +1 level por vitória)
UPDATE personagem
SET level = v_jogador_level_atual + 1
WHERE ID_personagem = NEW.jogador_id;
-- Exibe uma notificação no console do banco (útil para debug)
RAISE NOTICE 'Jogador ID % venceu! Ganhou % XP e subiu para o nível %.',
NEW.jogador_id, v_xp_ganho, v_jogador_level_atual + 1;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- PASSO 2: A Declaração do Trigger na Tabela 'confronta'
CREATE TRIGGER trg_grant_xp_on_victory
AFTER INSERT ON confronta
FOR EACH ROW
EXECUTE FUNCTION func_grant_xp_on_victory();
1.3. Trigger de Auditoria de Nível (trg_log_personagem_level_up
)
Para fins de auditoria e análise de progressão, este trigger cria um registro em uma tabela de log (log_personagem_level
) toda vez que o atributo level
de um personagem é modificado, armazenando o valor antigo, o novo valor e a data da alteração.
-- PASSO 1: Criar a tabela de log (se não existir)
CREATE TABLE IF NOT EXISTS log_personagem_level (
id_log SERIAL PRIMARY KEY,
id_personagem INT NOT NULL,
level_antigo INT,
level_novo INT,
data_modificacao TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- PASSO 2: A Função do Trigger
CREATE OR REPLACE FUNCTION func_log_personagem_level_up()
RETURNS TRIGGER AS $$
BEGIN
-- Verifica se a coluna 'level' foi de fato alterada
IF OLD.level IS DISTINCT FROM NEW.level THEN
INSERT INTO log_personagem_level (id_personagem, level_antigo, level_novo)
VALUES (OLD.ID_personagem, OLD.level, NEW.level);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- PASSO 3: A Declaração do Trigger na Tabela 'personagem'
CREATE TRIGGER trg_log_personagem_level_up
AFTER UPDATE ON personagem
FOR EACH ROW
EXECUTE FUNCTION func_log_personagem_level_up();
2. Stored Procedures (Ações Manuais)
Estes são os procedimentos que podem ser chamados pela aplicação para executar operações que exigem uma interrupções no banco.
2.1. Procedure de Execução de Batalha (sp_executar_batalha
)
Esta procedure encapsula toda a lógica de um combate entre um jogador e uma criatura. Ao ser chamada, ela simula a luta, determina um vencedor com base nos atributos de cada um e registra os resultados nas tabelas batalha
e confronta
. Isso simplifica a lógica na aplicação, que precisa apenas chamar um único comando para uma ação complexa.
CREATE OR REPLACE PROCEDURE sp_executar_batalha(p_jogador_id INT, p_criatura_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
v_ataque_jogador INT;
v_vida_criatura INT;
v_dano_causado INT;
v_vitoria BOOLEAN;
BEGIN
-- Obter atributos de combate
SELECT atq INTO v_ataque_jogador FROM skill WHERE ID_jogador = p_jogador_id;
SELECT vida_maxima INTO v_vida_criatura FROM personagem WHERE ID_personagem = p_criatura_id;
-- Lógica de batalha simplificada: Dano = ataque do jogador
v_dano_causado := v_ataque_jogador;
-- Determinar o vencedor
IF v_dano_causado >= v_vida_criatura THEN
v_vitoria := TRUE;
ELSE
v_vitoria := FALSE;
END IF;
-- Registrar o log da batalha
INSERT INTO batalha (Dano_causado, Controle_Dano, Ambiente_batalha, Dano_sofrido)
VALUES (v_dano_causado, 100, 'Campo Aberto', 0); -- Valores de exemplo
-- Registrar o resultado do confronto (isso irá disparar o trigger de XP)
INSERT INTO confronta (vencedor, criatura_id, jogador_id)
VALUES (v_vitoria, p_criatura_id, p_jogador_id);
COMMIT;
END;
$$;
2.2. Procedure de Transferência de Itens (sp_transferir_item
)
Esta procedure gerencia a troca de itens entre dois personagens. Ela executa todas as validações necessárias: verifica se o vendedor realmente possui o item e se o comprador tem um inventário com capacidade disponível. A transferência só é concluída se todas as regras forem satisfeitas, garantindo a consistência dos dados.
CREATE OR REPLACE PROCEDURE sp_transferir_item(p_item_id INT, p_vendedor_id INT, p_comprador_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
v_inventario_vendedor_id INT;
v_inventario_comprador_id INT;
v_item_inventario_atual INT;
BEGIN
-- Pega os IDs dos inventários
SELECT id_inventario INTO v_inventario_vendedor_id FROM inventario WHERE id_personagem = p_vendedor_id;
SELECT id_inventario INTO v_inventario_comprador_id FROM inventario WHERE id_personagem = p_comprador_id;
-- Pega o inventário atual do item
SELECT id_inventario INTO v_item_inventario_atual FROM item WHERE id_item = p_item_id;
-- Validação 1: O vendedor realmente possui o item?
IF v_item_inventario_atual IS DISTINCT FROM v_inventario_vendedor_id THEN
RAISE EXCEPTION 'O vendedor (ID: %) não possui o item (ID: %).', p_vendedor_id, p_item_id;
END IF;
-- Validação 2: O comprador tem um inventário?
IF v_inventario_comprador_id IS NULL THEN
RAISE EXCEPTION 'O comprador (ID: %) não possui um inventário.', p_comprador_id;
END IF;
-- Ação: Atualizar o item para o novo inventário.
-- O trigger trg_check_inventory_weight será disparado automaticamente aqui para validar o peso.
UPDATE item SET id_inventario = v_inventario_comprador_id WHERE id_item = p_item_id;
RAISE NOTICE 'Item ID % transferido com sucesso do personagem % para o personagem %.', p_item_id, p_vendedor_id, p_comprador_id;
COMMIT;
END;
$$;
3. Testes
Depois de executar os scripts fizemos alguns testes, como um check caso o peso do inventário passe do limite, transferencias de itens e XP:
3.1. Testando o Trigger de Peso do Inventário (trg_check_inventory_weight)
-- O inventário do personagem 1 tem capacidade 30. A espada já pesa 0.1 e a armadura 5.0 (Total: 5.1).
-- Tentar adicionar um item pesado que exceda o limite.
-- Esta operação deverá FALHAR com uma exceção.
INSERT INTO item (id_item, id_inventario, nome, peso, durabilidade)
VALUES (3, 1, 'Bigorna de Anão', 28.0, 999);
-- ESPERADO: ERRO: Capacidade de peso (Pods) do inventário excedida!
3.2.Testando o Trigger de XP (trg_grant_xp_on_victory) e a Stored Procedure de Batalha
-- O jogador 1 está no level 12. O Goblin Ladrão (ID 10) dá 1200 XP.
-- Vamos fazer o jogador 1 batalhar com o Goblin.
CALL sp_executar_batalha(p_jogador_id := 1, p_criatura_id := 10);
-- Verifique o resultado. O jogador 1 deve ter subido de nível.
SELECT nome, level FROM personagem WHERE ID_personagem = 1;
-- ESPERADO: O level deve ser 13 agora.
-- Verifique a tabela de log para ver a alteração.
SELECT * FROM log_personagem_level WHERE id_personagem = 1;
-- ESPERADO: Um registro com level_antigo=12 e level_novo=13.
3.3. Testando a Stored Procedure de Transferência de Item (sp_transferir_item)
-- Vamos transferir a 'Armadura de Couro' (ID 2) do personagem 1 para o personagem 2.
-- O inventário 2 tem 90 de capacidade, então deve funcionar.
CALL sp_transferir_item(p_item_id := 2, p_vendedor_id := 1, p_comprador_id := 2);
-- Verifique o inventário do item 2.
SELECT id_inventario FROM item WHERE id_item = 2;
-- ESPERADO: O id_inventario deve ser 2 agora.
3.4. Testando a aplicação completa em bloco com (BEGIN...ROLLBACK)
Aliado a esses testes específicos fizemos um script para a validação completa do game o script é envolvido em um bloco de transação (BEGIN...ROLLBACK), o que permite sua execução segura em qualquer ambiente sem alterar permanentemente os dados, tornando-o ideal para nossos testes e demonstrações.
BEGIN; -- Inicia uma transação para não salvar os dados de teste
-- ===================================================================
-- TESTE 1: Trigger de Peso de Inventário (trg_check_inventory_weight)
-- ===================================================================
-- -------------------------------------------------------------------
-- Teste 1.1: FALHA - Tentar adicionar item excedendo o peso máximo
-- -------------------------------------------------------------------
RAISE INFO '--> TESTE 1.1: FALHA - Adicionar item pesado demais.';
RAISE INFO 'Cenário: Inventário 1 (Personagem 1) tem capacidade 30. Vamos tentar adicionar um item de peso 28, o que excederia o limite (5.1 já em uso + 28 > 30).';
-- Verificação Pré-Teste
SELECT pods AS capacidade_maxima,
(SELECT SUM(peso) FROM item WHERE id_inventario = 1) AS peso_atual
FROM inventario WHERE id_inventario = 1;
-- Ação (ESPERADO: ERRO!)
-- Esta linha deve causar um erro e interromper a transação se não for tratada.
-- Para um teste automatizado, você usaria um bloco DO/EXCEPTION. Para visualização, apenas saiba que ela falha.
-- INSERT INTO item (id_item, id_inventario, nome, peso, durabilidade) VALUES (100, 1, 'Bigorna de Anão', 28.0, 999);
RAISE INFO 'NOTA: A ação de INSERT para o teste 1.1 foi comentada para permitir que o script continue. Se descomentada, ela deve gerar uma exceção: "Capacidade de peso (Pods) do inventário excedida!".';
-- -------------------------------------------------------------------
-- Teste 1.2: SUCESSO - Adicionar item com peso válido
-- -------------------------------------------------------------------
RAISE INFO '--> TESTE 1.2: SUCESSO - Adicionar item com peso válido.';
RAISE INFO 'Cenário: Inventário 1 (Personagem 1) tem 24.9 de espaço livre. Vamos adicionar um item de peso 10.0.';
-- Verificação Pré-Teste
SELECT count(*) AS total_itens_antes FROM item WHERE id_inventario = 1;
-- Ação
INSERT INTO item (id_item, id_inventario, nome, peso, durabilidade) VALUES (101, 1, 'Elmo de Aço', 10.0, 150);
RAISE INFO 'Ação: Item "Elmo de Aço" inserido no inventário 1.';
-- Verificação Pós-Teste
SELECT count(*) AS total_itens_depois,
SUM(peso) AS novo_peso_total
FROM item WHERE id_inventario = 1;
RAISE INFO 'ESPERADO: total_itens_depois deve ser 3. novo_peso_total deve ser 15.10.';
-- ===================================================================
-- TESTE 2: Procedure de Transferência de Item (sp_transferir_item)
-- ===================================================================
-- -------------------------------------------------------------------
-- Teste 2.1: SUCESSO - Transferência válida
-- -------------------------------------------------------------------
RAISE INFO '--> TESTE 2.1: SUCESSO - Transferência de item válida.';
RAISE INFO 'Cenário: Transferir a "Armadura de Couro" (ID 2, peso 5.0) do Personagem 1 para o Personagem 2 (Inventário 2, capacidade 90).';
-- Verificação Pré-Teste
SELECT id_inventario FROM item WHERE id_item = 2;
RAISE INFO 'ESPERADO (antes): id_inventario deve ser 1.';
-- Ação
CALL sp_transferir_item(p_item_id := 2, p_vendedor_id := 1, p_comprador_id := 2);
-- Verificação Pós-Teste
SELECT id_inventario FROM item WHERE id_item = 2;
RAISE INFO 'ESPERADO (depois): id_inventario deve ser 2.';
-- -------------------------------------------------------------------
-- Teste 2.2: FALHA - Vendedor não possui o item
-- -------------------------------------------------------------------
RAISE INFO '--> TESTE 2.2: FALHA - Vendedor não possui o item.';
RAISE INFO 'Cenário: Tentar fazer o Personagem 3 (que não tem o item) vender a "Espada Longa" (ID 1) para o Personagem 1.';
-- Verificação Pré-Teste
SELECT p.nome, i.id_inventario FROM item i JOIN inventario inv ON i.id_inventario = inv.id_inventario JOIN personagem p ON inv.id_personagem = p.id_personagem WHERE i.id_item = 1;
RAISE INFO 'NOTA: Espada Longa pertence ao Personagem 1.';
-- Ação (ESPERADO: ERRO!)
-- CALL sp_transferir_item(p_item_id := 1, p_vendedor_id := 3, p_comprador_id := 1);
RAISE INFO 'NOTA: A ação de CALL para o teste 2.2 foi comentada. Se descomentada, ela deve gerar a exceção: "O vendedor (ID: 3) não possui o item (ID: 1).".';
-- -------------------------------------------------------------------
-- Teste 2.3: FALHA - Comprador não tem espaço no inventário
-- -------------------------------------------------------------------
RAISE INFO '--> TESTE 2.3: FALHA - Comprador com inventário cheio.';
RAISE INFO 'Cenário: Transferir a "Espada Longa" (ID 1, peso 0.1) do Personagem 1 para o Personagem 3. Antes, vamos lotar o inventário do Personagem 3.';
-- Setup: Criar inventário para Personagem 3 e lotá-lo
INSERT INTO inventario (id_inventario, id_personagem, pods) VALUES (3, 3, 20);
INSERT INTO item (id_item, id_inventario, nome, peso, durabilidade) VALUES (102, 3, 'Escudo Torre', 20.0, 500);
RAISE INFO 'Setup: Inventário para Personagem 3 criado com capacidade 20 e já preenchido com um item de peso 20.0.';
-- Ação (ESPERADO: ERRO!)
-- Tenta mover a espada de peso 0.1 para o inventário já lotado.
-- CALL sp_transferir_item(p_item_id := 1, p_vendedor_id := 1, p_comprador_id := 3);
RAISE INFO 'NOTA: A ação de CALL para o teste 2.3 foi comentada. A procedure deve falhar por causa do trigger trg_check_inventory_weight, gerando a exceção de capacidade excedida.';
-- ===================================================================
-- TESTE 3: Batalha, Concessão de XP e Log (sp_executar_batalha,
-- trg_grant_xp_on_victory, trg_log_personagem_level_up)
-- ===================================================================
-- Este teste verifica 3 funcionalidades de uma vez.
-- -------------------------------------------------------------------
-- Teste 3.1: SUCESSO - Jogador vence, ganha XP, sobe de nível e gera log
-- -------------------------------------------------------------------
RAISE INFO '--> TESTE 3.1: SUCESSO - Batalha com vitória e progressão.';
RAISE INFO 'Cenário: O "Guia Roric" (ID 1, jogador, ATQ 100) enfrenta o "Goblin Ladrão" (ID 10, vida 90, XP 1200). O jogador deve vencer.';
-- Verificação Pré-Teste
SELECT level AS nivel_antes FROM personagem WHERE id_personagem = 1;
SELECT count(*) AS total_confrontos_antes FROM confronta;
SELECT count(*) AS total_logs_antes FROM log_personagem_level WHERE id_personagem = 1;
-- Ação
CALL sp_executar_batalha(p_jogador_id := 1, p_criatura_id := 10);
RAISE INFO 'Ação: Batalha executada entre jogador 1 e criatura 10.';
-- Verificação Pós-Teste
SELECT level AS nivel_depois FROM personagem WHERE id_personagem = 1;
RAISE INFO 'ESPERADO: nivel_depois deve ser maior que o anterior.';
SELECT vencedor FROM confronta ORDER BY unique_id DESC LIMIT 1;
RAISE INFO 'ESPERADO: vencedor deve ser TRUE.';
SELECT level_antigo, level_novo FROM log_personagem_level WHERE id_personagem = 1 ORDER BY id_log DESC LIMIT 1;
RAISE INFO 'ESPERADO: Um novo log de alteração de nível deve existir.';
-- -------------------------------------------------------------------
-- Teste 3.2: LÓGICA DO TRIGGER - Confronto com derrota não concede XP
-- -------------------------------------------------------------------
RAISE INFO '--> TESTE 3.2: LÓGICA - Derrota não concede XP.';
RAISE INFO 'Cenário: Vamos simular uma derrota para o Jogador 2, inserindo um confronto com "vencedor = FALSE". O trigger de XP não deve ser acionado.';
-- Verificação Pré-Teste
SELECT level AS nivel_antes FROM personagem WHERE id_personagem = 2;
-- Ação
INSERT INTO confronta(vencedor, criatura_id, jogador_id) VALUES (FALSE, 9, 2);
RAISE INFO 'Ação: Inserido confronto com derrota para o jogador 2.';
-- Verificação Pós-Teste
SELECT level AS nivel_depois FROM personagem WHERE id_personagem = 2;
RAISE INFO 'ESPERADO: nivel_depois deve ser igual ao nivel_antes.';
RAISE INFO '==================================================';
RAISE INFO 'Todos os testes foram executados.';
RAISE INFO 'A transação será desfeita com ROLLBACK.';
RAISE INFO '==================================================';
ROLLBACK; -- Desfaz todas as alterações feitas durante o teste
Tabela de Versionamento
Versão | Data | Descrição | Autor(es) | Revisor(es) |
---|---|---|---|---|
1.0 | 04/07/2025 | Criação da documentação para triggers e stored procedures do jogo | Felipe das Neves | Felipe das Neves |