Por Que Não Devemos Mais Usar SQL em Nossos Sistemas Compilados

Eu gosto de SQL. Sempre gostei. Acho que sempre vou gostar.

Tive o primeiro contato com a linguagem em 2002. O banco de dados era o MySQL. Eu era estagiário de uma minúscula empresa mais ou menos perto de casa e não tinha salário. Passava o dia inteiro lá mexendo no computador, estudando coisas que eu gostava, montando pequenos sistemas e websites, e de vez em quando fazendo alguma coisa que eles precisavam. O dono da empresa era amigo da minha mãe e não exatamente havia uma função ou um projeto para mim. Havia trabalho, eu fazia. Caso contrário, podia usar o tempo da forma como melhor me convinha. Era um favor que ele fazia me deixando lá em contato com tecnologia. Para mim, estava ótimo.

A coisa que eu achava mais legal no SQL era a proximidade dele com o inglês. Era como conversar com o computador com um pouquinho de matemática. As instruções eram claras e divertidas, e era agradável criar, editar e excluir registros, ainda que como experimentos. Lembro de ter tido tardes muito proveitosas praticando interações com o banco de dados.

14 anos depois, venho a este artigo com a mesma impressão positiva sobre SQL, mas não dentro de sistemas compilados. Fora deles. É até um pouco melancólico defender isso, mas preciso ser razoável.

SQL dentro de um sistema compilado causa mais problemas do que benefícios, essencialmente quando é um sistema não trivial, ou seja, que não é formado em grande parte por CRUDs. Quanto mais complexa a regra de negócios envolvendo SQL, mais incontrolável o sistema se torma. Mas, por que passei a defender isso?

Hoje, quase todas as linguagens compiladas possuem uma implementação de um mapeador objeto-relacional. Há muita gente que é contra, alegando que mapeadores objeto-relacionais são lentos, causam problemas de performance, são limitados em operações, e assim por diante. Isso é verdade para alguns casos, não mais para a grande maioria. No nível em que as ferramentas estão, a perda de performance pode ser considerada desprezível para uma maioria de casos, merecendo otimização pontos em que os frameworks ainda não são capazes de resolver.

Ainda, modificação de sistemas compilados demoram mais em quase tudo. No teste local, no teste unitário, no teste de integração (até porque essas três etapas exigem a compilação antes) e a própria compilação não verifica SQL. Ela supõe que está tudo certo, que nós, programadores, fizemos nossa parte e produzimos o melhor SQL possível. É um otimismo que tem um preço caro lá na frente.

Em resumo, o ganho de produtividade evitando o uso de strings SQL dentro da aplicação é melhor do que a perseguição de desempenho a todo custo, simplesmente descartando o ORM. Outros problemas de desempenho podem ser atacados em compensação.

Posso ilustrar isso numa experiência recente. Meu segundo cliente nos Estados Unidos foi um grande grupo empresarial, com escritórios em três cidades diferentes. O sistema era um serviço REST cheio de ferramentas bacanas e modernas, alguns processos ricos em detalhes, e que tinha três etapas de testes. Um dos testes levava 70 minutos para iniciar. A outra coisa é que um desses testes tinha que ser repetido em dois ambientes, resultando, portanto, em quatro testes diferentes. A melhor parte era fazer merge dos meus colegas e descobrir que um código deles provocou uma falha na minha última etapa de testes. Em média, eu levava 4 dias para terminar uma história de complexidade média, contra 3 horas em sistemas sem teste algum, na minha plataforma de maior domínio, o ASP.NET MVC, com Entity Framework, um ORM em que não há escrita de SQL, onde erros de SQL praticamente inexistem.

A melhor parte: os testes nesse sistema (bom dizer, em Java) não contemplavam tudo. Alguns erros eram descobertos apenas em tempo de execução. Adivinhe de onde eles vinham?

O sistema usava pesadamente sentenças SQL bastante complexas, que eram segmentadas em blocos monolíticos. Como as sentenças eram reusadas por todo o sistema, qualquer erro fazia uma boa parte do sistema parar de funcionar.

A tendência não é algo exclusivo de uma linguagem ou de um framework. Há um padrão que pode ser notado em várias frentes. Citarei algumas.

.NET

Este é bastante óbvio, e começarei pelo clássico. Começou no LINQ to SQL, que depois deu origem ao LINQ to Entities e, finalmente, Entity Framework. Por padrão, as sentenças são escritas ou como LINQ, que é parecido com o SQL, mas que está dentro das linguagens que compilam em .NET, ou como métodos de extensão. Portanto, há toda uma conferência do compilador para que o SQL gerado pelo código seja válido.

Há também o NHibernate, o famigerado framework portado do Java, mais limitado que o Entity Framework e quase morto na data desse texto, mas que cumpriu bem sua função ainda quando era novidade, de transformar métodos do .NET em sentenças SQL. Pularei a parte de HQL porque ela é um desserviço duplo: uma sintaxe própria que nem SQL é.

Não falarei de PetaPoco, Massive e Dapper aqui porque eles usam pedaços de strings para gerar SQL, então não entram no conceito que quero explorar nessa minha tese, que é a do ganho de produtividade com ORMs agnósticos. Aqui estou analisando apenas frameworks que geram 100% do SQL a partir de estruturas que o compilador pode conferir.

Java

Obviamente eu vou falar do Hibernate, mas só de uma parte do Hibernate que não recebeu a devida atenção: o Entity Manager. Acredito que foi aqui que nasceu alguma ideia para a Microsoft fazer o Entity Framework alguns anos depois.

Este era, pra mim, o componente de framework que finalmente poderia tirar o Java da miséria criativa em que se encontra há tantos anos, assim como fez o Spring IoC no seu devido tempo. Ao invés de propor uma sintaxe sucinta, agradável e fluente, a equipe do Hibernate foi no sentido contrário: fez duas formas de interface, sendo uma idêntica ao SQL (o famigerado HQL) e outra usando critérios, objetos especiais que o Java interpreta e constrói sentenças SQL. Ainda que sejam objetos Java, são pura verborragia. É inacreditável como, ao finalmente acertarem a mão em alguma coisa, escolhem o pior caminho para implementar.

Felizmente sou um otimista, e carrego comigo a convicção de que tudo no mundo melhora, ainda que seja Java, e nesse sentido não errei. Os seguintes frameworks merecem total atenção:

Poucos deles ainda têm suporte a migrações incrementais, e o horror a folhas de estilo e sites responsivos para escrever documentações continua, além da onipresente preguiça de desenvolver os exemplos, mas me mantenho otimista.

C++

O C++ jamais irá me decepcionar. Foi minha primeira linguagem OO e ela me inspira um passado em que eu era empolgadíssimo com programação. É sempre um prazer colocar a mão em sistema C++, ainda que os empregos estejam meio raros.

C++ possui hoje os seguintes ORMs:

Concluindo

Não falei de linguagens interpretadas porque a curva de produtividade delas é alta o suficiente para que o uso de SQL puro ou ORM não seja um problema como é nas linguagens compiladas.

Considero que o exposto aqui deva ser seriamente levado em consideração por arquitetos e gestores de times grandes (ou seja, mais do que 8 pessoas). Supondo 10 pessoas, 2 erros de SQL para cada e 30 minutos entre arrumar o código, compilar, testar, enviar para o serviço de integração e aguardar o deploy - sendo MUITO otimista, claro - são 10 homens/hora jogados fora por dia. Coloque isso na sua planilha mensal de custos e vamos ver se não vale mesmo a pena sair do SQL tradicional para algo que seu compilador possa detectar.