Posts Marcados ORM

Produtividade – Camada de Domínio – Revisão

Olá pessoal! Em 2009 (nossa!), eu escrevi uma série sobre produtividade, onde a ideia era colocar as técnicas que usamos no nosso dia a dia na White Fox para o desenvolvimento de sistemas. Embora os princípios continuem todos válidos, a tecnologia evolui. Assim, a ideia é fazer uma série atualizando cada uma destas postagens, de acordo com a linha que usamos atualmente.

A primeira delas é sobre a camada de domínio. Nesta camada, tivemos um amadurecimento de tecnologias que hoje tornam mais fácil a vida de quem precisa utilizar um ORM. No entanto, cresceram as dúvidas e questionamentos teóricos sobre que mecanismo utilizar para acessar dados. A linha do NoSQL ganha força e é o padrão para muitos tipos de sistema, especialmente agora, com um uso maior de sistemas baseados em PAAS como Azure ou Amazon.

Mesmo para os sistemas com domínio orientado à objeto e banco SQL, há vários questionamentos sobre se faz sentido o uso de um ORM. Existem várias pessoas que apontam os problemas de se tentar usar um (exemplo aqui) e nós mesmos já sofremos bastante com problemas oriundos deste mapeamento; recentemente fiz até um post sobre isto. Existem até aqueles que questionam o uso de OO em si, ou os que estão partindo para linguagens funcionais como o F#.

Mas, para quem usa bancos SQL, o uso de ORM ainda é a melhor alternativa. Claro que isto pode gerar problemas e definitivamente há casos em que é melhor não usar. Mas em termos de produtividade e manutenabilidade, ainda é a melhor opção. Temos usado consistentemente em todos nossos sistemas e, salvo algumas exceções em pequenas áreas, o resultado tem sido excelente.

Em termos de tecnologia, temos mais alternativas do que o nHibernate, que reinava absoluto em 2009. Vários pequenos frameworks como o nPoco tem um uso mais difundido. E o Entity Framework (EF) da Microsoft amadureceu e hoje compete de igual para igual com o nHibernate. Nossos sistemas usam predominantemente o nHibernate, até porque temos grandes sistemas em operação que começaram com ele. Mas, para novos sistemas, estamos preferindo o EF, principalmente por sua excelente integração com o LINQ do .NET. Utilizamos o database-first, com a declaração fluente de mapeamento e ainda usamos arquivos .TT para gerar todos os artefatos como repositórios, containers etc.

Quando o sistema é muito pequeno ou quando desempenho é um requisito especialmente severo, criamos uma versão de ORM que utiliza stored procedures diretamente. É claro que isto tem sérias restrições, mas para estes tipos de sistemas conseguimos otimizar removendo quase todas as camadas e utilizando o máximo poder do servidor SQL.

Para tentar minimizar a manutenção, criamos bibliotecas comuns a todos os ORM que utilizamos. Assim, trabalhar em sistemas que usam ORMs diferentes é uma experiência similar da camada de negócios para cima. Temos caso até de, em um mesmo sistema, termos diferentes áreas usando diferentes ORMs. Claro que nestes casos, temos que ter uma camada de comunicação para garantir integridade, muitas vezes baseadas em microservices.

Em termos de regras de negócio, continuamos utilizando um modelo anêmico. Esta continua sendo uma guerra santa na comunidade técnica, mas no nosso caso, não temos como negar todos estes anos de sucesso. O modelo anêmico facilita o isolamento de regras de negócio, simplifica o treinamento de novos desenvolvedores e a manutenção de todos os nossos sistemas. E estamos ainda colhendo alguns bônus adicionais, pois este tipo de modelo tem facilitado o isolamento de porções de regras de negócio para encapsulamento em micro-serviços, o que nos tem permitido evoluir nossos sistemas mais antigos de maneira viável. E, na inevitável migração para a nuvem, que deve ocorrer ao longo dos próximos anos, vamos poder também fazer uso de recursos avançados como Azure Servless Functions, graças a esta arquitetura.

Nos próximos posts devo comentar sobre a camada de interface, onde tivemos as maiores mudanças e as maiores evoluções tecnológicas. Até lá!

, , , , ,

Deixe um comentário

ORM e Concorrência

