Delphi e banco de dados orientado a objetos

By: Ricardo Barbieri

Abstract: Usando delphi Win32 e .NET para acessar bancos de dados OO

    Introdução

Este é um artigo prático, estamos preocupados em demonstrar a utilização do Delphi para acessar objetos em um banco de dados orientado a objetos. Não nos deteremos em discussões filosóficas sobre vantagens ou desvantagens desta ou daquela tecnologia, não tencionamos evangelizar ninguém, apenas, tentamos prestar um serviço àqueles desenvolvedores que já utilizam o paradigma de POO e, no entanto acabam persistindo seus objetos em um banco de dados relacional por costume ou por terem dificuldade em utilizar a persistência em um SGBD OO, já que ainda há a carência de material didático ensinando e exemplificando seu uso.

Também não tentaremos ensinar a teoria que envolve um banco de dados OO, nem mesmo os detalhes e artimanhas próprios desta tecnologia, já que isto renderia um artigo maior que este. Para os interessados neste assunto, sugiro a leitura da documentação que acompanha o banco de dados que usaremos como exemplo que é de excelente qualidade.

O banco de dados escolhido é o Caché da empresa Intersystems, vários quesitos influenciaram na escolha, dentre eles podemos citar a facilidade de download e instalação, a excelente documentação que o acompanha, a riqueza de sua linguagem própria, a forma como são criados os objetos e principalmente o fato de que ele é realmente orientado a objetos, não há mapeamento objeto-relacional ou vice-versa no acesso aos dados.

    Parte 1 - Download e instalação do Caché®

