Refatorando Atributos – Que tal um ORM Básico? Parte 13

Olá

No post anterior, iniciamos análise do código e efetuamos os primeiros ajustes. Agora, iremos trabalhar a unit Atributos.pas. Primeiro, irei colocar o código já alterado e depois tecerei comentários a respeito:

unit Atributos;

interface

uses
  Base, Rtti, System.Classes;

type
  TTipoCampo = (tcNormal, tcPK, tcRequerido);

  TCamposAnoni = record
    NomeTabela: string;
    Sep: string;
    PKs: TResultArray;
    TipoRtti: TRttiType;
  end;

  TFuncReflexao = reference to function(ACampos: TCamposAnoni): Integer;

  TNomeTabela = class(TCustomAttribute)
  private
    FNomeTabela: string;
  public
    constructor Create(ANomeTabela: string);
    property NomeTabela: string read FNomeTabela write FNomeTabela;
  end;

  TCampo = class(TCustomAttribute)
  private
    FDescricao: string;
    FTipoCampo: TTipoCampo;
    procedure SetDescricao(const Value: string);
    procedure SetTipoCampo(const Value: TTipoCampo);
  public
    constructor Create(ANome: string; ATipo: TTipoCampo = tcNormal);

    property Descricao: string read FDescricao write SetDescricao;
    property TipoCampo: TTipoCampo read FTipoCampo write SetTipoCampo;
  end;

  //Reflection para os comandos Sql
  function ReflexaoSQL(ATabela: TTabela; AnoniComando: TFuncReflexao): Integer;

  function PegaNomeTab(ATabela : TTabela): string;
  function PegaPks(ATabela : TTabela): TResultArray;
  function ValidaTabela(ATabela : TTabela): Boolean;


implementation

uses
  System.TypInfo, System.SysUtils, Forms, Winapi.Windows;

function ReflexaoSQL(ATabela: TTabela; AnoniComando: TFuncReflexao): Integer;
var
  ACampos: TCamposAnoni;
  Contexto  : TRttiContext;
begin
  ACampos.NomeTabela := PegaNomeTab(ATabela);

  if ACampos.NomeTabela = EmptyStr then
    raise Exception.Create('Informe o Atributo NomeTabela na classe ' +
      ATabela.ClassName);

  ACampos.PKs   := PegaPks(ATabela);

  if Length(ACampos.PKs) = 0 then
    raise Exception.Create('Informe campos da chave primária na classe ' +
      ATabela.ClassName);

  Contexto  := TRttiContext.Create;
  try
    ACampos.TipoRtti := Contexto.GetType(ATabela.ClassType);

    //executamos os comandos Sql através do método anônimo
    ACampos.Sep := '';
    Result := AnoniComando(ACampos);

  finally
    Contexto.free;
  end;
end;

function PegaNomeTab(ATabela : TTabela): string;
var
  Contexto  : TRttiContext;
  TipoRtti : TRttiType;
  AtribRtti : TCustomAttribute;
begin
  Contexto := TRttiContext.Create;
  TipoRtti := Contexto.GetType(ATabela.ClassType);
  try
    for AtribRtti in TipoRtti.GetAttributes do
      if AtribRtti Is TNomeTabela then
        begin
          Result := (AtribRtti as TNomeTabela).NomeTabela;
          Break;
        end;
  finally
    Contexto.Free;
  end;
end;

function ValidaTabela(ATabela : TTabela): Boolean;
var
  Contexto  : TRttiContext;
  TipoRtti  : TRttiType;
  PropRtti  : TRttiProperty;
  AtribRtti : TCustomAttribute;
  ValorNulo : Boolean;
  Erro      : TStringList;