Olá pessoal! Há algum tempo atrás sofremos um pouco na manutenção de um dos grandes sistemas da White Fox. Quero compartilhar a experiência para que outros não tenham que passar pelas mesmas dificuldades!

Só relembrando, conforme meu antigo post sobre a camada de domínio, nós utilizamos ORM (nHibernate ou Microsoft Entity Framework) para fazer o mapeamento de entidades para o banco de dados, de modo a abstrair os mecanismos de persistência. Isto tem funcionado muito bem para nós – utilizamos ORM em praticamente todos nossos sistemas de maior porte há mais de 10 anos com sucesso. Claro que por mais que tentemos compartimentalizar, ao longo do tempo os domínios acabam crescendo. Hoje temos sistemas com domínios de mais de 300 objetos e milhares de linhas de código de regras de negócio.

Pois bem, neste sistema em particular, começamos a perceber há algum tempo, problemas de dados somente no ambiente de produção. Valores que aparentemente ficavam errados do nada. E, obviamente, nenhum destes problemas aparecia em homologação ou durante a execução de testes unitários. Claro que, como todo acidente, nenhum erro mais grave tem uma única causa. Neste caso em específico, também temos uma sequência de eventos que levaram à falha. Olhando agora, são até que óbvios, mas gastamos um bom tempo no diagnóstico e solução.

O primeiro componente começou com uma prática usual de ORM. Quase todos possuem uma proteção para evitar que a persistência ocorra em um registro que foi alterado por outrem. Por exemplo, se o ORM carrega um objeto em memória, faz nele alguma alteração e ao salvar, detecta que o registro no banco não é o mesmo de quando o objeto foi carregado, ele gera uma exceção. Porém, este mecanismo deixa tudo lento, já que para implementá-lo, o ORM acaba tendo que fazer uma query a cada UPDATE. E como usamos transações, os LOCK do banco de dados acabam garantido a atomicidade da operação, assim esta proteção acaba ficando redundante. No nosso caso ela sempre é desligada.

O segundo componente é a criação de campos para contadores ou totalizadores. Sim, claro, isto é algo que se deve evitar, especialmente como atributo de uma entidade de domínio. Por exemplo, colocar o total de uma nota fiscal como atributo ao invés de calcular o total através da soma de seus elementos. Usualmente evitamos isto, mas, em algumas entidades, calcular o valor toda hora pode ser complicado, seja porque existe alguma regra de negócio muito complexa envolvida ou um número muito grande de elementos para compor o total. Então, em algum momento, alguém resolve que manter o totalizador oferece uma melhor razão custo/benefício. Obviamente que se usam transações para manter os totais e se criam testes unitários para garantir que os totalizadores funcionam em todos os cenários.

O último componente do problema é a concorrência. Dificilmente teste unitários são criados para simular uma carga de múltiplos usuários simultâneos, pela complexidade de se simular este tipo ambiente. Assim, no teste unitário, quase não há concorrência. Porém, em produção os nossos sistemas são usados por centenas de usuários simultâneos. Apesar dito, tipicamente um usuário faz transações em uma única grande “entidade” por vez (p. ex., vendendo um produto). Assim, mesmo vários usuários em paralelo dificilmente mexem na mesma entidade, simultaneamente, ao mesmo tempo.

Mas as exceções é que fazem a coisa desandar. Se desligamos a proteção de dados alterados, usarmos totalizadores em entidade e usuários alteram esta mesma entidade quase ao mesmo tempo, temos o nosso problema acontecendo! Para detalhar, vejam a figura a seguir. Imaginem 2 processos executando em paralelo, em tempos muito próximos um do outro.

image

Em um momento 1, ambos carregam a mesma entidade em memória, uma delas com um atributo totalizador. Como neste momento ainda não houve nenhuma transação, ambos conseguem carrega-la simultaneamente, e ambas possuem o mesmo valor para o atributo. No momento 2, ambos fazem algum processamento em que vão atualizar o campo totalizador. No momento 3, o primeiro processo abre uma transação, salva os objetos e faz o COMMIT. O segundo processo tenta fazer o mesmo, porém como o primeiro fez o LOCK, ele é bloqueado e fica em espera. Se não cair por timeout, quando o primeiro processo acabar, ele é liberado, começa sua transação, salva os seus objetos e faz o seu COMMIT. Como não há proteção para alteração, ele não vai perceber que o objeto foi alterado pelo primeiro processo e vai completar a ação achando que tudo correu bem.

