Servidores REST usando TMS Aurelius e DataSnap Introdução Em artigo anterior a framework na Active Delphi, apresentados o TMS Aurelius, uma framework para mapeamento objeto-relacional (ORM) e mostramos como utilizar essa framework para montar uma aplicação usando o paradigma de objetos (ORM), evitando o uso de SQL s e abstraindo ainda mais a camada de negócios. Neste artigo, mostraremos como construir um servidor REST usando o TMS Aurelius, e as vantagens dessa abordagem. Esse artigo é baseado em uma palestra realizada na Delphi Conference 2012, na qual mostramos como construir uma aplicação REST passo-a-passo. O exemplo utilizado era uma aplicação simples para controle de incidentes (bugs, chamados, etc.), e vamos utilizar o mesmo conceito aqui. Criando e mapeando as classes Primeiramente e uma das coisas mais importantes da aplicação devemos criar nossas classes de negócio. No caso da nossa aplicação de controle de incidentes, teremos as classes TIncidente, Tusuario, TProjeto, etc.. Como não é possível neste artigo mostrar toda a aplicação, listamos abaixo a classe principal TIncidente: [Entity, Automapping] TIncidente = class private FId: Integer; FAssunto: string; FDescricao: string; FPrioridade: TPrioridadeIncidente; FStatus: TStatusIncidente; FResponsavel: TUsuario; FComentarios: TList<TComentarioIncidente>; FProjeto: TProjeto; public constructor Create; destructor Destroy; override; property Id: Integer read FId write FId; property Assunto: string read FAssunto write FAssunto; property Descricao: string read FDescricao write FDescricao; property Prioridade: TPrioridadeIncidente read FPrioridade write FPrioridade; property Status: TStatusIncidente read FStatus write FStatus; property Responsavel: TUsuario read FResponsavel write FResponsavel; property Comentarios: TList<TComentarioIncidente> read FComentarios write FComentarios; property Projeto: TProjeto read FProjeto write FProjeto; Numa aplicação típica cliente-servidor (ou local usando SQLite, por exemplo) podemos usar o TMS Aurelius para buscar e gravar no bando de dados essa entidade: // Busca incidente pelo Id e atualiza o campo Status
Incidente := ObjectManager.Find<TIncidente>(IdIncidente); Incidente.Status := TStatusIncidente.Encerrado; ObjectManager.Flush; // persiste no banco tudo que foi alterado Ou, por exemplo, podemos pedir uma lista de todos os incidentes cujo status seja pendente: Var IncidentesPendentes: TList<Tincidente>; IncidentesPendentes := ObjectManager.Find<Tincidente>.Where(TLinq.Eq('Status', TStatusIncidente.Pendente)).List; E assim por diante. O Aurelius é extremamente flexível em suas consultas e mapeamentos (você pode definir nomes dos campos, tipos, relacionamentos (associações) um-pra-muitos, muitos-pra-um, herança, etc.). Esse é o uso básico do Aurelius que cobrimos no artigo anterior e é um assunto extenso, portanto não entraremos em detalhes aqui. Edição visual com TAureliusDataset Com as classes definidas e mapeadas, podemos criar a interface visual para editá-las. Você irá notar que não precisamos nos preocupar muito em como os dados são obtidos e salvos no banco de dados estamos apenas lidando com objetos (no exemplo, da classe TIncidente) e nossas telas de edição precisam apenas saber sobre esses objetos e editá-los. A parte de persistência é feita automaticamente pelo Aurelius, e não nos preocuparemos se os objetos são salvos diretamente no banco de dados, ou enviados a um servidor de aplicação (REST, nesse exemplo). Assim, nosso formulário de edição de incidente, por exemplo, poderia ter a propriedade a seguir (outras propriedades e métodos foram removidos): TFormIncidente = class(tform) private procedure SetIncidente(const Value: TIncidente); public property Incidente: TIncidente read FIncidente write SetIncidente; e o método SetIncidente seria apenas: procedure TFormIncidente.SetIncidente(const Value: TIncidente); FIncidente := Value; EditDescricao.Text := FIncidente.Descricao; EditAssunto.Text := FIncidente.Assunto; ComboStatus.ItemIndex := Ord(FIncidente.Status); // E assim por diante
Ou seja, você estaria montando uma tela qualquer de edição e via código simplesmente transferindo os dados do objeto pra tela. No clique do botão salvar, você faria o caminho reverso, pegando os dados da tela e setando as propriedades do objeto. É simples mas pode ser trabalhoso. Pra facilitar essa tarefa o TMS Aurelius disponibiliza o TAureliusDataset, que simplesmente faz essa ponte entre o objeto e os controles da tela (no caso, controles db-aware). Note o form da tela de edição do incidente: No caso, foram definidos campos no dataset de acordo com as propriedades do objeto TIncidente. Cada campo corresponde a uma propriedade. Note o campo Projeto.Nome é um recurso do dataset que permite buscar propriedades de associações. Ou seja, nossa classe TIncidente possui a seguinte propriedade: TIncidente = class; {...} property Projeto: TProjeto read FProjeto write FProjeto; Assim, o campo Projeto.Nome permite acessar essa propriedade inclusive para edição. No caso da nossa tela, estamos mostramo o nome do projeto em um label. A lista de campos do dataset pode inclusive ser obtida em design-time, basta você compilar suas classes em um package separado e informar ao dataset o local do package. O mapeamento de cada controle com cada propriedade é feito normalmente como com qualquer dataset:
Você pode inclusive usar campos lookup para, por exemplo, editar o campo responsável (escolher o responsável pelo incidente usando um combo com a lista objetos TResponsavel). E como informar ao dataset qual objeto estamos editando? Pode ver na nova implementação do método SetIncidente: procedure TFormIncidente.SetIncidente(const Value: TIncidente); FIncidente := Value; DatasetIncidente.Close; DatasetIncidente.SetSourceObject(FIncidente); DatasetIncidente.Open; DatasetIncidente.Edit; podemos ver que usa-se o dataset normalmente como qualquer outro dataset. A única diferença é que usamos o método SetSourceObject pra indicar qual objeto estamos editando e nossa tela de edição estará pronta. Ao chamarmos o método Post do dataset, as propriedades do objeto serão atualizadas de acordo com os controles da tela. TAureliusDataset e Listagens Como qualquer dataset, o TAureliusDataset também pode ser usado pra listas onde cada objeto é um registro no dataset. A tela de listagens usa esse recurso, mostrando os incidentes em um grid, como mostrado na tela a seguir:
Também como em qualquer dataset, basta ligar o TDBGrid (ou qualquer outro data-aware grid) ao dataset através do datasource, e definir as colunas do grid ligando a cada campo. Para informar ao dataset de onde buscar os objetos, podemos usar o método SetSourceList: DatasetIncidentes.Close; IncidentesPendentes := ObjectManager.Find<Tincidente>.Where(TLinq.Eq('Status', TStatusIncidente.Pendente)).List; DatasetIncidentes.SetSourceList(IncidentesPendentes); DatasetIncidentes.Open; No exemplo acima usamos a lista de incidentes pendentes apenas como ilustração por já havíamos mencionado essa consulta anteriormente. No código real da aplicação, basta buscarmos a lista de incidentes de algum lugar, seja diretamente do banco (como no exemplo acima) ou de um servidor de aplicação (REST, no nosso exemplo). Outro detalhe interessante a se mencionar são os campos com sufixo EnumName. Esse sufixo informa ao dataset para usar tipos enumerados no formato texto. No caso da figura, o dataset tem o campo Status.EnumName definido. Nosso campo Status é um tipo enumerado do tipo TStatusIncidente, mas como usamos o sufixo.enumname, o nome da enumeração será mostrado no grid. Assim, na coluna Status veremos automaticamente os textos Pendente, Encerrado, etc., sem nenhuma necessidade de conversão especial. Criando o servidor REST Até o momento, utilizamos vários recursos do Aurelius mas em uma aplicação cliente-servidor. Agora, o objetivo é disponibilizar dados da nossa aplicação no caso nossas entidades através de um servidor REST. Primeiramente vamos criar o servidor REST em branco no Delphi. O objetivo desse artigo não é explicar o mecanismo de funcionamdo de servidores REST usando DataSnap. Para maiores detalhes,
visite a página http://docwiki.embarcadero.com/radstudio/xe3/en/datasnap_rest_application_wizard. No nosso exemplo, basta executarmos o assistente do Delphi para criar o servidor DataSnap, e em seguida informamos ao servidor onde está a classe com os métodos que precisamos: procedure TModuleServidor.DSServerClass1GetClass(DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass); PersistentClass := TBugTrackerMethods; É na classe TbugTrackerMethods que vamos definir os métodos da nossa aplicação. A seguir apresentamos alguns métodos e como eles foram implementados (algumas partes foram retiradas para melhor leitura): Uses {...} Aurelius.Engine.ObjectManager, Aurelius.Json.DataSnap, {...}; {$METHODINFO ON} TBugTrackerMethods = class(tcomponent) private FManager: TObjectManager; FSerializer: TDataSnapJsonSerializer; FDeserializer: TDataSnapJsonDeserializer; function ToJson(Obj: TObject): TJsonValue; function FromJson<T: class>(value: TJsonValue; AManaged: boolean): T; {...} public procedure SalvaIncidente(Incidente: TJsonValue); function CarregaIncidente(Id: integer): TJsonValue; function ListaIncidentes: TJsonValue; function ListaIncidentesProjeto(IdProjeto: integer): TJsonValue; {...} { } constructor TBugTrackerMethods.Create(AOwner: TComponent); inherited Create(AOwner); FManager := TObjectManager.Create(ConexaoBanco); FSerializer := TDataSnapJsonSerializer.Create; FDeserializer:= TDataSnapJsonDeserializer.Create;
procedure TBugTrackerMethods.SalvaIncidente(Incidente: TJsonValue); FManager.SaveOrUpdate(FromJson<TIncidente>(Incidente)); FManager.Flush; function TBugTrackerMethods.CarregaIncidente(Id: integer): TJsonValue; Result := ToJson(FManager.Find<TIncidente>(Id)); function TBugTrackerMethods.ListaIncidentes: TJsonValue; var Lista: TList<TIncidente>; Lista := FManager.Find<TIncidente>.List; try Result := ToJson(Lista); finally Lista.Free; function TBugTrackerMethods.ListaIncidentesProjeto( IdProjeto: integer): TJsonValue; var Lista: TList<TIncidente>; Lista := FManager.Find<TIncidente>.SubCriteria('Projeto').Where(TLinq.IdEq(IdProjeto)).List<TIncidente>; try Result := ToJson(Lista); finally Lista.Free; E assim dessa forma os demais métodos podem ser implementados. Em resumo, como o Aurelius permite que trabalhemos diretamente com objetos, o servidor REST nada mais é que uma interface para chamadas do Aurelius: Carregar um incidente é apenas uma chamada ao Aurelius para carregar o objeto incidente, e em seguida retornar esse objeto para o servidor. JSON - Serializer e Deserializer
Além do próprio TObjectManager, que é a classe do Aurelius que permite persistir objetos no banco e dados, outras duas classes essenciais para facilitar a construção do servidor são as classes TDataSnapJsonSerializer TDataSnapJsonDeserializer. Elas permitem facilmente converter os objetos Aurelius em uma representação JSON e vice-versa. JSON é uma abreviação de JavaScript Object Notation e é simplesmente um texto contendo as informações do objeto. No caso do DataSnap, a classe TJsonValue contém a estrutura de uma representação JSON e podemos retornar/receber objetos desse tipo através de nossos métodos. Assim nosso servidor fica mais genérico e podemos acessá-lo inclusive de outros clientes, como JavaScript (como veremos adiante). Existem várias bibliotecas que fazer parser JSON e contém tipos específicos que representam essa estrutura. O Aurelius não se prende a uma framework específica e você pode usar outros tipos de parser (até mesmo se você quiser montar servidores usando outras frameworks que não seja o DataSnap). A tabela a seguir mostra as frameworks suportadas, mas você mesmo pode fazer a interface para outras: Framework Classe serialização/deserialização Tipo retornado Unit DataSnap TDataSnapJsonSerializer/Deserializer TJsonValue Aurelius.Json.DataSnap SuperObject TsuperObjectJsonSerializer/Deserializer IsuperObject Aurelius.Json.SuperObject Os métodos ToJson e FromJson usados pelo nosso servidor são implementados de forma simples como mostramos a seguir: function TBugTrackerMethods.ToJson(Obj: TObject): TJsonValue; result := FSerializer.ToJson(Obj); function TBugTrackerMethods.FromJson<T>(Value: TJsonValue): T; Result := FDeserializer.FromJson<T>(Value); Ou seja, não poderia ser mais simples: apenas chamadas aos métodos do Serializer e Deserializer. As classes de serialização do Aurelius são importantes porque elas tratam todas as especificidades da framework, como tipos especiais (TBlob,Nullable), lazy-loading (com Proxy), associações, etc., ou seja, elas se beneficiam de todo o mapeamento feito na classe. Testando o servidor no browser Ao executarmos o servidor pela primeira vez, podemos fazer um teste rápido no servidor via browser (visto que nossa servidor é um servidor REST). Utilizando o browser pra acessar o endereço do método ListaIncidentes, obtemos a resposta:
Usando uma ferramenta que formata JSON (o site jsbeautifier.org, por exemplo), podemos visualizar melhor a resposta do servidor: { "result": [ [{ "$type": "Entidades.TIncidente", "$id": 1, "FDescricao": "", "FId": 1, "FAssunto": "N\u00E3o \u00e9 poss\u00edvel excluir conta no plano de contas", "FPrioridade": "Alta", "FStatus": "Pendente", "FResponsavel": null, "FComentarios": [], "FProjeto": { "$type": "Entidades.TProjeto", "$id": 2, "FId": 2, "FNome": "Modulo Contabilidade" } }, { "$type": "Entidades.TIncidente", "$id": 3, "FPrioridade": "Baixa", "FId": 2, "FAssunto": "\u00daltima p\u00e1gina em branco ao imprimir balan\u00e7o", "FDescricao": "", "FStatus": "Executando", "FResponsavel": null, "FComentarios": [], "FProjeto": { "$ref": 2 } }]
} ] Alterando o cliente para obter os objetos do servidor Uma das vantagens de se trabalhar com objetos puros é justamente a manutençao e portabilidade. Como mostramos anteriormente, toda interface visual da nossa aplicação cliente trabalha apenas com objetos. Sendo assim, podemos facilmente alterá-la para buscar os objetos do servidor REST, ao invés de buscá-los diretamente do banco dados. Para acessar os métodos no servidor precisamos criar um proxy com os métodos. Basta executarmos o servidor e utilizarmos o wizard do DataSnap para obtermos um proxy. É só utilizar a opção File New Other... do Delphi e escolher DataSnap REST Client Module. Um wizard será iniciado, basta seguir os passos do wizard e uma unit será gerada com os métodos proxy. Basta então utilizar o proxy para chamar os métodos no servidor, retornar os objetos, e passar para nossa aplicação. Por exemplo, na nossa tela de listagem de incidentes, o código que fornece os objetos ao dataset passaria a ser simplesmente o seguinte: DatasetIncidentes.Close; Incidentes := ClientModule1.ListaIncidentesProjeto(IdProjetoSelecionado); DatasetIncidentes.SetSourceList(Incidentes); DatasetIncidentes.Open; Assim, facilmente você passa de uma aplicação cliente-servidor para uma aplicação multi-camadas. Utilizando clientes JavaScript Como o servidor REST fornece todos os objetos em formato JSON, é fácil utilizar outros clients para acesso ao servidor. Ainda, o formato JSON que o serializador do Aurelius retorna é bastante compatível com JavaScript de modo que o código que você deve escrever é muito similar ao código em Delphi. A tela e listagem abaixo mostram uma página html e seu código javascript que buscam e mostram os dados de um incidente. É permitido então ao usuário alterar o seus status para Encerrado ou Concluído, dependendo do botão que se pressiona. Utilizamos a framework jquery para deixar o código mais legível (e sem tratamento de erros):
Código JavaScript: var incidente = null; $(document).ready(function() { $('#incidentediv').hide(); $('#buscabutton').click(function(event) { incidente = servermethods().carregaincidente(buscaidfield.value).result; $('#incidentediv').show(); $('#idfield').val(incidente.fid); $('#assuntofield').val(incidente.fassunto); $('#prioridadefield').val(incidente.fprioridade); $('#statusfield').val(incidente.fstatus); $('#responsavelfield').val(incidente.fresponsavel.fnome); $('#projetofield').val(incidente.fprojeto.fnome);
$('#descricaofield').val(incidente.fdescricao); }); $('#encerrabutton').click(function(event) { incidente.fstatus = 'Encerrado'; servermethods().salvaincidente(incidente); $('#incidentediv').hide(); }); $('#cancelabutton').click(function(event) { incidente.fstatus = 'Cancelado'; servermethods().salvaincidente(incidente); $('#incidentediv').hide(); }); }); Conclusão O objetivo deste artigo era mostrar técnicas e exemplos de como construir um cliente e servidor REST usando o TMS Aurelius, beneficiando-se dos recursos da ferramenta e do paradigma de uso de uma ferramente de mapeamento objeto-relacional. Autor Wagner Rafael Landgraf, 36, é formado em Engenharia Eletrônica e Mestre em Informática Industrial pela Universidade Federal Tecnológica do Paraná. Atualmente Gerente de Produtos da TMS Software (http://www.tmssoftware.com), trabalha com Delphi desde sua primeira versão em 1995, e é responsável por produtos como TMS Scripter, TMS Diagram Studio, TMS Data Modeler e TMS Aurelius.