79 Universidade Federal de Santa Maria Anexo II Construção de Aplicações de Acesso a Banco de Dados Parte II (Relação Mestre/Detalhe) O objetivo deste material é demonstrar possíveis códigos (enxutos e seguros) para realizar operações de acesso/manutenção em uma tabela resultante de um relacionamento NxN (muitos para muitos) utilizando componentes DataWare (componentes que representam automaticamente informações contidas em uma origem de dados). Nas aplicações este tipo de relação (NxN) é normalmente denominado de relação Mestre/Detalhe. Neste tipo de relação os dados exibidos na tabela Detalhe são dependentes dos dados exibidos na tabela Mestre. Para exemplificar essa relação vamos utilizar uma tabela resultante da relação Receitas e Ingredientes (uma receita pode conter muitos ingredientes e um ingrediente pode estar contido em muitas receitas). O modelo de dados utilizado neste exemplo utiliza três tabelas relacionadas da seguinte forma: CREATE TABLE RECEITAS ( ID_RECEITA INTEGER NOT NULL, NOME VARCHAR(100) NOT NULL, ORIGEM VARCHAR(100), TIPO_RECEITA CHAR(1) NOT NULL, TEMPO_PREPARO DECIMAL(5,2), MODO_PREPARO BLOB SUB_TYPE 1, FOTO BLOB SUB_TYPE 0 ); CREATE TABLE INGREDIENTES_RECEITA ( ID_RECEITA INTEGER NOT NULL, ID_INGREDIENTE INTEGER NOT NULL, QUANTIDADE DECIMAL(5,2) NOT NULL ); CREATE TABLE INGREDIENTES ( ID_INGREDIENTE INTEGER NOT NULL, DESCRICAO VARCHAR(100) NOT NULL, UNIDADE CHAR(3) NOT NULL ); A relação mestre/detalhe pode se dar por qualquer uma das tabelas relacionadas. Por exemplo, no cadastro de receitas podem-se exibir todos os seus ingredientes (neste caso a tabela mestre é a tabela Receitas e a tabela detalhe é a tabela Ingredientes_Receita). Essa relação também seria válida no cadastro de Ingredientes, ou seja, exibir todas as receitas onde determinado ingrediente é utilizado (neste caso a tabela mestre é a tabela Ingredientes e a tabela detalhe é a tabela Ingredientes_Receita). Independente de quem seja a tabela mestre, a tabela detalhe sempre será aquela que representa o relacionamento NxN. Observe na figura abaixo uma relação de dependência entre Receitas e IngredientesReceita Caderno Didático Lazarus IDE Página 79
80 Universidade Federal de Santa Maria Em uma aplicação com uma relação mestre/detalhe, os códigos discutidos no Anexo I (Cadastro Simples) sofrerão algumas alterações (agora será necessário executar operações e controlar mais de uma tabela). Para fazer a demonstração será utilizada uma janela de manutenção semelhante à figura abaixo com o devido tratamento dos eventos que possam ocorrer objetivando um código simples que possa funcionar para qualquer tela de cadastro com características semelhantes. De forma a organizar o código, um componente ActionList será utilizado com a criação e implementação 3 novas ações (actincluirdetalhe, actsalvardetalhe, actexcluirdetalhe) além daquelas sete já discutidas anteriormente actnovo, actlocalizar, actsalvar, actcancelar, actexcluir, actsair, actlocalizarpordescricao). Edit DBEdit (ParentColor = true, ReadOnly = true) GroupBox para agrupar todas os campos da tabela detalhe e suas relações. DBGrid para listar as informações da tabela detalhe evitando a necessidade de mecanismos para pesquisa (o usuário clica sobre o registro que deseja fazer manutenção) Três novas ações para permitir a manutenção nas informações da tabela Detalhe. Relacionamento Mestre/Detalhe entre dois componentes SQLQuery O relacionamento de dependência (mestre/detalhe) entre duas consultas é realizado por meio de dois procedimentos: Inclusão de uma condição na SQLQuery da tabela Detalhe: a propriedade SQL da tabela detalhe deve ser alterada de forma a conter uma condição na cláusula WHERE que filtre os dados da tabela detalhe de acordo com o registro selecionado na tabela mestre. A condição de filtro da tabela detalhe deve-se utilizar de um campo obrigatoriamente existente na tabela mestre (geralmente o campo chave), precedido do sinal de ":" (dois pontos); Alterar a propriedade DataSource da SQLQuery Detalhe indicando o DataSource da SQLQuery Mestre. Caderno Didático Lazarus IDE Página 80
81 Universidade Federal de Santa Maria A figura a seguir ilustra as alterações necessárias para ativar uma relação mestre/detalhe entre dois componentes SQLQuery: Com essa relação configurada, quando a tabela MESTRE está ativa a tabela DETALHE se comporta exibindo somente os registros que fazem referência ao registro atual da tabela MESTRE. Em outras palavras, o comando SQL da SQLQueryDetalhe é alterado de forma a substituir o valor do parâmetro :ID_RECEITA pelo valor contido no campo de mesmo nome (ID_RECEITA) da SQLQueryMestre. Abertura e Fechamento de Consultas Os procedimentos AbrirConsultas() e FecharConsultas() desenvolvidos no Anexo I (Cadastro Simples) precisarão ser alterados para ativar e desativar os dados da tabela detalhe e eventualmente tabelas relacionadas a ela. procedure AbrirConsultas(); //Se a transação não está ativa então inicia ela if not SQLTransaction.Active then SQLTransaction.StartTransaction;... //Restante do código discutido no Anexo I //Se a tabela detalhe e suas tabelas relacionadas não estão ativas então devem ser ativadas if not TabelaRelacionadaComDetalhe.Active then TabelaRelacionadaComDetalhe.Open; if not TabelaDetalhe.Active then TabelaDetalhe.Open; //se a tabela não está ativa então a ative if not TabelaMestre.Active then TabelaMestre.Open; procedure FecharConsultas(); //se as tabelas relacionadas estão ativas então as mesmas devem ser fechadas... //Restante do código discutido no Anexo I //Se a tabela detalhe e suas tabelas relacionadas estão ativas então devem ser fechadas if TabelaDetalhe.Active then TabelaDetalhe.Close; if TabelaRelacionadaComDetalhe.Active then TabelaRelacionadaComDetalhe.Close; //se a tabela está ativa então ela deve ser encerrada if TabelaMestre.Active then TabelaMestre.Close; //se a transação estiver ativa a mesma deve ser encerrada if SQLTransaction.Active then SQLTransaction.EndTransaction; Caderno Didático Lazarus IDE Página 81
82 Universidade Federal de Santa Maria Mantendo as tabelas (mestre e detalhe) com o mesmo estado O código a seguir demonstra como sincronizar o estado da tabela mestre e da tabela detalhe. Sempre que a tabela mestre estiver ativa a tabela detalhe deve ser ativada e sempre que a tabela mestre estiver inativa então a tabela detalhe deve ser inativada. Essa alteração será feita no procedimento HabilitarDesabilitarControles que também será alterado para habilitar e/ou desabilitar o GroupBox onde estão os controles da tabela detalhe. procedure HabilitarDesabilitarControles;... //Código que habilita e desabilita os controles visuais //de acordo com o status da tabela mestre //sincronização da situação da tabela detalhe em função da situação da tabela mestre TabelaDetalhe.Active:=TabelaMestre.Active; //Se a tabela mestre estiver em status de inserção ou edição então os controles da //tabela detalhe devem ficar desabilitados caso contrario os controles devem ser //habilitados se a tabela detalhe estiver ativa ou desabilitados se estiver fechada if TabelaMestre.State = dsinsert then GroupBoxDadosTabelaDetalhe.Enabled := false else GroupBoxDadosTabelaDetalhe.Enabled := TabelaDetalhe.Active; //Se a tabela detalhe está ativa e não contém nenhum registro (está vazia) então //a ação de gravação da tabela detalhe deverá ficar desabilitada if TabelaDetalhe.Active and TabelaDetalhe.IsEmpty then actsalvardetalhe.enabled := false; Inclusão de um novo registro na tabela Datalhe (ação actincluirdetalhe) A inclusão de um registro na tabela detalhe ocorre normalmente através do método insert e deve atribuir ao campo que representa a relação entre as tabelas o valor da tabela mestre. Também é valido verificar se o status da tabela detalhe já não é de inclusão, neste o método insert nem precisa ser executado. Se existirem campos que precisarem ser inicializados ou então componentes que precisam receber o foco este é o momento apropriado para isso. //Se a tabela detalhe estiver ativa if TabelaDetalhe.Active then //Coloca a tabela detalhe em modo de inserção apenas se já não o estiver if TabelaDetalhe.State <> dsinsert then TabelaDetalhe.Insert; //Atribui ao campo que faz a relação entre a tabela mestre e a tabela detalhe //o valor do campo chave da tabela mestre TabelaDetalhe.FieldByName('CampoRelacaoMestreDetalhe').Value := TabelaMestre.FieldByName('CampoChave').value; //Aqui pode-se indicar componentes que receberão o foco e/ou componentes que //precisarão ser inicializados. DBEditDetalheQualquer.SetFocus; DBLkpCmbBxDetalheQualquer.ItemIndex := -1 //Após o usuário clicar no botão que faz a inserção na tabela detalhe então //a ação de salvamento deve ficar habilitada actsalvardetalhe.enabled:=true; Caderno Didático Lazarus IDE Página 82
83 Universidade Federal de Santa Maria Persistência (gravação) do registro atual da tabela detalhe (ação actsalvardetalhe) A gravação das informações da tabela detalhe vai exigir um pouco de atenção. O procedimento de gravação/persistência é simples: o comando Post grava os dados no DataSet, o comando ApplyUpdates envia os dados do DataSet para serem persistidos na base de dados e por último, a ação de Commit da transação vai efetivar os dados e encerrar a transação. Essa última situação é justamente onde se deve prestar mais atenção: quando a transação for encerrada, todos os DataSets ligados a ela serão automaticamente fechados. Isso significa, transpondo a situação para o exemplo dos ingredientes e receitas, que a cada vez que um ingrediente for incluído em uma receita o usuário teria que novamente localizar a receita para fazer uma nova inclusão ou alteração de dados. Para contornar essa situação, a cada vez que os dados da tabela detalhe forem persistidos haverá necessidade de ativar novamente as consultas e posicionar os registros. O código a seguir demonstra como isso pode ser feito: var //variáveis auxiliares para guardar informações do registro que será salvo Chave1, Chave2 : integer; //Chave1 = Chave da tabela mestre, e parte da chave // da tabela detalhe, Chave2 = chave complementar da tabela detalhe if TabelaDetalhe.State in [dsinsert, dsedit] then //Se o status da tabela detalhe for // inserção o edição //Inicia a validação dos campos do cadastro detalhe //(se forem muitos campos recomenda-se a criação de uma subrotina) if Length(Trim(DBEditQualquer.Text)) = 0 then DBEditQualquer.SetFocus; MessageDlg('Uma informação obrigatória não foi informada.', mterror, [mbok], 0); Abort; if DBLkpCmbBxQualquer.ItemIndex = -1 then DBLkpCmbBxQualquer.SetFocus; MessageDlg('Uma informação obrigatória não foi selecionada.', mterror, [mbok], 0); Abort; //Guarda as informações da chave da tabela Detalhe Chave1:= TabelaDetalhe.FieldByName('CAMPO_CHAVE_1_TABELA_DETALHE').asInteger; Chave2:= TabelaDetalhe.FieldByName('CAMPO_CHAVE_2_TABELA_DETALHE').asInteger; try //Inicia o processo de gravação TabelaDetalhe.Post; //Grava os dados no DataSet TabelaDetalhe.ApplyUpdates; //Envia os dados do DataSet para o banco de dados SQLTransaction.Commit; //Efetiva as alterações no BD e encerra a transação except //Se ocorrer um erro on E : Exception do //Mostra uma mensagem de aviso ao usuário MessageDlg('Erro de gravação', 'Não foi possível salvar. Erros: ' + #13 + E.Message, mterror, [mbok],0); SQLTransaction.Rollback; //Cancela a transação AbrirConsultas; //Ativa as consultas //Posiciona a tabela mestre no registro que ela estava antes do salvamento TabelaMestre.Locate('CampoChave', Chave1, []); //Posiciona a tabela detalhe no registro que ela estava antes do salvamento TabelaDetalhe.Locate('CampoChave1;CampoChave2', VarArrayOf([Chave1, Chave2]),[]); //uses variants HabilitarDesabilitarControles; //Atualiza a situação dos controles visuais Caderno Didático Lazarus IDE Página 83
84 Universidade Federal de Santa Maria Exclusão do registro atual na tabela Detalhe (ação actexcluirdetalhe) A exclusão de uma informação na tabela detalhe pressupõe que o usuário esteja posicionado sobre o registro ao qual deseja excluir. Assim como na ação de gravação, a ação de exclusão exigirá que a transação seja encerrada, de forma que será necessário prever uma forma de reposicionar o registro da tabela mestre var chavetbmestre: integer; //Variável auxiliar para guardar a chave da tabela mestre //Se a tabela detalhe está ativa e não estiver vazia (há algo para excluir) if (TabelaDetalhe.Active) and (not TabelaDetalhe.IsEmpty) then //Guarda em uma variável auxiliar o campo da tabela detalhe que faz relação com //a tabela mestre chavetbmestre := TabelaDetalhe.FieldByName('CampoRelacaoMestreDetalhe').asInteger; try //inicia o processo de exclusão TabelaDetalhe.Delete; //Delata o registro no DataSet TabelaDetalhe.ApplyUpdates; //Envia a exclusão para o BD SQLTransaction.Commit; //Efetiva a exclusão no BD except //Se ocorrer um erro on E : Exception do //Mostra uma mensagem de alerta para o usuário MessageDlg('Erro de exclusão', 'Não foi possível excluir o registro. Ocorreram os seguintes erros:' + #13 + E.Message, mterror, [mbok],0); //Desfaz as alterações da transação SQLTransaction.Rollback; AbrirConsultas; //Ativa as consultas //Posiciona a tabela mestre no registro que ela estava antes da exclusão TabelaMestre.Locate('CampoChave', chavetbmestre, []); HabilitarDesabilitarControles; //Atualiza a situação dos controles visuais Caderno Didático Lazarus IDE Página 84