Mas percebam que o segundo processo partiu objetos com totalizadores incorretos! Como o ele carregou os objetos no mesmo momento que o primeiro processo, ele não vai contemplar as alterações feitas por ele e vai salvar um total incorreto. Um exemplo, ambos carregam um atributo com um valor total de 10, ambos somam 1, o primeiro salva 11, o segundo também vai salvar 11, incorretamente!!!

Uma vez diagnosticado, o problema também não é simples de resolver. Não há solução trivial com o uso de ORM. Soluções como ativar a proteção de alteração, usar LOCK pessimista ou usar singletons, possuem pontos extremamente negativos e foram rejeitadas por nós. No final, a solução que adotamos foi, para campos totalizadores, ignorar totalmente o ORM e ir direto ao banco. Fizemos isto com o uso de um repositório especialmente projetado para este fim e com o uso de stored procedures para garantir que as alterações sejam feitas com as proteções adequadas. Esta solução conseguiu até mesmo melhorar o desempenho da aplicação, pois evitamos a manipulação de totais pelo ORM e transferimos todo o trabalho para o banco. O ponto negativo é que o sistema fica bem mais complexo de manter e uma porção das regras de negócio saiu do domínio e foi para o banco. Mas de todos os males possíveis, este foi o que achamos de menor impacto.

Moral da história é: conheça seu ORM, evite a todo custo campos totalizadores e, se tiver que usá-los, não se esqueça que poderá ter graves problemas em um ambiente com concorrência.

Este e outros eventos tem nos feito repensar o uso de ORM como um absoluto. Acho que já está na hora de refazer minha série sobre produtividade, atualizando-a com as tecnologias e práticas que temos adotado nos últimos anos. Tudo muda e TI muda ainda mais rápido. Acho que até que conseguimos ter uma relativa estabilidade nos nossos ambientes por muito tempo. Mas obviamente a evolução é necessária e tem hora que mudar paradigmas é essencial para mantermos nossa produtividade. Mais sobre isto em breve!

Até a próxima!

, , ,

1 comentário

Produtividade – Camada de Domínio

Continuando a série sobre produtividade, vou falar neste post sobre a camada de domínio. A camada de domínio é a que contem todas as entidades e regras de negócio, sendo utilizada pela camada de interface e fazendo uso do banco de dados para armazenamento de informações.

Inicialmente existe a questão da impedância objeto-relacional. Para o desenvolvimento o usual é trabalhar com objetos em código para representar objetos do mundo real, com seus vários atributos e relacionamentos. Uma tabela de banco é uma representação mais pobre e difícil de manipular, por isto trabalhar com objetos é mais simples. No entanto, o banco de dados relacional é necessário (bancos de dados orientados a objeto ainda são praticamente inviáveis, por uma série de razões), logo, deve haver uma maneira de converter classes em tabelas e instâncias em registros. Assim, o uso de um ORM é fundamental, pois esta tarefa tem que ser simples, não faz sentido gastarmos tempo de desenvolvimento escrevendo e debugando comandos SQL básicos.

Nosso ORM de escolha é o nHibernate (NH). No entanto, escrever arquivos de configuração paro nHibernate é trabalhoso (e não queremos trocar o trabalho de escrever SQL para o de escrever configurações), assim utilizamos também o Castle ActiveRecord (AR). O AR permite definir objetos com simples atributos de decoração e ele já traz também uma série de implementações pra facilitar o uso do NH. Apesar de ser mais simples, decorar classes e atributos ainda é trabalhoso (e erros nesta etapa podem causar graves conseqüências para aplicação), então para simplificar ainda mais, utilizamos o ActiveWriter (AW), que é uma DSL gráfica que permite descrever as entidades e ele gera todas as classes e propriedades já decoradas. O AW tem outras vantagens, descritas a seguir.

