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:
[sourcecode language=”Delphi”]
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.
[/sourcecode]
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á!
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.
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:
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:
[sourcecode language=”Delphi”]
var
Cidade: TCidade;
begin
if Trim(edCidade.Text)=” then
begin
edNomeCidade.Caption := ”;
Exit;
end;
Cidade := TCidade.Create;
try
Cidade.Codcid := edCidade.Text;
if dmConexao.Dao.Con.Buscar(Cidade) > 0 then
begin
edNomeCidade.Caption := Cidade.Nome;
end
else
begin
ShowMessage(‘Cidade Inexistente!’);
edCidade.Clear;
end;
finally
Cidade.Free;
end;
end;
[/sourcecode]
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.
Gostaria de saber como tratar um campo do tipo TBitmap, para pegar o Set e GET
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.
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.
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?
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.
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í ..
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.
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?
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
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.
Boa tarde,
Parabéns pela iniciativa, muito bom!
Uma pequena duvida você ira continuar com o artigo?
Bom dia!
Muito bom artigo, estou acompanhando tudo. Gostaria de saber se vai continuar com as publicações. obrigado!
Você vai continuar com as publicações?
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
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.
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) ?
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.
Boa tarde
achei maravilhoso o post
gostaria de saber se foi abandonado ou
ainda se retomado
Aguardo um posicionamento
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.
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;}