Para fazer o download do Caché vá ao site do fabricante em www.intersystems.com e localize a página de download (http://download.intersystems.com/download/ no momento em que este texto foi escrito). Nesta página você encontrará um link para efetuar seu registro junto ao fabricante, após preencher o registro você estará apto a fazer o download de uma versão totalmente funcional do Caché.

Na página seguinte clique no checkbox se você concorda com os termos da licença e escolha o produto e o tipo de download desejado (sugiro offline installation, desta forma você terá o executável para instalar em outras máquinas e até para futuras reinstalações).

Clique no botão download e será apresentada a tela com as informações do arquivo.

Após o download estar completo dê um duplo clique no arquivo CachePCkit.exe, isto lançará o descompactador. Escolha uma pasta onde os arquivos serão descompactados e clique em Next.

Hide image

Após a descompactação será questionado o idioma para instalação, e a boa surpresa é que está disponível o Português (Brasil).

Hide image

Clique em OK para prosseguir com a instalação.

Logo você será solicitado a concordar com os termos da licença, caso concorde clique em Sim para prosseguir.

Hide image

Escolha a pasta onde o Caché será instalado e clique em OK.

Hide image

Será solicitada uma confirmação para a criação da pasta escolhida, clique em Sim para prosseguir.

Hide image

Neste momento será apresentada uma tela questionando se deseja instalar o sistema com suporte a UNICODE (16 bits) ou não, caso não use caracteres de duplo byte (alguns idiomas específicos como o Japonês, por exemplo) escolha a opção padrão de 8-bit.

Hide image

Será apresentada uma tela com as informações fornecidas para conferência, caso tudo esteja de acordo clique em Avançar para prosseguir.

Hide image

Neste ponto, caso o IIS esteja em execução no sistema, o instalador solicitará permissão para pará-lo para que ele possa instalar o gateway CPS (o Caché tem um recurso fantástico para criação de paginas da WEB). Sugiro que você clique em Sim para que o instalador possa fazer seu trabalho. A partir daí o instalador entrará nas telas de progresso.

Hide image
Click to see full-sized image

Caso nenhum problema ocorra será apresentada a tela de sucesso perguntando se você deseja exibir o Getting Started (adianta eu recomendar que você leia o manual antes de começar a usar o software?), clique em Terminar.

Hide image

Neste momento o Caché já está instalado e executando em sua máquina, observe o ícone na bandeja do sistema que dá acesso às várias funcionalidades do sistema.

Como o instalador não se preocupa em apagar os arquivos temporários da instalação, sugiro que você apague a pasta C:\CacheKit e todo seu conteúdo.

Não se preocupe com o IIS, pois o instalador se encarregará de colocá-lo no ar novamente.

    Parte 2 - Criando as classes no banco

Para criar as classes no Caché inicie o Studio, isso pode ser feito pelo grupo de programas ou clicando no í isso pode ser feito pelo grupo de programas ou clicando no cone do Caché na barra de tarefas.

ou

O Studio será iniciado.

O Caché trabalha com classes dentro de pacotes, pacotes dentro de projetos e projetos dentro de namespaces, porém, não é necessário criar estes elementos nesta ordem já que o wizard nos possibilitará a criação simultânea.

Supondo que você acabou de instalar o Caché, o namespace padrão será (LOCALTCP:USER) e o projeto Default__system, caso não seja, alterne para este namespace através do menu Arquivo | Change Namespace...

Observe se você está trabalhando neste namespace

Neste namespace criaremos nosso projeto, nosso pacote e nossas classes.

Clique em Arquivo | Novo Projeto

Clique em Arquivo | Salvar Projeto, salve como “Exemplo”, observe o novo projeto

Clique com o botão direito do mouse (CD daqui em diante) sobre Classes e selecione Criar Nova Classe

Na janela que se abre informe o nome do pacote (“Exemplo” em nosso caso), o nome da classe (“Telefone”) e se desejar, uma descrição também.

Hide image

Clique Next (em algumas instalações não aparece o botão Next, se for este o caso, clique em Concluir, isto pode acontecer em outros casos, esteja avisado, daqui pra frente se não vir o botão Next clique em Concluir), Na janela que se abre selecione Serial (isto quer dizer que as instancias de Telefone serão embutidas em outras classes como Cliente, Empregado, Fornecedor, etc.), clique em Next

Hide image

Vamos fazer uma breve parada para entender as opções desta tela.

Sugiro uma consulta à documentação do Caché, mas vamos a uma breve descrição:

  • Persistente: opção para objetos que devem ter seus atributos persistidos no banco;
  • Serial: cria objetos que podem ser embutidos em outros objetos permitindo assim a criação de tipos complexos;
  • Registrada: objetos registrados, mas que não serão gravados no banco;
  • Abstrata: classe utilizada para a derivação de novas classes, não deverão existir objetos deste tipo;
  • Tipo de Dados: cria uma definição de tipo;
  • CSP: objetos que responderão a eventos http;
  • Derivada: classe que herda de outra classe, usada em conjunto com a propriedade a seguir;
  • Nome da superclasse: caso a classe seja derivada, informamos nesta janela o nome da classe ancestral (clicando no botão procurar será aberta uma janela com as classes existentes para que uma seja selecionada, veja a figura).

Reiterando, neste caso selecione Serial e Clique Next

A janela que se abre questiona se a classe possui suporte a XML e se a classe suportará população automática de dados (neste caso será criado um método que insere valores aleatórios nos objetos para teste). Deixe tudo como está (não marque nada) e clique em Finish.

Hide image

Neste ponto a classe foi criada e o arquivo aberto na área principal do programa. A seguir a definição da classe:

Class Exemplo.Telefone Extends %SerialObject [ ClassType = serial, ProcedureBlock ]
{

}

Precisamos agora inserir os atributos desta classe. Isto pode ser feito tanto diretamente no código, quanto utilizando wizards.

Antes de prosseguir vamos dar uma olhada no modelo que pretendemos implementar:

Como visto, a classe Telefone terá três atributos do tipo String (Numero, Ramal e Prefixo) e a classe Cliente terá três atributos do tipo String (Nome, Sobrenome e CPF).

Para inserir as propriedades na classe Telefone certifique-se de que o arquivo aberto no editor é o arquivo que contem a classe Telefone.

Podemos usar o menu Classe, ou os speedbuttons da barra de tarefas para inserir as propriedades:

ou

O wizard será iniciado e na primeira tela informaremos o nome da propriedade

Hide image

Clique Next

Nesta tela podemos escolher o tipo da propriedade, selecione Um único valor do tipo: e escolha %String, caso deseje outro tipo clique no botão Procurar

Existem outros tipos mais complexos à disposição, sugiro uma consulta na documentação para entendimento de todas as opções.

Clique Next

Na janela que se abre podemos ajustar parâmetros para especificar se a propriedade é Obrigatória (NOT NULL), Indexada, Chave, ou calculada, podemos ainda dar um nome opcional para ser exibido no acesso usando SQL.

Deixe tudo conforme o padrão e Clique Next;

Hide image

Na janela que se abre podemos ajustar diversos parâmetros da propriedade, mais uma vez aceitaremos o padrão, Clique Next

Hide image

Na janela que se abre podemos escolher sobrescrever os métodos Get e Set da propriedade. Estes métodos não devem ser novidade para os Analistas, já que é um padrão da Engenharia de Software, e nem para programadores Delphi, já que o Delphi segue este padrão quando criamos properties em nossas classes; de qualquer forma, uma rápida explicação: para respeitar o encapsulamento (principio de information hide neste caso), todos os campos de uma classe são privados ou protegidos e fornecemos métodos chamados Getters para leitura e Setters para escrita, alem de ocultar a implementação (black box) estes métodos permitem, por exemplo, que se faça alguma validação antes de ler ou escrever no campo especifico. Em nosso exemplo não desejamos sobrescrever os métodos. Clique Finish.

Hide image

A propriedade foi criada e nosso código deve estar como a seguir:

Class Exemplo.Telefone Extends %SerialObject [ ClassType = serial, ProcedureBlock ]
{

Property Numero As %String;
} Siga em frente e crie as outras duas propriedades (Ramal e Prefixo). Definição final da classe Telefone: Class Exemplo.Telefone Extends %SerialObject [ ClassType = serial, ProcedureBlock ]
{

Property Numero As %String;

Property Prefixo As %String;

Property Ramal As %String;

}

Salve o projeto.

Para ter certeza de que tudo está bem precisamos compilar a classe (ou diretamente o pacote, o que implica em compilar todas as classes dentro dele). Para isso, CD no nome do pacote e selecione a opção Compilar Pacote ‘Exemplo’.

Obs. Existem duas janelas que exibem informações muito parecidas no Workspace, uma é a aba Projeto, outra a aba Namespace, para compilar o pacote é necessário que você esteja na aba Projeto.

Se tudo estiver bem, será dada mensagem de sucesso e apresentada uma tela sugerindo a releitura do arquivo, aceite clicando em Yes

Vamos agora criar a classe Cliente, siga em frente e crie a classe (esta classe deverá ser do tipo Persistent e não Serial como Telefone), depois adicione as propriedades, Nome, Sobrenome e CPF.

Sua nova classe deverá estar como a seguir:

Class Exemplo.Cliente Extends %Persistent [ ClassType = persistent, ProcedureBlock ]
{

Property CPF As %String;

Property Nome As %String;

Property Sobrenome As %String;

}

    Inserindo a propriedade Telefone em Cliente

O próximo passo é inserir uma ou mais propriedades do tipo Telefone na classe Cliente.

Adicione uma nova propriedade, informe TelResidencial no nome e clique Next

Hide image

Em Um único valor do tipo: clique no botão Procurar, expanda o nó Other depois expanda o pacote Exemplo e selecione Telefone

Hide image
Hide image

Clique Finish

Salve tudo e compile o pacote.

Código final das classes sem os comentários:

Class Exemplo.Cliente Extends %Persistent [ ClassType = persistent, ProcedureBlock ]
{
Property CPF As %String;

Property Nome As %String;

Property Sobrenome As %String;

Property TelResidencial As Telefone;
}
Class Exemplo.Telefone Extends %SerialObject [ ClassType = serial, ProcedureBlock ]
{

Property Numero As %String;

Property Prefixo As %String;

Property Ramal As %String;

}

Obs: neste momento estamos trabalhando em um wizard para ser instalado na IDE do Delphi que permitirá a exportação das classes contidas em uma unit para arquivos xml no formato do Caché para que as classes sejam importadas no Caché, sem necessidade deste processo manual de criação, brevemente liberaremos uma versão beta deste wizard.


    Parte 3 – Criando a aplicação Cliente no Delphi 2005®

Na verdade, não criaremos uma aplicação cliente, mas sim, três (ou mais...). Demonstraremos como acessar a parte relacional via ODBC em Win32 e .NET e também como acessar diretamente os objetos usando COM.

Quando criamos nossas classes, o próprio Caché se encarrega de criar a parte relacional, ou seja, uma tabela para cada classe Persistent que definimos (neste caso somente a classe Cliente estará acessível via SQL, já que Telefone é Serial, e não, Persistent). Como o Delphi não possui drivers específicos para acessar o Caché, usaremos ODBC.

    3a - Cliente Win32

Crie uma nova aplicação Win32, adicione e configure os componentes como a seguir:

Componente

Propriedade

Valor

ADOTable

Name

ConnectionString

TableName

ADOtbCliente

Veja abaixo

Veja abaixo

DataSource

DataSet

ADOtbCliente

DBGrid

DataSource

Align

DataSource1

alClient

DBNavigator

DataSource

Align

DataSource1

alBottom

Vamos configurar a propriedade ConnectionString da ADOTable, para isso, usaremos o assistente.

Selecione a propriedade ConnectionString no Object Inspector (OI daqui em diante), clique no botão com as reticências que surge ao lado do nome da property para abrir o assistente, clique no botão Build... (Criar...)

Na janela que se abre selecione a aba Connection (Conexão), expanda o ComboBox da opção Use data source name (Usar o nome da fonte de dados) e selecione CACHEWEB User (supondo que você criou o pacote no namespace USER como foi recomendado)

Clique em OK nesta tela e na seguinte.

Selecione a propriedade TableName da ADOTable, na lista deverá aparecer apenas a tabela Cliente, ai existe um pequeno truque, não adianta selecionar a tabela, ao tentar ativar o componente você obterá uma mensagem de erro

Hide image

É necessário informar o pacote no qual a tabela está inserida, portanto, na propriedade TableName digite Exemplo.Cliente, agora sim você pode ativar o componente

E isso é tudo, execute seu programa e observe que o Caché já expandiu a propriedade TelResidencial exibindo todos os seus campos, insira alguns clientes na tabela para testar sua aplicação.

Não se impressione com o fato de todos os ID aparecerem como 0, o Caché atribuirá números seqüenciais corretos ao campo ID.

Projetos\Exemplo1.zip

Hide image

    3b - Cliente .NET Windows Forms

Crie uma aplicação Windows Forms para Delphi, adicione um DataGrid ao form e configure sua propriedade Dock como Fill.

Por padrão o Delphi não instala os componentes ODBC, vamos então fazer isso agora.

Clique no menu Components | Installed .NET Components...

Na janela que se abre procure por OdbcConnection, marque o CheckBox ao lado dele;

Próximo a este componente você deverá encontrar o OdbcDataAdapter, marque o CheckBox correspondente e clique em OK

Hide image
Click to see full-sized image

Os componentes foram instalados e estão na paleta Data Components, adicione um de cada ao form.

Selecione o OdbcConnection e selecione ConnectionString, clique nas reticências para abrir o assistente. Aqui há outro truque, não podemos configurar a string como fizemos na aplicação Win32, selecione a aba Connection (Conexão), marque a opção Use connection string (Usar a seqüência de conexão) e clique no botão Build... (Criar...)

Hide image

Na janela que se abre selecione a aba Machine Data Source (Fontes de dados de máquina) e ai sim, selecione CACHEWEB User

Hide image

Clique em OK nas duas telas para fechar o assistente.

Selecione o OdbcDataAdapter , expanda a propriedade SelectCommand, em Connection selecione o OdbcConnection e em CommandText digite “select * from Exemplo.Cliente”;

Adicione um componente DataSet ao form;

Vamos agora inserir o seguinte código no evento Create do form:

constructor TWinForm.Create;
begin
  inherited Create;
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent;
  //
  // TODO: Add any constructor code after InitializeComponent call
  //

  OdbcConnection1.Open;
  OdbcDataAdapter1.Fill(DataSet1, 'Cliente');
  DataGrid1.DataSource:= DataSet1.Tables['Cliente'];
end;

Isso é tudo, execute a aplicação.

Projetos\Exemplo2.zip

Hide image

    Parte 4 - Acessando os objetos usando COM

Ok Barbieri, muito bom, mas até agora só vi acesso a BD relacional... Para usar relacional eu uso o Interbase que é um BD espetacular! Onde estão os objetos?

Vamos construir agora a aplicação que acessa diretamente os objetos do Caché usando COM. para isso, crie uma nova aplicação Win32 e monte o form como a figura a seguir:

Para podermos acessar o servidor COM do Caché vamos importar a Type Library dele.

Menu Component | Import Component, selecione Import a Type Library e clique em Next

Hide image

Em Description localize CacheObject, selecione-o e clique Next

Na janela que se abre em Unit dir Name, aponte para a pasta do seu projeto ou para uma pasta que esteja no Browsing Path do Delphi, clique Next, na janela seguinte, deixe marcada a opção Create Unit e clique em Finish. O Delphi importou a Type Library e criou uma unit (CacheObject_TLB.pas) para ela.

Precisamos adicionar esta unit à cláusula uses de nosso form, adicione no uses da Interface já que usaremos um tipo contido nesta unit ainda na declaração da classe do nosso form. Adicione também a unit ComObj à cláusula uses (Interface ou Implementation).

Sugiro que você dê uma boa olhada nesta unit para se familiarizar com as interfaces e métodos exportados pela TLB.

Adicione um campo chamado Fabrica do tipo IFactory na seção private da classe do form

type
  TForm1 = class(TForm)
    btConectar: TButton;
    btLocalizar: TButton;
    edtID: TEdit;
    Label2: TLabel;    
  private
    { Private declarations }
    Fabrica: IFactory;
  public
    { Public declarations }
  end;

Vamos agora programar o manipulador de evento do click do botão btConectar

procedure TForm1.btConectarClick(Sender: TObject);
begin
  Fabrica:= CreateComObject(CLASS_Factory) as IFactory;

  if not Fabrica.Connect('cn_iptcp:127.0.0.1[1972]:USER') then
    ShowMessage('Não foi possível estabelecer conexão');
end;

Na primeira linha nós nos conectamos ao servidor COM e podemos a partir de agora invocar os métodos declarados na interface IFactory.

Na segunda linha invocamos o método Connect passando para ele uma string que contem o socket da conexão mais a base com a qual desejamos estabelecer a conexão.

Neste momento já estamos conectados e com acesso a base User do Caché, podemos agora acessar seus objetos.

No click do botão btLocalizar invocaremos o método OpenID que recebe como parâmetro o nome da classe, o ID do objeto e o tipo de concorrência que será usado nesta consulta (0: sem bloqueio, 1: atômico, 2: compartilhado, 3: compartilhado, mas retido, 4: acesso exclusivo, veja a documentação para mais detalhes).

procedure TForm1.btLocalizarClick(Sender: TObject);
var
  Cliente: Variant;
begin
  try
    Cliente:= Fabrica.OpenId('Exemplo.Cliente', edtID.Text, 1);
  except
    on E: Exception do
      ShowMessage(E.Message);
  end;

  if VarIsNull(Cliente) or VarIsClear(Cliente) then
    Exit;

  ShowMessage('Nome: ' + Cliente.Nome + #13 +
              'Sobrenome: ' + Cliente.Sobrenome + #13 +
              'CPF: ' + Cliente.CPF + #13 +
              'TelRes Numero: ' + Cliente.TelResidencial.Numero + #13 +
              'TelRes Ramal: ' + Cliente.TelResidencial.Ramal + #13 +
              'TelRes Prefixo: ' + Cliente.TelResidencial.Prefixo);
end;

pronto, isto é tudo, execute o programa, clique em Conectar, depois, digite um ID valido no edit e clique em Localizar ID.

Projetos\Exemplo3.zip

    Parte 5 – Criando uma Query na classe

Como as classes no Caché são realmente classes e não meras tabelas ou deposito de dados, podemos inserir métodos nestas classes, mas como as classes no Caché não são meras classes, mas também poderosas tabelas de banco de dados (desculpem, mas não resisti). Vamos inserir uma query que receberá o nome do cliente como parâmetro e retornará seus dados.

Inicie o Studio, abra o projeto Exemplo, abra a classe cliente e adicione uma query como descrito a seguir:

Query PorNome(oNome As %String = "") As %SQLQuery(CONTAINID = 1, ROWSPEC = "ID:%Integer,Nome,Sobrenome,CPF,Tel_Num,Tel_Pref,Tel_Ramal", SELECTMODE = "RUNTIME")
{
SELECT ID, Nome, Sobrenome, CPF, TelResidencial_Numero,
TelResidencial_Prefixo, TelResidencial_Ramal
FROM Exemplo.Cliente
WHERE (Nome %STARTSWITH :oNome)
ORDER BY Nome
}

A seguir a definição completa da classe Cliente:

Class Exemplo.Cliente Extends %Persistent [ ClassType = persistent, ProcedureBlock ]
{

Property CPF As %String;
Property Nome As %String;
Property Sobrenome As %String;
Property TelResidencial As Telefone;

Query PorNome(oNome As %String = "") As %SQLQuery(CONTAINID = 1, ROWSPEC = "ID:%Integer,Nome,Sobrenome,CPF,Tel_Num,Tel_Pref,Tel_Ramal", SELECTMODE = "RUNTIME")
{
SELECT ID, Nome, Sobrenome, CPF, TelResidencial_Numero,
TelResidencial_Prefixo, TelResidencial_Ramal
FROM Exemplo.Cliente
WHERE (Nome %STARTSWITH :oNome)
ORDER BY Nome
}

}

Não esqueça de compilar o pacote antes de prosseguir!

Agora iremos montar uma aplicação mais sofisticada utilizando um pouco mais dos recursos do Caché (e isso é só a ponta do iceberg). Neste exemplo além da consulta (que já foi demonstrada no exemplo anterior), iremos utilizar uma query e fazer a inserção de um novo cliente.

Veja na figura o form de nossa aplicação (Win32):

Hide image
Click to see full-sized image

Componente

Propriedade

Valor

Button

Name

Caption

btTrocarBase

Trocar Base

Button

Name

Caption

btLocID

Localizar

Button

Name

Caption

btLocNomeParte

Localizar

Button

Name

Caption

btInserir

Inserir

Edit

Name

edtLocID

Edit

Name

edtLocNomeParte

Edit

Name

edtNome

Edit

Name

edtSobrenome

Edit

Name

edtCPF

Edit

Name

edtTelPrefixo

Edit

Name

edtTelNumero

Edit

Name

edtTelRamal

ListBox

Name

ListBox1

GroupBox

Name

Caption

GroupBox1

Cliente

GroupBox

Name

Caption

GroupBox2

Telefone Residencial

8 Label

Caption

Localizar Cliente com ID =

Parte do Nome:

Nome:

Sobrenome:

CPF:

Prefixo:

Número:

Ramal:

Uma breve observação sobre a metodologia utilizada:

Neste exemplo utilizamos programação procedural com dois intuitos, o primeiro é o de não desviar o foco de atenção da utilização dos recursos do Caché para os da POO, o segundo é não aumentar ainda mais este texto, porém, em uma parte futura deste artigo criaremos um exemplo usando POO, o fonte já está disponível para download junto aos demais exemplos ( Projetos\Exemplo7.zip ).

Vamos então ao código da nossa aplicação. Caso você tenha colocado a unit com a importação da Type Library em uma pasta que esteja no Browsing Path do Delphi, você só precisa adicioná-la à cláusula uses da unit do form (uses da interface), caso contrário, copie a unit para dentro da pasta do seu projeto atual (CacheObject_TLB.pas).

Na seção private do form declare um campo fabrica do tipo IFactory:

……
  private
    { Private declarations }
    Fabrica: IFactory;
  public
    { Public declarations }
  end;

Adicione a unit ComObj à cláusula uses (interface ou implementation).

No evento OnCreate do form colocaremos o código que estabelece a conexão ao banco e à base de dados:

procedure TForm1.FormCreate(Sender: TObject);
begin  
  Fabrica:= CreateComObject(CLASS_Factory) as IFactory;
  if not Fabrica.Connect('cn_iptcp:127.0.0.1[1972]:USER') then
    ShowMessage('Não foi possível estabelecer conexão');
end;

No click do botão btTrocarBase demonstramos como exibir uma janela para que o usuário possa se conectar a outra base de dados:

procedure TForm1.btTrocarBaseClick(Sender: TObject);
begin
  Fabrica.Connect(Fabrica.ConnectDlg('Conectar-se a:'));
end;

No click do botão btLocID exibimos em um ShowMessage os dados do cliente que possui o ID informado no edtLocID:

procedure TForm1.btLocIDClick(Sender: TObject);
var
  Cliente: Variant;
begin
  try
    Cliente:= Fabrica.OpenId('Exemplo.Cliente', edtLocID.Text, 1);
  except
    on E: Exception do
      ShowMessage(E.Message);
  end;

  if VarIsNull(Cliente) or VarIsClear(Cliente) then Exit;

  ShowMessage('Nome: ' + Cliente.Nome + #13 +
              'Sobrenome: ' + Cliente.Sobrenome + #13 +
              'CPF: ' + Cliente.CPF + #13 +
              'TelRes Numero: ' + Cliente.TelResidencial.Numero + #13 +
              'TelRes Ramal: ' + Cliente.TelResidencial.Ramal + #13 +
              'TelRes Prefixo: ' + Cliente.TelResidencial.Prefixo);
end;

No click do botão btLocNomeParte fazemos a chamada à query que criamos no Caché, esta chamada retornará um ResultSet com os dados do(s) Cliente(s) cujo nome inicie com as letras digitadas no edtLocNomeParte, usaremos o ListBox1 para exibir estes dados.

procedure TForm1.btLocNomeParteClick(Sender: TObject);
var
  ResultSet: IResultSet;
  i: Integer;
  s: string;
begin
  ListBox1.Clear;  
  ResultSet:= Fabrica.ResultSet('Exemplo.Cliente', 'PorNome') as IResultSet;

  Variant(ResultSet).Execute(edtLocNomeParte.Text);

  while ResultSet.Next do
  begin
    s:= '';
    for i:= 1 to ResultSet.GetColumnCount do
      s:= s + ' ' + ResultSet.GetColumnName(i) + ': ' +
        ResultSet.GetDataAsString(i);
      ListBox1.Items.Add(s);
  end;
       
  ResultSet.Close;
end;  

Neste código trabalhamos com um campo Fabrica que é do tipo IFactory e representa um objeto do tipo Factory do Caché, e também com uma variável ResultSet do tipo IResultSet que representa um objeto do tipo ResultSet do Caché (sugiro uma consulta a essas duas classes na documentação), como a fabrica já esta conectada, chamamos o método ResultSet informando o objeto e o nome da query que desejamos acessar, o resultado será colocado na var ResultSet, agora, podemos chamar o método Execute do ResultSet que recebe como parâmetro o conteúdo do edtLocNomeParte, esta consulta retornará todos os clientes cadastrados cujo nome inicie com as letras passadas como parâmetro, caso deixemos o edit em branco serão retornados todos os clientes.

De posse dos dados podemos percorrê-los usando Next. GetColumnCount retorna o número de colunas contido em cada linha do resultado e ResultSet.GetColumnName retorna o nome de cada coluna, GetDataAsString é que realmente traz os dados (é já os traz convertidos em string), tudo isso está sendo gravado em uma string e após obtermos todas as informações para cada linha, inserimos o conteúdo da string no ListBox.

Para inserir um novo objeto no banco é ainda mais fácil, observe o código do botão btInserir:

procedure TForm1.btInserirClick(Sender: TObject);
var
  Cliente: Variant;
begin
  //cria um novo
  Cliente:= Fabrica.New('Exemplo.Cliente', True);

  Cliente.Nome:= edtNome.Text;
  Cliente.Sobrenome:= edtSobrenome.Text;
  Cliente.CPF:= edtCPF.Text;
  Cliente.TelResidencial.Numero:= edtTelNumero.Text;
  Cliente.TelResidencial.Prefixo:= edtTelPrefixo.Text;
  Cliente.TelResidencial.Ramal:= edtTelRamal.Text;

  Cliente.Sys_Save;
  Cliente.Sys_Close;
  Cliente:= NULL;
end;

Chamamos o método New de Factory para obter uma nova instância, a partir daí é só passar os valores para as propriedades, por fim salvamos as informações, e isso é tudo, tudo muito simples e fácil de usar. Veja imagens do programa em execução:

Projetos\Exemplo4.zip

Bem, promessa cumprida, ai estão as três aplicações acessando os objetos do Caché. Porém, como um bônus, na próxima parte demonstraremos como publicar aquela query que criamos na classe Cliente como um método de um web service...

    Parte 6 – Publicando a Query como um método em um Webservice

Isso mesmo, podemos criar web services usando o Caché, para isso, devemos fazer nossa classe herdar da classe SOAP.WebService. Mas Barbieri... A minha classe Cliente já herda de Persistent, se eu herdar de outra classe não vou conseguir persistir os dados!

Calma, diferentemente do Delphi, o Caché suporta herança múltipla e não será nenhum problema faze-la herdar de SOAP.WebService.

Inicie o Studio, abra o projeto Exemplo, expanda o pacote Exemplo e abra a classe Cliente no editor.

Vamos modificar o código para dizer que a classe herda de Persistent e SOAP.WebService, para isso altere a definição da classe como a seguir:

Class Exemplo.Cliente Extends (%Persistent, %SOAP.WebService) [ ClassType = persistent, ProcedureBlock ]

Neste momento você não conseguirá mais compilar sua classe, pois uma classe que gerará um web service deve expor web methods e pelo menos um servicename

Hide image

Após a property TelResidencial e antes da query Por nome adicione as linhas a seguir:


Parameter NAMESPACE = "http://tempuri.org";

Parameter SERVICENAME = "PorNome";

Além disso, precisamos dizer que a query PorNome agora é um WebMethod, para isso adicione a informação [ WebMethod ] ao fim de sua definição:

Query PorNome(oNome As %String = "") As %SQLQuery(CONTAINID = 1, ROWSPEC = "ID:%Integer,Nome,Sobrenome,CPF,Tel_Num,Tel_Pref,Tel_Ramal", SELECTMODE = "RUNTIME") [ WebMethod ]

Agora compile o pacote.

Reinicie seu computador antes de prosseguir!

Inicie o Studio, abra o projeto Exemplo, abra a classe Cliente.

Vá até o menu Visualizar e selecione Página da Web

Lembre-se que a página aberta no editor deve ser a página que contém a classe Cliente.

Nesse momento o browser se abrirá e você verá uma página informando dados do web service, inclusive o serviço disponível (PorNome):

Clique no link PorNome e uma nova página se abrirá:

Informe um nome, ou parte de um nome e clique em Invoke, uma página com o conteúdo do arquivo XML gerado será exibida no browser com os dados de todos os clientes que coincidiram com a solicitação.

Bem, agora é só construir uma aplicação para consumir este web service. Neste caso criaremos uma aplicação Delphi Windows Forms e adicionaremos um TextBox, um Button e um DataGrid ao form, veja a figura:

O próximo passo é importar o WSDL para que possamos ter uma classe proxy em nossa aplicação, o que nos permitirá chamar o método no web service. Para isso, precisamos saber o endereço do WSDL, a maneira mais fácil de conseguir esse endereço é novamente no Studio selecionar o menu Visualizar, clicar em Página Web e após a página abrir no browser clicar no link Service Description, isso fará abrir uma nova página que é o WSDL, copie a url que aparece na barra de endereço do browser:

De volta ao Delphi, abra o Project Manager e clique com o botão direito sobre o nome do projeto e selecione a opção Add Web Reference...

Na janela que se abre, cole o endereço que copiamos na barra de endereços e clique na seta azul (Go)

Uma janela se abrirá contendo o documento WSDL, clique em Add Reference

Volte ao Project Manager e veja que a referencia foi adicionada e que alguns arquivos foram criados, entre eles o arquivo WebReference.Exemplo.pas, este arquivo contém a classe proxy que precisamos, portanto, adicione esta unit à clausula uses da unit do form (Alt+F11), pronto, agora temos tudo que precisamos para consumir o web service, vamos então programar o click do botão, veja o código:

procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Dados: PorNome;
  DataSetDados: DataSet;
begin
  Dados:= PorNome.Create;
  DataSetDados:= Dados.PorNome(TextBox1.Text);
  DataGrid1.DataSource:= DataSetDados;
  DataGrid1.DataMember:= DataSetDados.Tables[0].ToString;
end;

Criamos duas variáveis, uma do tipo PorNome (a nossa classe proxy. Dê uma olhada no arquivo gerado com a importação do WSDL para confirmar o nome da classe) que possui um método chamado PorNome que recebe uma string como parâmetro e retorna um Dataset. A outra variável é do tipo Dataset, poderíamos ter colocado um componente Dataset no form, mas já que o método retorna um dataset, é totalmente desnecessária a existência deste componente, uma variável do tipo (que nem precisa ser instanciada) é o suficiente.

O próximo passo é chamar o construtor, depois disso atribuímos o retorno do método à nossa variável Dataset, depois, vinculamos esse dataset ao datagrid, dizemos qual membro do Dataset ele exibirá e isso é tudo, simples assim, veja o programa em funcionamento:

Hide image

Projetos\Exemplo5.zip

    Bônus

Como este é um assunto empolgante vamos presentear o leitor com um bônus, vamos construir uma aplicação ASP.NET para consumir o Webservice.

Crie uma aplicação ASP.NET Web Application – Delphi for .NET, sugiro que você crie uma pasta dentro de C:\Inetpub\wwwroot para salvar seu projeto.

Siga os passos explicados anteriormente e adicione a web reference ao projeto;

Adicione um TextBox, um Button e um DBWebGrid à página e posicione-os como na figura:

Vamos agora programar o manipulador do click do botão:

procedure TWebForm1.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Dados: PorNome;
  DatasetDados: Dataset;
  DataSource: DBWebDataSource;
begin
  Dados:= PorNome.Create;
  DataSource:= DBWebDataSource.Create;
  DatasetDados:= Dados.PorNome(TextBox1.Text);
  DataSource.DataSource:= DatasetDados;
  DBWebGrid1.DBDataSource:= DataSource;
  DBWebGrid1.TableName:= DatasetDados.Tables[0].ToString;
end;

Infelizmente a diversão acabou, nosso programa está pronto, execute-o, informe um nome ou parte de um nome no Textbox e click no botão. Veja uma imagem da página no IE:

Projetos\Exemplo6.zip

    Parte 7 – Um exemplo OO com objetos COM

Conforme prometido, nesta parte demonstraremos como criar uma aplicação OO utilizando classes Delphi para acessar os objetos do Caché via COM.

Não é escopo deste artigo abordar técnicas de análise ou modelagem OO, utilizaremos o mínimo de código possível sem ferir as regras do paradigma.

Utilizaremos a mesma IU (Interface do Usuário) que construímos na parte 5, e estou assumindo que você leu as partes anteriores e já importou a type library do Caché no seu projeto.

Adicione uma unit ao projeto e salve-a como uTelefone, nesta unit criaremos nossa classe TTelefone; a declaração desta classe está na listagem a seguir:

type
  TTelefone = class
  private
    FPrefixo: string;
    FNumero: string;
    FRamal: string;
    procedure SetNumero(const Value: string);
    procedure SetPrefixo(const Value: string);
    procedure SetRamal(const Value: string);
  public
    property Numero: string read FNumero write SetNumero;
    property Prefixo: string read FPrefixo write SetPrefixo;
    property Ramal: string read FRamal write SetRamal;
  end;

Adicione uma unit ao projeto e salve-a como uCliente, nesta unit criaremos nossa classe TCliente; o código desta unit está na listagem a seguir:

unit uCliente;

interface

uses uTelefone;

type
  TCliente = class
  private
    FSobrenome: string;
    FCPF: string;
    FNome: string;
    FTelResidencial: TTelefone;
    procedure SetCPF(const Value: string);
    procedure SetNome(const Value: string);
    procedure SetSobrenome(const Value: string);
    procedure SetTelResidencial(const Value: TTelefone);
  public
    property Nome: string read FNome write SetNome;
    property Sobrenome: string read FSobrenome write SetSobrenome;
    property CPF: string read FCPF write SetCPF;
    property TelResidencial: TTelefone read FTelResidencial write SetTelResidencial;
    constructor Create;
    destructor Destroy; override;
  end;

implementation

{ TCliente }

//os metodos escritores (Setxxx) foram omitidos ja que sao criados automaticamente

constructor TCliente.Create;
begin
  inherited;
  FTelResidencial:= TTelefone.Create;
end;

destructor TCliente.Destroy;
begin
  FTelResidencial.Free;
  inherited;
end;

end.

Como você deve ter notado estas classes espelham exatamente as classes que criamos no Caché, claro que estas classes poderiam ter mais atributos e métodos que as classes Caché, afinal, as classes Caché possuem somente os atributos que serão persistidos.

    Cuidando da persistência

Para manter a coesão de nossas classes de negocio criaremos uma classe extra que cuidará do mapeamento de nossos objetos Delphi em objetos Caché, esta classe será responsável por carregar o estado do objeto Caché no objeto Delphi (processo geralmente chamado de materialização) e de carregar o estado de um objeto Delphi em um objeto Caché (persistência), além de interfacear todo tipo de comunicação com a camada de persistência.

Esta indireção é necessária para manter a coesão de nossas classes de negocio (como dito anteriormente) e também para permitir uma migração da tecnologia de armazenamento (DB) sem nenhuma interferência nas classes de negocio. Resumidamente, se a própria classe TCliente cuidar de sua persistência ela estará acoplada a objetos como DataSets, DataSources e instruções SQL que podem ser especificas de determinadas tecnologias (BDE, DbExpress, ADO) ou softwares específicos (Oracle, Interbase), caso seja necessário migrar de tecnologia ou software, será necessário alterar a classe de negocio TCliente, o que pode causar efeitos colaterais indesejáveis.

A separação das camadas de interesse é outro motivo para justificar a criação desta classe de persistência.

A técnica mais recomendada (a meu ver) é a criação de uma interface com os métodos de persistência, assim, toda vez que precisarmos substituir alguma coisa da camada de persistência (tecnologia ou software) estaremos bastante à vontade para fazê-lo.

Vale a pena ressaltar que às vezes um pouco de trabalho extra inicial pode nos poupar enorme quantidade de esforço e tempo futuramente na hora da migração, pense sempre no futuro, já que, como alguém já disse “é muito difícil fazer previsões, principalmente a respeito do futuro”.

Mãos a massa, adicione uma unit ao projeto e salve-a como uIntPersistencia, nesta unit criaremos nossa classe interface IPersistencia, que possuirá os métodos que toda classe que se propuser a persistir um objeto do tipo TCliente devera implementar. A declaração da interface está na listagem a seguir:

unit uIntPersistencia;

interface

uses
  Classes;

type
  IPersistencia = interface
    function Gravar(obj: TObject; const TableName: string): boolean;
    function Recuperar(ID: variant; const TableName: string): TObject;
    function CarregarPorNome(aLista: TList;
      const str, TableName: string): integer;
  end;

implementation

end.
 
Adicione uma unit ao projeto e salve-a como uPersistencia, nesta unit criaremos nossa classe TClienteDB, esta classe irá implementar IPersistencia especificamente para interagir com o Caché usando COM; o código desta unit está na listagem a seguir:

unit uPersistencia;

interface

uses CacheObject_TLB, uCliente, Classes, uIntPersistencia;

type
  TClienteDB = class(TInterfacedObject, IPersistencia)
  private
    Fabrica: IFactory;
  public
    destructor Destroy; override;
    function CarregarPorNome(aLista: TList; const str: string;
      const TableName: string): Integer;
    function Gravar(obj: TObject; const TableName: string): Boolean;
    function Recuperar(ID: Variant; const TableName: string): TObject;
    constructor Create;
  end;

implementation

uses Variants, SysUtils, Dialogs, ComObj;

{ TClienteDB }

function TClienteDB.CarregarPorNome(aLista: TList; const str,
  TableName: string): Integer;
var
  ResultSet: IResultSet;
  Cliente: TCliente;
  strMetodo: string;
begin
  Result:= 0;

  strMetodo:= 'PorNome';
  ResultSet:= Fabrica.ResultSet(TableName, strMetodo) as IResultSet;
  ResultSet.Execute(str,EmptyStr,EmptyStr,EmptyStr,EmptyStr,EmptyStr,
            EmptyStr,EmptyStr,EmptyStr,EmptyStr,EmptyStr,EmptyStr,
            EmptyStr,EmptyStr,EmptyStr,EmptyStr);

  while ResultSet.Next do
  begin
    Cliente:= TCliente.Create;
    with Cliente do
      begin
        Nome:= ResultSet.GetDataByName('Nome');
        Sobrenome:= ResultSet.GetDataByName('Sobrenome');
        CPF:= ResultSet.GetDataByName('CPF');
        TelResidencial.Numero:= ResultSet.GetDataByName('Tel_Num');
        TelResidencial.Prefixo:= ResultSet.GetDataByName('Tel_Pref');
        TelResidencial.Ramal:= ResultSet.GetDataByName('Tel_Ramal');
      end;
    aLista.Add(Cliente);
  end;

  ResultSet.Close; 
  Result:= aLista.Count;
end;


constructor TClienteDB.Create;
begin
  Fabrica:= CreateComObject(CLASS_Factory) as IFactory;
  if not Fabrica.Connect('cn_iptcp:127.0.0.1[1972]:USER') then
    ShowMessage('Não foi possível estabelecer conexão');
end;

destructor TClienteDB.Destroy;
begin
  Fabrica.Disconnect;
  Fabrica:= nil;
  inherited;
end;

function TClienteDB.Gravar(obj: TObject; const TableName: string): Boolean;
var
  Cliente: Variant;
begin
  Result:= False;
  Cliente:= Fabrica.New(TableName, True);

    with TCliente(obj) do
    begin
      Cliente.Nome:= Nome;
      Cliente.Sobrenome:= Sobrenome;
      Cliente.CPF:= CPF;
      Cliente.TelResidencial.Numero:= TelResidencial.Numero;
      Cliente.TelResidencial.Prefixo:= TelResidencial.Prefixo;
      Cliente.TelResidencial.Ramal:= TelResidencial.Ramal;

      Cliente.Sys_Save;
      Cliente.Sys_Close;
      Cliente:= NULL;
    end;
  Result:= True;
end;

function TClienteDB.Recuperar(ID: Variant; const TableName: string): TObject;
var
  Cliente: Variant;
begin
  Result:= TCliente.Create;
  try
    Cliente:= Fabrica.OpenId(TableName, ID, 1);
  except
    on E: Exception do
      ShowMessage(E.Message);
  end;

  if VarIsNull(Cliente) or VarIsClear(Cliente) then Exit;
  
  with TCliente(Result) do
    begin
      Nome:= Cliente.Nome;
      Sobrenome:= Cliente.Sobrenome;
      CPF:= Cliente.CPF;
      TelResidencial.Numero:= Cliente.TelResidencial.Numero;
      TelResidencial.Ramal:= Cliente.TelResidencial.Ramal;
      TelResidencial.Prefixo:= Cliente.TelResidencial.Prefixo;
    end;
end;

end.

Esta classe implementa os métodos da interface descritos a seguir:

function TClienteDB.Gravar(obj: TObject; const TableName: string): Boolean;

Método que mapeará um objeto Delphi para um objeto Caché, este método recebe como primeiro parâmetro um objeto do tipo TObject (embora saibamos que neste parâmetro será enviado um objeto do tipo TCliente o uso de TObject permite uma evolução das classes que implementam esta interface para trabalharem com outros tipos de objeto, futuro... sempre futuro), este objeto deverá estar devidamente instanciado e com seus valores já preenchidos, o segundo parâmetro é o nome da classe Caché para a qual este objeto será mapeado (usamos TableName já que esta interface poderá ser implementada por classes que acessem bancos de dados relacionais).

Este método retorna um boolean que informa o sucesso da operação.

function TClienteDB.Recuperar(ID: Variant; const TableName: string): TObject;

Método que mapeará um objeto do Caché em um objeto Delphi, este método recebe como primeiro parâmetro um integer que é o ID do objeto Caché (o Caché cria um atributo ID para todas as classes, mas pode ser tambem a chave primaria de uma tabela relacional, por isso ele é do tipo variant, flexibilidade... sempre flexibilidade), o segundo parâmetro é o nome da classe Caché da qual este objeto será mapeado.

Este método retorna um objeto do tipo TObject (embora nesta classe seja sempre um TCliente. Os motivos de usar TObject já foram explicados) devidamente instanciado e com seu estado devidamente recuperado do objeto Caché identificado pelo ID.

function TClienteDB.CarregarPorNome(aLista: TList; const str,

TableName: string): Integer;

Este método acessará a query “PorNome” criada na classe Cliente no Caché, este método recebe como primeiro parâmetro um objeto do tipo TList onde serão retornados objetos do tipo TCliente que atendam a consulta (clientes com nomes que iniciem com a string passada em str), o segundo parâmetro é uma string que será repassada à consulta existente na classe Caché, o terceiro parâmetro é o nome da classe Caché da qual os objetos serão mapeados.

Este método retorna um integer com o numero de objetos materializados.

    Métodos da IU

Vamos aos métodos do form.

Declare as units necessárias na clausula uses:

uses uIntPersistencia, uPersistencia;

Programamos o click do botão de localizar cliente pelo ID:

procedure TForm1.btLocIDClick(Sender: TObject);
var
  Cliente: TCliente;
  ClienteDB: IPersistencia;
begin
  ClienteDB:= TClienteDB.Create;
  Cliente:= TCliente(ClienteDB.Recuperar(StrToInt(edtLocID.Text), Classe));

  ShowMessage('Nome: ' + Cliente.Nome + #13 +
              'Sobrenome: ' + Cliente.Sobrenome + #13 +
              'CPF: ' + Cliente.CPF + #13 +
              'TelRes Numero: ' + Cliente.TelResidencial.Numero + #13 +
              'TelRes Ramal: ' + Cliente.TelResidencial.Ramal + #13 +
              'TelRes Prefixo: ' + Cliente.TelResidencial.Prefixo);

  Cliente.Free;
end;

Observe que ClienteDB é do tipo da interface (IPersistencia) e não da classe que a implementa (TClienteDB), porem, na hora de instanciarmos o objeto, usamos o construtor de TClienteDB.

Este artifício nos permite instanciar qualquer classe que implemente IPersistencia, desacoplando dessa forma as classes cliente e servidora, veremos um exemplo em breve.

A seguir programamos o click do botão que dispara a consulta por parte do nome:

procedure TForm1.btLocNomeParteClick(Sender: TObject);
var
  ClienteDB: IPersistencia;
  Lista: TList;
  i: Integer;
begin
  ClienteDB:= TClienteDB.Create;
  Lista:= TList.Create;
  ListBox1.Clear;

  if (ClienteDB.CarregarPorNome(Lista, edtLocNomeParte.Text, Classe)) > 0 then

   for i:= 0 to Lista.Count -1 do
    begin      
      with TCliente(Lista[i]) do
      begin
      ListBox1.Items.Add(
        'Nome: ' + Nome +
        ' | Sobrenome: ' + Sobrenome +
        ' | CPF: ' + CPF +
        ' | TelNum: ' + TelResidencial.Numero +
        ' | TelPref: ' + TelResidencial.Prefixo +
        ' | TelRam: ' + TelResidencial.Ramal);
      end;
    end;

  Lista.Free;
end;

Por fim, o código do click do botão que insere um novo cliente no banco:

procedure TForm1.btInserirClick(Sender: TObject);
var
  Cliente: TCliente;
  ClienteDB: IPersistencia;
begin
  Cliente:= TCliente.Create;
  ClienteDB:= TClienteDB.Create;

  with Cliente do
    begin
      Nome:= edtNome.Text;
      Sobrenome:= edtSobrenome.Text;
      CPF:= edtCPF.Text;
      TelResidencial.Numero:= edtTelNumero.Text;
      TelResidencial.Prefixo:= edtTelPrefixo.Text;
      TelResidencial.Ramal:= edtTelRamal.Text;
    end;

  if ClienteDB.Gravar(Cliente, Classe) then
    ShowMessage('Cliente gravado')
  else
    ShowMessage('Não foi possível gravar o Cliente');

  Cliente.Free;
end;

O código é bastante claro e auto explicativo, mesmo assim inserimos alguns comentários no fonte disponibilizado para download.

( Projetos\Exemplo7.zip ).


    Parte 8 – Utilizando uma consulta SQL com objetos COM

Embora o Caché nos permita criar métodos e consultas em nossas classes (como visto anteriormente), a maioria dos programadores está ainda muito habituada a utilizar o SQL, a boa noticia é que mesmo trabalhando orientado a objetos utilizando COM para acessar os dados no Caché podemos continuar enviando nossas instruções SQL para o Caché. Para isso devemos utilizar a interface IResultSet (já vista anteriormente), a qual nos permite enviar instruções SQL e manipular o ResultSet.

Utilizaremos a aplicação criada na parte 7 para este exemplo, modifique a IU conforme a figura:

Foram adicionados um Label, um Edit (edtSQL) e um Botão (btExecSQL).

Abra a unit uIntPersistencia e adicione o método a seguir à interface IPersistencia:

function ExecutarSQL(var aLista: TList; const aSQL: string): integer;

Este método recebe como primeiro parâmetro um objeto do tipo TList onde serão retornados objetos do tipo TCliente que atendam a consulta SQL enviada como uma string no segundo parâmetro.

Este método retorna um integer com o numero de objetos materializados.

Abra a unit uPersistencia, posicione o cursor em uma linha em branco da classe TClienteDB e tecle Ctrl + Space para abrir o Code Completion, deverá aparecer o método inserido na interface, selecione-o e tecle Enter, o Delphi colocará sua declaração na classe, tecle Shift + Ctrl + C para que o Delphi gere o esqueleto do método e implemente-o como a seguir:

function TClienteDB.ExecutarSQL(var aLista: TList; const aSQL: string): Integer;
var
  ResultSet: IResultSet;
  Cliente: TCliente;
begin
  Result:= 0;
  
  ResultSet:= Fabrica.DynamicSQL(aSQL) as IResultSet;
  Variant(ResultSet).Execute;

  while ResultSet.Next do
  begin
    Cliente:= TCliente.Create;
    try
      with Cliente do
        begin
          Nome:= ResultSet.GetDataByName('Nome');
          Sobrenome:= ResultSet.GetDataByName('Sobrenome');
          CPF:= ResultSet.GetDataByName('CPF');
          TelResidencial.Numero:=
            ResultSet.GetDataByName('TelResidencial_Numero');
          TelResidencial.Prefixo:=
            ResultSet.GetDataByName('TelResidencial_Prefixo');
          TelResidencial.Ramal:=
            ResultSet.GetDataByName('TelResidencial_Ramal');
        end;
    except

    end;
    aLista.Add(Cliente);
  end;

  ResultSet.Close;
  Result:= aLista.Count;
end;

Apenas chamamos o método DynamicSQL de IResultSet passando como parâmetro a string recebida (que deve conter uma instrução SQL válida, como por exemplo: select * from exemplo.cliente where ID = '3' (desde que sua instancia possua ao menos 3 clientes cadastrados)).

Sempre forneça o nome da classe antecedido pelo nome do pacote onde ela se encontra, ex: Exemplo.Cliente, onde Exemplo é o nome do pacote e Cliente o nome da classe.

O código para utilizar este metodo é praticamente o mesmo que o utilizado para a consulta “PorNome” vista anteriormente e deve ser inserido no click do btExecSQL:

procedure TForm1.btExecSQLClick(Sender: TObject);
var 
  ClienteDB: IPersistencia;
  Lista: TList;
  i: Integer;
begin
  ClienteDB:= TClienteDB.Create;
  Lista:= TList.Create;

  ClienteDB.ExecutarSQL(Lista, edtSQL.Text);

  ListBox1.Clear;
  for i:= 0 to Lista.Count -1 do
    begin
      with TCliente(Lista[i]) do
      begin
      ListBox1.Items.Add(
        'Nome: ' + Nome +
        ' | Sobrenome: ' + Sobrenome +
        ' | CPF: ' + CPF +
        ' | TelNum: ' + TelResidencial.Numero +
        ' | TelPref: ' + TelResidencial.Prefixo +
        ' | TelRam: ' + TelResidencial.Ramal);
      end;
    end; 

  Lista.Free;
end;

Projetos\Exemplo8.zip

    Parte 9 – Introduzindo um segundo mecanismo de persistencia

Como dito anteriormente, a introdução de uma interface nos traria independência e flexibilidade na hora da migração, ou mesmo no caso de persistências paralelas usando tecnologias/softwares diferentes, mas como diz o ditado, palavras o vento leva... vamos sair então do campo teórico e vamos ver isto na pratica, o exemplo também serve para demonstar como lidar com o tão temido e alardeado mapeamento objeto – relacional.

Não implementamos um mecanismo de persistência completo, mas acredito que o exemplo demonstre de forma clara e objetiva os conceitos básicos envolvidos no processo.

Suponha que desejemos uma alternativa para persistir nossos objetos TCliente em um BD relacional, usando o bom e velho SQL e nossos componentes de acesso a dados prediletos (dbExpress, BDE, ADO, IBX), a proposta é fazer isso, sem que nossa classe de negócio (TCliente) precise sofrer qualquer alteração ou manutenção, senão, não teria valido a pena o trabalho extra inicial.

Animem-se amigos, coisas boas estão por vir!

Uma corrente de projetistas defende a tese de que para cada classe de negocio que deve ter seus objetos persistidos, deve existir uma classe paralela que faz esta tarefa, foi este o conceito que adotamos ao criar a classe TClienteDB, porem, fomos um passo alem ao criar uma interface e agora chegou a hora de tirar proveito disso, peguem suas pedras e atirem-nas em mim se mudarmos uma única linha da classe de negocio (TCliente).

Bem, como a lógica para persistir usando objetos COM é totalmente diferente da lógica para persistir usando um SGBD relacional, não há muito sentido em escrever novos métodos para a classe existente e ficar usando condicionais para saber se chamamos o método que usa COM ou o que usa SQL, ate porque, novas mudanças poderão vir e a idéia e que essas classes que estão prontas e debugadas não sejam mais mexidas; criaremos então uma nova classe para realizar esta tarefa.

Adicione uma nova unit e salve-a como uPersistencia2 e certifique-se de inserir o código abaixo:

unit uPersistencia2;

interface

uses CacheObject_TLB, uCliente, Classes, uIntPersistencia, ADODB;

type
  TClienteDB2 = class(TInterfacedObject, IPersistencia)
  private
    Fabrica: IFactory;
    ADOQuery: TADOQuery;

  public
    destructor Destroy; override;
    function CarregarPorNome(aLista: TList; const str: string;
      const TableName: string): Integer;
    function Gravar(obj: TObject; const TableName: string): Boolean;
    function Recuperar(ID: Variant; const TableName: string): TObject;
    function ExecutarSQL(var aLista: TList; const aSQL: string): Integer;
    constructor Create;
  end;

implementation

uses Variants, SysUtils, Dialogs, ComObj, DB;

{ TClienteDB2 }

function TClienteDB2.CarregarPorNome(aLista: TList; const str,
  TableName: string): Integer;
var
  Cliente: TCliente;
begin
  with ADOQuery do
    begin
      Close;
      SQL.Clear;
      SQL.Add('select * from ');
      SQL.Add(TableName);
      SQL.Add(' where Nome like ');
      SQL.Add(QuotedStr(str + '%')); 
      Open;
    end;

  while not ADOQuery.Eof do
  begin
    Cliente:= TCliente.Create;
      with Cliente, ADOQuery do
        begin
          Nome:= FieldByName('Nome').AsString;
          Sobrenome:= FieldByName('Sobrenome').AsString;
          CPF:= FieldByName('CPF').AsString;
          TelResidencial.Numero:=
            FieldByName('TelResidencial_Numero').AsString;
          TelResidencial.Prefixo:=
            FieldByName('TelResidencial_Prefixo').AsString;
          TelResidencial.Ramal:=
            FieldByName('TelResidencial_Ramal').AsString;
        end;

    aLista.Add(Cliente);
    ADOQuery.Next;
  end;

  Result:= aLista.Count;
end;

constructor TClienteDB2.Create;
begin
  Fabrica:= CreateComObject(CLASS_Factory) as IFactory;
  if not Fabrica.Connect('cn_iptcp:127.0.0.1[1972]:USER') then
    ShowMessage('Não foi possível estabelecer conexão');

  ADOQuery:= TADOQuery.Create(nil);
  with ADOQuery do
    begin
      ConnectionString:= 'Provider=MSDASQL.1;Persist Security Info=False;' +
       'Data Source=CACHEWEB User';
      SQL.Add('select * from Exemplo.Cliente where ID=0');
    end;
end;

destructor TClienteDB2.Destroy;
begin
  Fabrica:= nil;
  ADOQuery.Close;
  ADOQuery.Free;
  inherited;
end;

function TClienteDB2.ExecutarSQL(var aLista: TList; const aSQL: string): Integer;
var
  Cliente: TCliente;
begin
  with ADOQuery do
    begin
      Close;
      SQL.Clear;
      SQL.Add(aSQL);
      Open;
    end;

  while not ADOQuery.Eof do
  begin
    Cliente:= TCliente.Create;
      with Cliente, ADOQuery do
        begin
          Nome:= FieldByName('Nome').AsString;
          Sobrenome:= FieldByName('Sobrenome').AsString;
          CPF:= FieldByName('CPF').AsString;
          TelResidencial.Numero:=
            FieldByName('TelResidencial_Numero').AsString;
          TelResidencial.Prefixo:=
            FieldByName('TelResidencial_Prefixo').AsString;
          TelResidencial.Ramal:=
            FieldByName('TelResidencial_Ramal').AsString;
        end;

    aLista.Add(Cliente);
    ADOQuery.Next;
  end;

  Result:= aLista.Count;
end;

function TClienteDB2.Gravar(obj: TObject; const TableName: string): Boolean;
begin
  Result:= False;

  with ADOQuery, TCliente(obj) do
    begin
      Close;
      SQL.Clear;
      SQL.Add('select * from Exemplo.Cliente where ID=0');
      Prepared:= True;
      Open;
      Append;

      FieldByName('Nome').AsString:= Nome;
      FieldByName('Sobrenome').AsString:= Sobrenome;
      FieldByName('CPF').AsString:= CPF;
      FieldByName('TelResidencial_Numero').AsString:= TelResidencial.Numero;
      FieldByName('TelResidencial_Prefixo').AsString:= TelResidencial.Prefixo;
      FieldByName('TelResidencial_Ramal').AsString:= TelResidencial.Ramal;

      Post;
    end;
  Result:= True;
end;

function TClienteDB2.Recuperar(ID: Variant; const TableName: string): TObject;
begin
  Result:= TCliente.Create;

  //mapeamos os dados para o objeto Delphi
  with TCliente(Result), ADOQuery do
    begin
      Close;
      SQL.Clear;
      SQL.Add('select * from Exemplo.Cliente where ID=' + string(ID));
      Open;

      Nome:= FieldByName('Nome').AsString;
      Sobrenome:= FieldByName('Sobrenome').AsString;
      CPF:= FieldByName('CPF').AsString;
      TelResidencial.Numero:= FieldByName('TelResidencial_Numero').AsString;
      TelResidencial.Prefixo:= FieldByName('TelResidencial_Prefixo').AsString;
      TelResidencial.Ramal:= FieldByName('TelResidencial_Ramal').AsString;
     end;
end;

end.

Como você já deve ter notado esta classe (TClienteDB2) implementa a mesma interface que a classe TClienteDB, portanto, possui os mesmos métodos, porem a implementação destes métodos é diferente da outra classe, já que usa o componente ADOQuery para acessar a parte relacional do Caché e não os objetos COM. Poderíamos usar outro SGBD como Interbase ou SQL Server, mas teríamos que criar a base de dados neste servidores e não faria a menor diferença, o código seria exatamente o mesmo já que estamos usando componentes comuns e SQL padrão.

Para fazer uso desta nova classe, apenas substitua as chamadas ao construtor atribuídas as variáveis do tipo IPersistencia, trocando TClienteDB por TClienteDB2:

ClienteDB:= TClienteDB2.Create;

Funciona? Claro que funciona, e viram só? Nenhuma alteração na classe alvo da persistência (TCliente). Caso desejemos persistir em outro SGBD que não segue nenhuma destas técnicas (COM ou SQL padrão) crie uma nova classe que implemente a mesma interface e pronto.

Projetos\Exemplo9.zip

Para obter mais flexibilidade só mesmo se pudéssemos escolher qual classe de persistência usar de acordo com as regras de negócio. Mas... epa, regras de negocio mudam, as vezes com uma freqüência irritante. Isso tem solução?

Tem sim, e é bem simples, podemos criar uma fabrica de objetos de persistência que aplicaria as regras de negocio e instanciaria o objeto certo para cada situação... Como isso soa aos seus ouvidos? Vamos então? Siga-me.

    Parte 10 – Criando uma fabrica de objetos de persistência

Muitas vezes nos deparamos com situações onde temos duas ou mais classes que prestam o mesmo serviço, mas não sabemos de antemão qual delas iremos usar, pois a escolha depende de fatores que acontecerão em runtime, ou seja, na hora da execução do programa. São as famosas regras de negócio, situações comuns são: aplicação de política de descontos, pagamentos (dinheiro, cheque, cartão (Visa, Mastercard)), etc.

Vamos analisar o caso da política de descontos. Uma loja pode ter descontos altamente variáveis (vocês sabem sobre o que estou falando...) de acordo com o tipo do cliente, tipo do produto, valor total da venda, dia da semana, promoção relâmpago e outros tipos inimagináveis, alem de, é claro, a combinação de todas as opções acima (o cliente é especial, comprou um produto em promoção e o valor total da venda atingiu o limite de desconto x), ou seja, muitas vezes é necessário aplicar algoritmos complexos e um grande “Case” para resolver problemas como esse, e o pior, estas regras mudam periodicamente, portanto, se queremos que nossas classes de negocio sejam estáveis, devemos protege-las dessas variantes, se todo esse código for colocado na classe Venda, alem de ter uma classe “inchada” você terá uma classe pouco reaproveitável , já que as regras de negocio variam muito de empresa para empresa, alem disso, essa classe estará sempre em manutenção para mudança de regras, o que poderá fazer coisas essenciais que estavam funcionando parar de funcionar, por esses motivos é que, em geral, migramos essas regras para classes auxiliares, essas classes tornam nosso modelo mais legível, coeso e estável, na hora de reaproveitar classes, as classes “puras” são reaproveitadas muito mais facilmente, as auxiliares as vezes podem ser reaproveitadas, as vezes precisam ser reescritas, mas o core do sistema não é afetado.

Muito bem, mas e ai? Já que temos classes auxiliares que serão instanciadas de acordo com a regra de negocio quem é o responsável por criar objetos deste tipo? Quem define qual classe Desconto deve ser criada? Um primeiro olhar indica Venda como a classe certa, já que ela é a especialista na informação, mas ai, aquele longo Case deveria estar em venda... Já sabemos que não queremos isso. A solução é criar uma classe auxiliar para fazer isso, a nossa fabrica de objetos.

No caso da aplicação que estamos desenvolvendo possuímos duas classes de persistência, uma que persiste através da tecnologia COM acessando diretamente a parte OO do Caché e outra que usa a parte relacional através de componentes ADO e instruções SQL.

Uma alternativa seria deixar a IU cuidar disso, colocar dois checkbox para verificarmos qual está checked e então instanciarmos o objeto correto, mas essa solução alem de pouco elegante também não é muito reaproveitável, por isso vamos deixar de papo e criar nossa fabrica de objetos de persistência.

Adicione uma unit ao projeto (estamos reaproveitando o projeto que desenvolvemos na parte 9) e salve-a como uFactoryDB, o código esta na lista a seguir:

unit uFactoryDB;

interface

uses uIntPersistencia;

type
  TFactoryDB = class
    class function GetDB: IPersistencia;
  end;

implementation

uses uPersistencia, uPersistencia2;

{ TFactoryDB }

class function TFactoryDB.GetDB: IPersistencia;
begin
  Randomize;
  if Random(2) = 0 then
    Result:= TClienteDB.Create
  else
    Result:= TClienteDB2.Create;
end;

end.

Como nesse exemplo não temos realmente uma regra de negocio para decidir qual classe deve ser instanciada, utilizaremos uma regra muito eficiente, o acaso...

A classe possui um único método que cuidará de criar o objeto correto para o chamador, esse método foi declarado como um class method para evitar que o usuário da classe tenha que declarar uma variável e instanciar um objeto deste tipo só para criar um objeto de outro tipo (que na verdade é seu objetivo), o método gera um numero aleatório entre 0 e 1, se o numero for 0 utilizaremos a classe TClienteDB (que usa COM), caso contrario, utilizaremos a classe TClienteDB2 (que usa ADO). Você perceberá a diferença entre o uso das classes quando pesquisar clientes por parte do nome, o método que usa COM não é Casesensitive enquanto a consulta SQL é, portanto, ao tentar localizar clientes cujo nome começa com a letra “a”, o TClienteDB trará resultados, enquanto TClienteDB2 dará a mensagem de que a consulta não retornou resultados.

Vamos agora ao form fazer as devidas alterações:

Observe como deve ficar a clausula uses:

uses uCliente, uIntPersistencia, uFactoryDB; 

Observe que o form não precisa conhecer TClienteDB nem TClienteDB2, ele nem sabe quem estará cuidando da persistência...

localize todas as linha onde o objeto ClienteDB é instanciado (ClienteDB:= TClienteDB2.Create;) e substitua por uma chamada ao método GetDB de TFactoryDB como a seguir:

ClienteDB:= TFactoryDB.GetDB;

Como ultima alteração, vamos inserir uma mensagem para o usuário que está realizando a pesquisa por nome, já que sabemos que uma das classes é sensível à caixa, localize o código do botão btLocNomeParte e envolva a chamada ao método CarregarPorNome em um if como a seguir:

if (ClienteDB.CarregarPorNome(Lista, edtLocNomeParte.Text, Classe)) = 0
   then
     ShowMessage('A consulta não retornou resultados' + #13 +
                 'A busca pode ser sensível à caixa')
  else

Isso é tudo, agora quem decide que tipo de objeto será instanciado é a fabrica, todas as outras classes estão livres dessa carga extra, lembre-se que isso só é possível porque as classes servidoras implementam a mesma interface, então, a IU que possui uma variável do tipo da interface (ClienteDB: IPersistencia;) pode tranquilamente chamar os métodos declarados na interface com a certeza de que eles existem no objeto que a variável recebeu.

Projetos\Exemplo10.zip

Bem, espero que estas informações sejam suficientes para aguçar a sua curiosidade e ajudá-lo a dar os primeiros passos em direção aos bancos de dados OO.

Pesquise também sobre a capacidade do Caché de gerar páginas da web, é outro assunto fascinante.

Um forte abraço a todos.

Ricardo Barbieri.

Graduado em Redes de Computadores;

Pós-graduado em Análise de Sistemas;

Borland Delphi7 Studio Product Certified;

Borland Delphi7 Instructor Certified;

Borland Delphi8 Instructor Certified;

Borland Delphi2005 for Win32 Product Certified;

Borland Delphi2005 Instructor Certified.

Borland Delphi2006 for Win32 Product Certified;

Borland Delphi2006 Instructor Certified.

Borland UML Instructor Certified.

Server Response from: ETNASC03