Até aqui super simples, mas existem duas guerras religiosas envolvidas que acho importante citar. As duas têm relação com o chamado “modelo anêmico”, definido pelo Fowler. A primeira é sobre se todos os objetos de domínio devem ou não ter representação direta em banco. Em um modelo OO puro isto não seria desta forma. Porém a minha opinião é de que é muito mais simples, pra efeito de desenvolvimento, manutenção, extração e até conversa com os usuários avançados, que haja o mapeamento 1:1. Ou seja, cada classe é uma tabela, cada registro uma instância. Os puristas afirmam que isto não é OO, pois o modelo de entidades de negócio acaba sendo só uma representação do banco. Na minha visão sim, pode ser… Porém como o banco na verdade é criado para suportar as classes, há um caminho de duas vias aí. E os benefícios que tenho colhido nos últimos 10 anos trabalhando desta maneira são muito grandes pra tentar mudar simplesmente pelo purismo. Assim, no nosso caso, há o mapeamento 1:1 sempre.

A segunda guerra religiosa tem a ver como onde as regras de negócio são implementadas. No Domain-Driven Design (DDD), existe o conceito de serviços, porém muitos acham que regras específicas de objetos devem estar na classe. Eu não. Eu acho que TODAS as regras de negócio devem estar definidas em Serviços (com exceção de regras de consultas, que estão nos repositórios e de regras de construção que estão nas fábricas). Com isto o nosso modelo é por definição, completamente anêmico. Nossos objetos são somente POCO e todas as regras estão sempre localizadas nos serviços. Apesar de alguns acharem isto extremo, no meu caso eu tenho tido muito sucesso trabalhando desta forma. Quando se tenta colocar regras em objetos, muitas vezes há a dúvida de se uma regra pertence a uma ou outra classe. Com o tempo, ninguém sabe onde está o que. Além disto, fica muito mais difícil trabalhar com AOP desta forma, pois em caso de um objeto chamar outro, fica difícil definir de quem é a responsabilidade e controlar transações, segurança, erros etc. Da maneira como eu faço, que é tudo na camada de serviços, ninguém nunca tem duvida de onde procurar uma regra, todos os aspectos estão lá e a manutenção do sistema é super tranqüila. Tenho sistemas de mais de 10 anos (com objetos ainda em C++!) onde continua sendo fácil mesmo pra quem não conhece, de identificar e alterar regras de negócio.

Assim, na nossa implementação, temos um repositório e uma fábrica para cada entidade de negócio (que por sua vez representa uma tabela). E os serviços são criados conforme necessário, onde cada regra é stateless e completamente procedural. Puristas falam que isto não é DDD. Pode ser. Mas é muito produtivo!

Para gerar serviços e repositórios, utilizamos arquivos de template (.TT) do visual studio. Eles utilizam a definição gerada pelo AW e automaticamente já constroem todos os repositórios, fábricas, operadores e demais artefatos de auxílio. Ou seja, ao arrastar graficamente uma tabela para o nosso diagrama AW, todos os artefatos mínimos para uso da entidade já estão prontos! Claro que iremos expandir os repositórios e fábricas com novos métodos e isto é feito através de classes parciais. Todos os repositórios, fábricas e serviços são implementados através de um container IoC, o Castle Windsor. Com isto uma troca futura de qualquer destes componentes é super simples – p. ex., a única dependência para NH que temos são as implementações de repositórios. Tudo fica em um único assembly que chamamos de assembly de domínio.

Finalmente, temos algumas classes estáticas de apoio como, por exemplo, uma com todos os nomes de propriedades de todas as entidades, outra com um acesso rápido para repositórios e fábricas e uma com todos os serviços do projeto. Outra classe muito útil é a de geração de consultas. Em repositórios, as consultas podem ser feitas via HQL do NH ou via consulta direta. Como a maior parte das consultas é simples, elas são feitas por critérios diretos e pra isto utilizamos uma interface fluente para ajudar a escrever. Exemplo:

return Query.Where(Restrictions.Eq(PN.User, user)).And(Restrictions.IsNotNull(PN.ClosedOn)).Count();

A consulta é feita para uma entidade chamada “Incident” e retorna o total aberto para um usuário. Query é a classe de apoio de consulta e PN são os nomes de propriedades – usamos isto para evitar strings e com isto minimizar o número de erros de digitação.

Estou muito satisfeito com a qualidade e a produtividade gerada pela nossa camada atual de domínio. Fazer e dar manutenção em objetos é algo que para nós é super simples e podemos nos dedicar e escrever regras de negócio e interface. O próximo post vai ser sobre a camada de interface.

, , , , , , ,

3 Comentários