begin
  Result    := True;
  ValorNulo := false;
  Erro      := TStringList.Create;
  try
    Contexto := TRttiContext.Create;
    try
      TipoRtti := Contexto.GetType(ATabela.ClassType);
      for PropRtti in TipoRtti.GetProperties do
      begin
        case PropRtti.PropertyType.TypeKind of
          tkInt64, tkInteger: ValorNulo := PropRtti.GetValue(ATabela).AsInteger <= 0;
          tkChar, tkString, tkUString: ValorNulo := Trim(PropRtti.GetValue(ATabela).AsString) = '';
          tkFloat : ValorNulo := PropRtti.GetValue(ATabela).AsCurrency <= 0;
        end;

        for AtribRtti in PropRtti.GetAttributes do
        begin
          if AtribRtti Is TCampo then
          begin
            if ((AtribRtti as TCampo).TipoCampo in [tcPK, tcRequerido]) and (ValorNulo) then
            begin
              Erro.Add('Campo ' + (AtribRtti as TCampo).Descricao + ' não informado.');
            end;
          end;
        end;
      end;
    finally
      Contexto.Free;
    end;
    if Erro.Count>0 then
    begin
      Result := False;
      Application.MessageBox(PChar(Erro.Text),'Erros foram detectados:',
      mb_ok+MB_ICONERROR);
      Exit;
    end;
  finally
    Erro.Free;
  end;
end;

function PegaPks(ATabela : TTabela): TResultArray;
var
  Contexto  : TRttiContext;
  TipoRtti  : TRttiType;
  PropRtti  : TRttiProperty;
  AtribRtti : TCustomAttribute;
  i: Integer;
begin
  Contexto := TRttiContext.Create;
  try
    TipoRtti := Contexto.GetType(ATabela.ClassType);
    i:=0;
    for PropRtti in TipoRtti.GetProperties do
      for AtribRtti in PropRtti.GetAttributes do
        if AtribRtti Is TCampo then
          if (AtribRtti as TCampo).TipoCampo=tcPK then
          begin
            SetLength(Result, i+1);
            Result[i] := PropRtti.Name;
            inc(i);
          end;
  finally
    Contexto.Free;
  end;
end;

{ TCampo }

constructor TCampo.Create(ANome: string; ATipo: TTipoCampo);
begin
  FDescricao      := ANome;
  FTipoCampo := ATipo;
end;

procedure TCampo.SetDescricao(const Value: string);
begin
  FDescricao := Value;
end;

procedure TCampo.SetTipoCampo(const Value: TTipoCampo);
begin
  FTipoCampo := Value;
end;

{ TNomeTabela }

constructor TNomeTabela.Create(ANomeTabela: string);
begin
  FNomeTabela := ANomeTabela;
end;

end.

O código sofreu alterações importantes e acréscimo de novas funcionalidades. Vamos a elas:

  • Linha 9: Temos um tipo enumerado para guardar o tipo de campo da tabela, ou seja, se é Normal(não é um campo obrigatório), PK(chave primária) ou Requerido(campo obrigatório);
  • Linha 28: Retiramos o atributo TCampoPK, ficando apenas o atributo TCampo, tendo a incumbência de receber o tipo e a descrição do campo. Agora será possível por exemplo, devolver uma mensagem de erro mais amigável. Para ficar mais claro…
    • …digamos que temos uma tabela com um campo obrigatório chamado “Clie_Obs“. E ao atualizar a tabela, esquecemos de informar este campo obrigatório. Receberemos então o erro: “Campo Clie_Obs não informado!”. Agora, com a descrição do campo poderemos devolver a seguinte mensagem: “Campo Observações do Cliente não informado”. Ficou bem melhor, não é mesmo?
  • Linha 46 e 104: Temos um método para validar a Tabela e devolver um erro caso haja algum imprevisto. Desta forma, poderemos, antes de qualquer atualização, verificar os dados informados, evitando assim, erros vindos diretamente do banco de dados, ou seja, em inglês e de difícil entendimento por parte do usuário final. Ainda se encontra incompleto, visto que trata apenas de campos PK e requeridos. Mas poderemos futuramente adicionar novas validações;
  • Linhas 61 e 67: Foi inserido validações no método ReflexaoSQL. Isso ajuda o desenvolvedor quando, por exemplo, ao adicionar uma nova tabela esquecer-se de colocar os atributos obrigatórios, como o nome da tabela e uma chave primária. Sem essa verificação, temos uma maior dificuldade em detectar onde o erro ocorreu.

É isso! Fico por aqui. Continuamos no próximo post. Até lá!

Sobre o autor: Luiz Carlos (60 Posts)

Desenvolvedor de software Balsas/MA


Compartilhe:

22 Replies to “Refatorando Atributos – Que tal um ORM Básico? Parte 13”

  1. Muito bom, estou acompanhando o desenvolvimento desde a primeira parte, gostaria de saber quando o senhor ira tratar a questão de tabelas com relacionamento 1×1, 1xN e NxM…

    Obrigado por compartilhar seu conhecimento.

    1. Olá Carlos

      Já iniciei a utilização deste projeto em um novo sistema que estou desenvolvendo. Os cadastros básicos já estão funcionando e alguns cadastros mais elaborados já estão em fase de teste, como por exemplo, funcionários:
      Cad. Funcionário alfa

      Note que nele tem campos cidade, departamento e usuário que são chaves estrangeiras para suas respectivas tabelas.

      Ao informar o código ou efetuar uma consulta no campo cidade, no exit eu faço o seguinte:

      var
        ACidade: TCidade;
      begin
        if (Trim(edCidade.text)='') or (edCidade.Text = '0') then
        begin
          edNomeCidade.Caption := '';
          exit;
        end;
      
        ACidade := TCidade.Create;
        try
          ACidade.CODCID := edCidade.text;
          if dmConexao.Dao.Con.Buscar(ACidade)>0 then
          begin
            edNomeCidade.Caption := ACidade.NOME;
          end
          else
          begin
            Mensagem('Cidade Inexistente!',MB_OK+MB_ICONEXCLAMATION);
            edCidade.Clear;
          end;
        finally
          ACidade.free;
        end;
      end;
      

      Quando carrego a Tabela Funcionário, chamo método exit deste campo.

      Por enquanto, é assim que tenho feito o relacionamento. Como irei utilizar este projeto neste novo sistema, é possível que eu trabalhe o relacionamento de uma forma mais prática, mas por enquanto, este não é o meu objetivo. Conforme já falei em posts anteriores, o objetivo aqui é passar recursos do Delphi ainda pouco explorados pelo desenvolvedores, principalmente por iniciantes na área.

  2. Excelente, estou aprendendo muito com seu artigo… e gostaria de saber se é possível validar campo pela classe conforme exemplo abaixo, no caso do estado da cidade que seria uma chave estrangeira.

    Desde já agradeço sua atenção.

    unit uEstado;

    interface

    uses Base, Rtti, Atributos;

    type
    [TNomeTabela(‘estado’)]
    TEstado= class (TTabela)
    private
    FId: Integer;
    FNome: string;
    FUf: string;
    procedure SetId(const Value: Integer);
    procedure SetNome(const Value: string);
    procedure SetUf(const Value: string);
    public
    [TCampos(‘ID do Estado’,tcPK)]
    property Id: Integer read FId write SetId;
    [TCampos(‘Nome do Estado’,tcRequerido)]
    property Nome: string read FNome write SetNome;
    [TCampos(‘UF do Estado’,tcRequerido)]
    property Uf: string read FUf write SetUf;
    end;

    implementation

    { TEstado }

    procedure TEstado.SetId(const Value: Integer);
    begin
    FId := Value;
    end;

    procedure TEstado.SetNome(const Value: string);
    begin
    FNome := Value;
    end;

    procedure TEstado.SetUf(const Value: string);
    begin
    FUf := Value;
    end;

    end.

    unit uCidade;

    interface

    uses Base, Rtti, Atributos, uEstado;

    type
    [TNomeTabela(‘cidade’)]
    TCidade = class (TTabela)
    private
    FId: Integer;
    FEstado: TEstado;
    FNome: string;
    FData: TDateTime;
    FHabitantes: Integer;
    FRendaPerCapta: Currency;
    procedure SetId(const Value: Integer);
    procedure SetEstado(const Value: TEstado);
    procedure SetNome(const Value: string);
    procedure SetData(const Value: TDateTime);
    procedure SetHabitantes(const Value: Integer);
    procedure SetRendaPerCapta(const Value: Currency);
    public
    [TCampos(‘ID do Estado’,tcPK)]
    property Id: Integer read FId write SetId;
    [TCampos(‘Código do Estado’,tcRequerido)]
    property Estado: TEstado read FEstado write SetEstado;
    [TCampos(‘Nome da Cidade’,tcRequerido)]
    property Nome: string read FNome write SetNome;
    [TCampos(‘Data de Cadastro da Cidade’,tcNormal)]
    property Data: TDateTime read FData write SetData;
    [TCampos(‘Número de Habitantes da Cidade’,tcRequerido)]
    property Habitantes: Integer read FHabitantes write SetHabitantes;
    [TCampos(‘Renda Percapta dos habitantes da Cidade’,tcRequerido)]
    property RendaPerCapta: Currency read FRendaPerCapta write SetRendaPerCapta;
    end;

    implementation

    { TCidade }

    procedure TCidade.SetData(const Value: TDateTime);
    begin
    FData := Value;
    end;

    procedure TCidade.SetNome(const Value: string);
    begin
    FNome := Value;
    end;

    procedure TCidade.SetEstado(const Value: TEstado);
    begin
    FEstado := Value;
    end;

    procedure TCidade.SetHabitantes(const Value: Integer);
    begin
    FHabitantes := Value;
    end;

    procedure TCidade.SetId(const Value: Integer);
    begin
    FId := Value;
    end;

    procedure TCidade.SetRendaPerCapta(const Value: Currency);
    begin
    FRendaPerCapta := Value;
    end;

    end.

    1. Fico feliz que esteja gostando dos artigos.

      Bom, a resposta para sua pergunta é sim. Porém, ainda não tive tempo de trabalhar a questão das chaves estrangeiras. Então, não posso lhe afirmar que este projeto terá o mesmo direcionamento que o seu. Talvez eu tome um outro rumo… mas, isso só saberei quando conseguir voltar a postar novos artigos. A avalanche (trabalho) aqui só tem crescido…

      Eu sugiro que você mantenha uma fonte intacta, que esteja de acordo com as minhas fontes, e faça uma com a sua ideia. Assim, quando eu voltar a atualizar o projeto, você poderá continuar a acompanhar esta série.

      1. Também estou gostando muito dos seus artigos.
        Você já falou, e ficou ótimo, sobre DAO.
        Poderia fazer uma série sobre MVC, MVP e MVVP? Se possível, com exemplos simples que rodassem no Delphi 7.
        É possível usar DBGrid com MVC respeitando a POO ou devemos usar StringGrid?
        Por causa da POO, os componentes dataware tendem a desaparecer?
        POO e RAD (dataware) são inimigos mortais?

        1. Obrigado Roberto, fico feliz que tenha gostado dos meus artigos.

          Sobre os padrões, entendo que, lá no fundo o que nós buscamos é termos como resultado um código de fácil manutenção e com alto desempenho. Sabemos que os requisitos mudam e que novos aparecem rotineiramente. Então, ter um código organizado, padronizado, nos permite atender às necessidades que surgem com maior eficiência. A separação das responsabilidades de cada conjunto de código, seja para o acesso e manipulação dos dados, para visualização da informação por parte do usuário ou a intermediação entre as duas anteriores, é super importante. Cada padrão citado tem a sua característica, e seus prós e contras. Não penso em tratar deste assunto, pelo menos não por enquanto. Mesmo porque, já tenho suado a camisa para tentar terminar minha série sobre um ORM básico que estou construindo. E encontrar tempo para ele está muito difícil. Sugiro que você faça uma busca pelos termos, tenho certeza que você irá encontrar muito material a respeito.

          Sobre POO x Datawares (duas últimas perguntas), assunto polêmico, creio que a resposta para esta pergunta depende muito do que você deseja fazer e que projeto deseja tocar. Muitos desenvolvedores ainda utilizam intensamente componentes conectados, principalmente para projetos simples, de menor porte. Porém, todo projeto pequeno, com apenas umas poucas telas e relatórios, um dia pode vir a se tornar grande! Então falar que para projetos pequenos não compensa todo o esforço e planejamento que a POO exige é um tanto arriscado. Creio que o fundamental é sempre fazer uma análise profunda e “sincera” do que você deseja construir. Dependendo da conclusão que você chegar, adotará Datawares, POO ou, por que não, ambos. Eu sinceramente há muito tempo abandonei datawares (com exceção do DBGrid, mas a cada dia que passa vejo que é uma questão de tempo). Com a vinda do livebindings, onde podemos ligar os componentes entre si, sejam conectados ou não, nos leva a crer que os componentes criados especificamente para manipulação de dados, como o DBEdit, por exemplo, tenham a sua importância diminuída e até mesmo sua existência desnecessária. Isso já pode ser visto quando criamos um projeto Firemonkey. Procure pelos componentes da paleta Data Controls e veja se encontra. O motivo da inexistência da paleta pode ser uma questão de compatibilidade entre sistemas operacionais, mas também já é um indício dos rumos que estão sendo tomados.

          Eu passei muito tempo adiando o inevitável, ou seja, continuar insistindo no Delphi 7 e não atualizando para as novas versões. Mas chega a hora que não dá mais. Estava perdendo muita coisa boa que foi acrescida e melhorada no Delphi. Então, resolvi adquirir o XE2. A luta tem sido grande para atualizar meus sistemas. A grande vantagem que tenho, é justamente utilizar POO na veia! :) Tem me facilitado bastante a transição.

          Hoje, estamos às margens do XE5, que vem, entre outras coisas, pronto para o Android. Finalmente!

          Abraços.

  3. Luiz, primeiramente parabéns pelo seu post. Também estou trabalhando num projeto parecido com o teu. A princípio precisava criar interface padrão para cada ‘Entidade/Classe’ existente no projeto. Uma interface parecida com a do Firebird (Grade / Formulário / Relatório). Utilizando RTTI já consigo criar os forms dinâmicos para cada entidade. Porém, diferente do seu projeto, as classes eu criei utilizando a versão Beta do DataModeler da TMS. O que me interessei pelo seu post foi a questão de poder criar N atributos para os campos. Utilizando o DataModeler, ele só consegue (por enquanto) gerar dois atributos para cada campo. Então eu conseguir associar novos ‘atributos’ aos campos utilizando uma ‘adequação técnica = guambiarra’. Segue: [Description(‘TELEFONETelefone do FuncionárioTelefoneTelefone do funcionárioSContato!\(99\) 9999-9999;0;_’)]. Onde: c = Campo, d = Descrição do campo, h = hint do campo no formulário, i = instruções para preenchimento do campo (help formulário), v = visível ou não no formulário, g = organização do formulário ( identificação, localização, etc), o = ordem de apresentação no formulário, t = tipo do campo (defasado), li = limite inferior, ls = limite superior (ranges), m = mascara do campo; … é isso aí ..

  4. Olá Luiz, gostaria de parabeniza-lo pelo pela iniciativa e qualidade de seus artigos. Estou acompanhando e aprendendo muito com este sobre ORM, porem após efetuar as últimas atualizações o meu parou de funcionar. Está dando uma mensagem de que “Informe os campos que constituem a chave primária na classe”. Analisando vi que foi modificado a forma com que os atributos nos campos são declarados. Então tentei declarar desta forma:
    [TCampos(‘ID do Estado’,tcPK)] // <– atributo que define o campo ID como PK
    property Id: Integer read FId write SetId;

    Antigamente era declarado Assim
    [TCampoPK]
    property Id: Integer read FId write SetId;

    Porém continua não funcionando. Peço sua ajuda para saber como eu posso declarar estes atributos agora.

    Obrigado.

    1. Veja como está a chamada deste atributo na unit Atributos.pas? Talvez haja alguma divergência.
      De toda forma, estive novamente alterando os atributos aqui, para poder ter uma amplitude maior no que se refere às validações. Do jeito que eu havia inicialmente proposto, ficou bastante limitado. Vou fazer mais alguns testes (até sábado 14/09), e aí posto os fontes para que você possa comparar com o seu, ok?

      1. Primeiramente obrigado pela resposta.
        Vou conferir minha unit Atributos.pas, conforme você relatou, e aguardo os fontes para comparação, mas acredito que o erro seja meu aqui. Assim que conferir posto novamente. OK

      2. Luiz depois de muito tempo, volto a mexer no sisteminha devido a correria de trampo, e localizei o erro, eu tinha colocado [TCampos(‘Id do estado’,tcPK)] ao invés de [TCampo(‘Id do estado’,tcPK)].

        Estou aguardando ansioso por novos posts, por que até aqui já consegui mudar muito minha forma de trabalhar.

        Obrigado.

  5. Olá, seu artigo está excelente.
    Nunca aprendi tanto em um blog, você está realmente de parabéns.

    Tenho uma dúvida, não sei se você pode me ajudar.

    Quero declarar uma property do tipo BLOB.
    Consigo fazer isso?
    De que forma?

    Obrigada

  6. Boa Tarde Luiz, Primeiramente, gostei bastante do artigo, é muito instrutivo e com conteúdos com pouca abordagem nesta linguagem de programação.
    Estou tentando compilar o projeto com estas ultimas alterações, porém, não está compilando, pois as funções PegaNomeTab(ATabela) e PegaPks(ATabela) que estão no TDAOBASE não são reconhecidas, se incluo na uses a unit Atributos, o compilador diz que há uma referência circular e não deixa compilar, se eu deixar sem colocar a uses, o compilador diz que não consegue encontrar as funções. Tens como me mandar o projeto já com as alterações feitas para que eu possa ver o que estou fazendo de errado?

    Att.

  7. Bom dia Luiz!!!

    Gostaria de saber se você vai continuar o Post(excelente trabalho) e qual a previsão para conclusão(quantas partes restantes e se possível tempo previsto para os mesmo) ?

  8. Muito bom mesmo, era exatamente isso que eu precisava saber se o delphi faria. Apenas mudei do IBX para FireDac.

    Gostaria de ver como ficaria a camada de persistencia com um campo BLOB, e desenvolver uma classe basica que service de camada intermediaria entre o modelo de persistencia e a View, ou seja um Controller utilizando o livebinding. Se pudesse me ajudar, agradeceria muito.

    Um abraço.

  9. Excelente artigo.
    Você está de parabéns.

    Criei um ORM para meus projetos com meus conhecimentos “basiquicimos”.
    Ficou funcional, mas com forte acoplamento com o firedac.
    Está funcionando, porém gostaria de melhorar.

    Seu artigo está me ajudando muito, espero que você de continuidade.
    Fica aqui meu enorme obrigado por compartilhar seu conhecimento, é muito difícil achar conteúdo assim na internet.

  10. Olá, excelente esse seu post.
    Estou estudando a respeito de ORM e MVC e estou travado numa rotina, pois existe campo auto incremento que não estou conseguindo resolver na rotina abaixo nos campos que estão comentados. Eu até sei que ele cria a rotina sql e que seria fácil remover, mas nesse padrão como eu resolveria ? Pode me ajudar ? Segue o script.

    Estou transformando ele para Firedac.

    {function TDaoUib.Inserir(ATabela: TTabela): Integer;
    var
    Comando: TFuncReflexao;
    begin
    Comando := function(ACampos: TCamposAnoni): Integer
    var
    Campo: string;
    PropRtti: TRttiProperty;
    AtribRtti: TCustomAttribute; // Para pegar CAmpo Identity
    begin
    FechaQuery;
    with Qry do
    begin
    SQL.Add(‘Insert into ‘ + ACampos.NomeTabela);
    SQL.Add(‘(‘);

    // campos da tabela
    ACampos.Sep := ”;
    for PropRtti in ACampos.TipoRtti.GetProperties do
    begin
    // Verifico se o o campo é identity
    //if not(AtribRtti as TCampos).IsIdentity then
    //begin
    SQL.Add(ACampos.Sep + PropRtti.Name);
    ACampos.Sep := ‘,’;
    // end;
    end;
    SQL.Add(‘)’);

    // parâmetros
    SQL.Add(‘Values (‘);
    ACampos.Sep := ”;
    for PropRtti in ACampos.TipoRtti.GetProperties do
    begin
    // Verifico se o o campo é identity
    // if not(AtribRtti as TCampos).IsIdentity then
    // begin
    SQL.Add(ACampos.Sep + ‘:’ + PropRtti.Name);
    ACampos.Sep := ‘,’;
    // end;
    end;
    SQL.Add(‘)’);
    showmessage(SQL.Text);
    // valor dos parâmetros
    for PropRtti in ACampos.TipoRtti.GetProperties do
    begin
    Campo := PropRtti.Name;
    ConfigParametro(Qry, PropRtti, Campo, ATabela);
    end;
    end;
    Result := ExecutaQuery;
    end;

    // reflection da tabela e execução da query preparada acima.
    Result := ReflexaoSQL(ATabela, Comando);
    end;}

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *