ORM ainda é um antipadrão?
(github.com/getlago)- ORMs costumam ser criticados como “brinquedos para startups”, mas a questão real está mais no modo incorreto de uso do que no ORM em si
- Como grafos de objetos e tabelas relacionais são modelos diferentes, surge um desalinhamento de paradigmas, mas isso por si só não basta para classificar ORM como antipadrão
- O debate sobre violação de SRP e SOC é válido, mas como o ORM é, desde o início, uma abstração para conectar dois modelos de dados, é difícil julgá-lo apenas por possível violação de princípios
- Problemas de desempenho costumam surgir menos do ORM e mais do padrão de combinar dados na lógica da linguagem hospedeira, e podem ser reduzidos com os recursos semelhantes a SQL do ORM
- O principal motivo para a Lago trocar consultas do Active Record por SQL bruto não foi velocidade, e sim visibilidade e depuração; em consultas grandes, migrar para SQL bruto pode ser mais vantajoso na prática
Posição básica sobre as críticas ao ORM
- ORMs são frequentemente atacados como “antipadrão”, mas concluir que o ORM em si é ruim é um exagero
- Como qualquer outra abstração, ORMs não são perfeitos e podem gerar alguma perda de visibilidade e custo de desempenho
- A Lago enfrentou problemas ao depender do Active Record, o ORM do Ruby on Rails, e isso a levou a reavaliar a forma como dependia de ORMs
- A conclusão ficou mais próxima de tratar ORM como uma abstração com vantagens e desvantagens claras, e não de abandoná-lo por completo
Diferença entre o modelo de objetos e o banco de dados relacional
- Os objetos com que o ORM lida se parecem mais com um grafo direcionado em que nós apontam para outros nós
- Tabelas de bancos de dados relacionais se parecem mais com um grafo não direcionado, em que os dados se conectam bidirecionalmente por meio de chaves compartilhadas
- Um ORM pode até simular um grafo não direcionado, mas a configuração e o comportamento nem sempre são simples
- O objeto
Userpode não incluir o arrayPosts - As entidades do array
Postspodem não ter uma referência de volta para o mesmo objetoUser - Mesmo que exista referência de volta, ela pode apontar para um objeto duplicado, e não para a mesma instância
- O objeto
- Por exemplo, um ORM pode retornar as postagens de um usuário em um array sem incluir, em cada postagem, a referência inversa ao usuário no papel de autor
- Esse desalinhamento é uma boa crítica acadêmica ao ORM, mas na operação real os problemas maiores aparecem no modo concreto de uso
Debate sobre SRP e SOC
-
Princípio da Responsabilidade Única (SRP)
- ORMs são criticados por violar o Princípio da Responsabilidade Única (SRP)
- Isso porque podem levar uma classe ou camada a assumir vários papéis ao mesmo tempo
- Executar operações de banco de dados e transações
- Representar registros
- Definir relacionamentos
- Gerar e executar migrações
- Mas, como o objetivo do ORM é justamente conectar dois paradigmas de dados diferentes, é difícil rejeitá-lo apenas com base em alguma violação de princípio
-
Separação de Responsabilidades (SOC)
- ORMs também podem enfraquecer o princípio de Separação de Responsabilidades (SOC)
- O SOC diz que um componente de infraestrutura deve ter apenas uma única responsabilidade
- Como o ORM desloca a gestão do banco de dados para o lado do backend, há espaço para considerá-lo uma violação de SOC
- Ainda assim, hoje em dia componentes de infraestrutura e padrões de código também combinam vários papéis por razões de desempenho, latência e organização do código
- Agregadores de CPU dentro de bancos de dados OLAP
- Backends-frontends de edge
- Monorepos
Problemas de desempenho surgem mais no modo de uso do que no ORM
- A crítica de que ORMs são sempre ineficientes é em geral incorreta
- Muitos ORMs podem operar com muito mais eficiência do que os desenvolvedores imaginam
- O problema é que eles facilitam combinar dados usando lógica da linguagem hospedeira como JavaScript ou Ruby
- No exemplo com TypeORM, é tratado como padrão ruim consultar primeiro os autores de uma empresa específica, depois buscar novamente as postagens de cada autor e então salvar cada postagem separadamente
- Uma forma melhor é montar uma única query de atualização com
createQueryBuilder()do TypeORM - Na refatoração do SQL de faturamento da Lago, também não houve diferença de desempenho entre a versão com Active Record e a versão em SQL bruto
- Como os recursos de composição de dados do Active Record já eram bastante usados, a consulta existente já estava otimizada
- Reescrever em SQL bruto teve mais a ver com ganho de visibilidade do que com melhoria de desempenho
Quando o ORM pode realmente ficar lento
- ORMs nem sempre são tão eficientes quanto SQL bruto e, em algumas situações, podem ser bastante ineficientes
- O primeiro problema é que o ORM pode criar grande sobrecarga computacional ao converter o resultado da query em objetos
- TypeORM é citado como exemplo desse problema
- O segundo problema é quando, ao percorrer relacionamentos um-para-muitos ou muitos-para-muitos, várias idas e vindas são feitas ao banco de dados
- Esse padrão é chamado de problema N+1
- A uma query original somam-se N subqueries
- No exemplo com Prisma, ao buscar usuários, postagens e comentários de forma aninhada, cada comentário pode gerar uma nova requisição ao banco de dados
- O N+1 aparece com frequência em ORMs, mas com um data loader é possível reduzir N+1 queries para apenas 2
- Boa parte dos problemas comuns de ORM pode ser evitada se seus recursos forem usados de forma completa
O problema maior é visibilidade e depuração
- O maior ponto fraco de ORMs é a visibilidade
- Como o ORM é, na prática, um construtor de queries, ele não é o transmissor final do erro, exceto em casos óbvios como erro de tipo primitivo
- Quando um erro de SQL é retornado, o ORM precisa interpretá-lo e repassá-lo ao usuário
- O Active Record teve dificuldades nesse ponto, e foi por isso que a Lago refatorou sua query de assinaturas de faturamento
- Quando o resultado saía diferente do esperado, era preciso repetir o seguinte processo
- Verificar a query SQL renderizada
- Executar essa SQL novamente
- Traduzir o erro SQL de volta para mudanças no Active Record
- Esse vai e vem enfraquece o objetivo original de usar Active Record justamente para não interagir diretamente com o banco SQL
Avaliação prática
- Quando usados corretamente, ORMs podem alcançar eficiência próxima à do SQL bruto
- Muitos problemas surgem quando, em vez de usar os recursos semelhantes a SQL do ORM, se depende demais da estrutura lógica da linguagem hospedeira
- Se consultas grandes estiverem dificultando o trabalho dos desenvolvedores e atrapalhando a depuração, migrar para queries em SQL bruto pode ser um bom investimento
- A maioria dos ORMs oferece recursos para executar queries SQL de dentro do próprio ORM
1 comentários
Comentários do Hacker News
Uso ORM desde que o Hibernate apareceu no Java, mas sempre achei ruim
A vantagem de “poder trocar de banco de dados” hoje parece quase conversa fiada, e na prática ninguém usa assim
A ideia de que “não precisa saber SQL” também é falsa, e qualquer app não trivial que dure bastante tempo acaba exigindo ajustes em consultas específicas no nível de string
A camada de dados funciona melhor criando e interpolando cada consulta como SQL em string, e, na minha visão, quanto mais perto de raw JDBC, melhor
A alegação de que ORM dá suporte a um “modelo de domínio” também é um mau motivo, porque esse modelo de domínio sempre acaba virando um modelo de domínio anêmico, sem lógica
Dá vontade de chorar pensando no tempo desperdiçado com ORM, XML, anotações, depuração do SQL gerado etc.
As partes fáceis já são fáceis, então não me interessam muito; em compensação, no pior momento, quando as consultas desmoronam ao ganhar escala, muitas vezes o trabalho ficou um milhão de vezes mais difícil porque eu ainda tinha de brigar com o próprio ORM
Query builders não são exatamente a mesma coisa que ORM, mas gosto deste texto, https://gajus.medium.com/stop-using-knex-js-and-earn-30-bf41..., e virei fã de tecnologias no estilo Slonik
Elas oferecem 90% do que eu quero, como binding automático do resultado em objetos e checagem de tipos forte, e mesmo assim deixam você simplesmente escrever SQL normal
Também não entendo a ideia de que SQL seria uma linguagem misteriosamente difícil. Ela é antiga e tem defeitos, mas ainda é melhor aprender SQL do que aprender a linguagem de consulta própria criada por ferramentas de ORM
Na época, quando eu fazia esse tipo de crítica, outros desenvolvedores olhavam de cima para mim
Ainda hoje há muitas ferramentas e técnicas que, como desenvolvedor, sou forçado a usar mesmo sendo antipadrões, e a triste realidade do setor é que boa parte das ferramentas amplamente usadas é ruim e incentiva antipadrões
Na indústria de tecnologia existem alguns poucos desenvolvedores e ex-desenvolvedores bem conectados que concentram o poder de decisão, e acho que eles são bem ruins nisso
Há ORMs com uma relação razoável entre prós e contras, especialmente no ecossistema Python
O Django ORM é meio bruto e o desempenho não é grande coisa, mas é bem integrado, prático, produtivo e agradável de usar
O Peewee oferece ORM em um pacote pequeno, então, quando você não precisa de nada muito sofisticado e só quer evitar dor de cabeça, ele torna agradável escrever programinhas pequenos
O SQLAlchemy exige mais investimento, mas é muito flexível, gera SQL limpo e funciona corretamente
Ele também expõe query builders de nível mais baixo para quando você não quer o paradigma orientado a objetos e prefere expressar em Python uma abstração de comportamento idiomático de SQL
No fim, isso vira uma decisão normal de engenharia baseada em retorno sobre o investimento
Faço isso há mais de 10 anos e nunca houve uma consulta que eu não conseguisse tratar melhor apenas lendo a documentação do Hibernate por 5 minutos e fazendo o que ela mandava naquela situação
Parece que os fanboys de SQL, que acreditam que todo desenvolvedor deveria decorar tabelas de lógica de três valores e nomes de funções em estilo COBOL, na verdade não querem ter o trabalho de ler a documentação do Hibernate
ORM é para workloads OLTP, não para OLAP, e um dos criadores do Hibernate já disse isso
A utilidade principal é evitar escrever manualmente consultas INSERT/UPDATE longas e propensas a erro, além de fazer o mapeamento automático de linhas SQL para objetos
Se você for pelo caminho raw, vai acabar reimplementando boa parte disso, então isso também não é um jogo de soma zero
Em apps suficientemente complexos, em algum momento acaba surgindo algo como um query builder, em que várias partes da aplicação podem compor consultas de forma adiada juntas
A estrutura passa a combinar tudo em consultas, em vez de juntar, filtrar e ordenar os dados em memória
O ORM é uma ferramenta que facilita isso, mas também pode acabar te sabotando ao tornar muito fácil criar consultas N+1 sem perceber
Como qualquer ferramenta, é preciso avaliar os trade-offs junto com o caso de uso real, e não recomendo manipular diretamente strings de comandos SQL em hipótese alguma
Mesmo assim, sempre há quem ignore essas diferenças e diga de forma absoluta que ORM é bom ou ruim
É melhor tratar isso como uma postura provocativa para chamar atenção e seguir em frente; prefiro focar em discussões que partem de “ORM tem vantagens, mas é preciso saber onde elas se aplicam e garantir que os problemas de uso não superem os benefícios”
Se os dados são necessários, vá até a camada de acesso a dados; se ela não fornece o que você precisa, crie um novo repositório, provider ou outro elemento compatível com o padrão adotado
Seja um ORM ou um combinador de consultas feito à mão, quando o app fica a ponto de te fazer achar que “é complexo o bastante para precisar disso”, ele já ficou complexo demais para ser usado com estabilidade
No fim, você acaba avaliando o builder na camada errada ou deixando passar que está criando consultas N+1 ao iterar pelos resultados
ORM não é necessário
LINQ não é exatamente um ORM; quando você cria instruções LINQ fortemente tipadas, elas viram árvores de expressão, e depois um provedor de consultas as converte em consultas
Durante o desenvolvimento, prefiro a legibilidade do ORM dentro dos modelos, mas, quando necessário, monitoreo de perto e otimizo N+1 e outras ineficiências
O limite de quando isso é “necessário” não é claro, mas tento não otimizar cedo demais
Em projetos novos, costumo começar pelo banco de dados e, antes de codar, pego a intuição do modelo de dados com factories, seeders e consultas em raw SQL
ORMs como o Eloquent do Laravel têm bons métodos para resolver N+1 e fazer lazy loading, mas sempre há trade-offs
Em vez de permitir composição adiada de consultas, você escreve a consulta diretamente e a biblioteca gera, em tempo de compilação, o código para aquela consulta
Por exemplo, você escreve uma consulta como
getUser: SELECT * FROM users WHERE id = ?, e ela gera uma classeGetUserQuerye um método comogetUser(id: String): GetUserQueryResult; acho esse modelo muito melhorO principal motivo de eu não gostar de ORM é a tendência de tratar os motores SQL modernos como um depósito exageradamente burro de bits
CTEs, joins LATERAL e cláusulas RETURNING podem simplificar muito o processamento ou eliminar a possibilidade de inconsistência de dados, mas os ORMs em geral ficam no nível de fazer um mapeamento simples de tabelas básicas e views para objetos predefinidos
Fica pior ainda quando o ORM cria as tabelas também
SQL é, por natureza, uma ferramenta de extração de dados e também uma linguagem de transformação, mas os ORMs quase ignoram esse lado, então muitos desenvolvedores nem chegam a saber que existe algo em SQL além de INSERT/SELECT/UPDATE/DELETE
Existem tabelas pivô, consultas temporais, CUBE/ROLLUP, funções de janela, funções que retornam conjuntos, views materializadas, tabelas externas, processamento de JSON, tratamento de datas, restrições de exclusão, tipos como range/interval/domain, segurança em nível de linha, MERGE e mais
É como ter uma oficina completa de ferramentas e alguém te entregar só um martelo, uma chave de fenda e um arco de serra, tentando te convencer de que isso basta, enquanto você passa a carreira sem nem saber que a poucos metros dali havia serra de bancada, tupia, lixadeira e um conjunto de cunhas
O ORM tende a fazer as pessoas pensarem principalmente em instruções CRUD simples, mas talvez isso seja melhor mesmo, e recursos avançados devam ser usados com parcimônia
Ainda assim, quando preciso usar DB, tento de propósito usar apenas um subconjunto muito pequeno dos recursos do banco
Isso porque vejo o “código” como uma área que posso controlar, e o “banco de dados” como uma área perigosa e difícil de alcançar, em que os erros aparecem depois, em lugares como testes de integração lentos
A lógica que acontece dentro do DB fica fora do alcance dos testes de baixo nível, então me assusta; provavelmente eu nem usaria uma consulta
select-from-selectDeixaria com prazer a maior parte da oficina de ferramentas de lado
A maior parte do tempo foi gasta criando soluções alternativas elaboradas para reinventar proteções básicas de integridade de dados
Criar seu próprio banco de dados pode ser divertido como aprendizado, mas não em um app de produção
ActiveRecord sempre me pareceu bom
Há algumas situações excepcionais, mas ele acerta bem no ponto 80/20, permite escapar para SQL quando necessário, e os hooks de ciclo de vida para organizar a lógica de domínio também são excelentes e fáceis de entender
Gosto de ORMs pragmáticos que não escondem tudo atrás de “pacotes” nem introduzem sua própria linguagem de consulta
Os ORMs que tentam “resolver” completamente o problema de ORM é que são difíceis de lidar
É assim que um ORM deveria ser feito
Mas, para usá-lo corretamente, é preciso ter um bom conhecimento prévio de SQL
Os problemas começam quando as pessoas não aprendem SQL e não entendem as consultas que o AR gera nem suas limitações
A premissa está errada. ORM nunca foi uma “ideia prejudicial”
Existem ORMs horríveis de se trabalhar, mas isso não significa que o gênero inteiro seja uma má ideia
Uso SQLAlchemy há anos com prazer justamente porque ele não tenta reinventar o próprio conceito de SQL
Em vez disso, ele fornece uma camada conveniente para montar e passar queries, além de cuidar direito dos pequenos detalhes para que você não precise mexer neles manualmente
Aprendendo Rust recentemente, escrever na mão o código boilerplate para lidar com CRUD simples parece doloroso e uma perda de tempo
O ORM do SQLAlchemy pode atrapalhar um pouco em queries mais complexas ou que você quer otimizar, mas nesses casos sempre dá para descer direto para SQL
Como qualquer coisa, há situações em que encaixa bem e outras em que não, e tecnologia pode ser facilmente abusada ou usada de forma errada
Por exemplo, ORMs como o Entity Framework do .NET podem ser um grande trunfo, especialmente ao trabalhar com uma abordagem database-first
Se for um banco com centenas de tabelas e relacionamentos de chave estrangeira corretos, um ORM ajuda a navegar por esquemas complexos e fornece verificação em tempo de compilação quando há mudanças
SQL embutido é uma faca de dois gumes que parece trazer vantagens de performance, mas é muito difícil de manter quando se trata de SQL verboso hardcoded, especialmente em joins
Não estou falando de quando há 5 a 10 tabelas, e sim de quando há centenas
Não pretendo convencer ninguém, mas você não deve simplesmente acreditar no que leu na internet sobre boas práticas
As pessoas podem estar falando de coisas completamente diferentes, e sempre vai haver desenvolvedores ruins usando qualquer tecnologia
Se você não usa ORM nem outra interface para DB, no fim acaba escrevendo e mantendo uma por conta própria para coisas como CRUD
E há uma boa chance de ela vir com bugs estranhos, edge cases e brechas de segurança
Também há um problema em ensinar princípios como SOLID em disciplinas universitárias como se fossem princípios de design de software
Na academia isso faz sentido, porque restrições como tempo, manutenibilidade, dinheiro e esforço importam menos, mas quando recém-formados entram no mercado acontece um choque bem ruim
Porque eles se acostumam a passar o dia inteiro aparando pelo em iaque e reinventando a roda em busca da solução “perfeita”
Se não houver desenvolvedores sêniores/líderes suficientes e competentes para conter isso, o resultado são produtos muito interessantes, estouros de orçamento, grandes incêndios, ou alguma combinação disso
Pior ainda é quando algo criado na academia e para a academia é solto para o público em geral
Um bom/mau exemplo clássico é o OpenStack, que tem alças e pontos de ajuste demais e componentes modularizados de forma insana para suportar os ambientes especiais do CERN e de centenas de universidades, mas é tão bizantino que é quase impossível começar a usá-lo
ORMs não trocam de banco sem esforço, não eliminam a necessidade de aprender e entender o SQL que geram, nem deixam o schema suave e sem atrito
Ainda assim, a razão para usá-los é que eles são uma abstração padrão que de qualquer forma teria de ser replicada
Eles permitem cache de acesso ao DB, algo surpreendentemente difícil de fazer em escala com SQL manual
Junto vêm ferramentas padronizadas e boas práticas que podem tornar irrelevantes problemas como N+1
O principal problema dos ORMs são as expectativas erradas e os ORMs ruins
Se quiser entender o valor de um ORM, abra um projeto escrito sem ORM e tente mudar qualquer coisa
Veja https://news.ycombinator.com/item?id=36498583
Nada útil avançava, ou demorava de 20 a 100 vezes mais do que uma abordagem rápida e suja
O pior caso foi quando uma equipe de designers HTML experientes foi forçada a aprender programação funcional
O mago da configuração de backend era uma maravilha da engenharia moderna e do design elegante, mas levou 1 ano para 10 pessoas construírem
Dinheiro de VC deixa as pessoas estranhas
A linguagem SQL é muito complexa, e criar um query builder realmente completo e com todos os recursos não é nada trivial
Isso mesmo deixando de lado a parte de mapeamento relacional em si
No caso de SQL, acho que este é um daqueles casos em que o certo é “simplesmente programe essa droga você mesmo”
Não faz muito sentido gastar tempo aprendendo SQL ou uma biblioteca ORM; é melhor aprender e usar SQL diretamente
Acesso a banco de dados provavelmente é algo fundamental com que você vai lidar durante toda a carreira, então o melhor é aprender a tecnologia real em si
Com CSS é parecido: em vez de aprender uma biblioteca de CSS, você deveria aprender e usar CSS
Na verdade, isso vale para quase todos os aspectos do desenvolvimento frontend; em vez de uma “biblioteca de formulários”, basta lidar diretamente com a API de formulários do navegador
Pessoalmente, achei SQL muito mais fácil de aprender do que qualquer ORM
Não vale a pena desperdiçar o custo de aprender a coisa errada
O mesmo vale para CSS: se você conhece CSS, fica muito mais flexível e entende muito melhor quando escolhe uma biblioteca por conveniência
Passando por mainframes, desktops, cliente/servidor, web e várias linguagens sobre essas plataformas, SQL sempre foi o fio comum
Concordo totalmente que um desenvolvedor que entende bem a camada de dados e o que acontece por baixo é um desenvolvedor mais forte
Se você vai gastar tempo aprendendo as peculiaridades de um ORM, fica a pergunta clássica: por que não investir esse tempo em aprender SQL?
Os juros compostos da experiência com SQL têm grande chance de valer mais, daqui a 10 a 15 anos, do que o ORM de uma plataforma ou linguagem específica
Há motivos para aprender e usar ORM, mas não dá para pender para esse lado só porque você não quer aprender SQL
Assim você sabe o que está pagando por aquela abstração. Claro, se for uma “abstração sem custo”, aí é diferente
Mas hoje uso
cruddoazer, que acaba sendo classificado como ORM, por causa do marshaling/unmarshaling com tipos de GoA biblioteca padrão
sqldo Go é básica demais, então você praticamente precisa escrever por conta própria o marshaling/unmarshaling de objetos, o que é realmente tediosoSe eu usar ORM hoje em dia, provavelmente seria só para isso
Gosto de micro ORMs de “meio-termo”, como Dapper e Diesel
Você escreve as queries em SQL, mas ganha tipagem forte e mapeamento automático do conjunto de resultados para objetos
É como ter produtividade junto com uma linguagem de consulta “de verdade”, completa em recursos
Também passei a gostar da abordagem de usar o mapeador de objetos apenas para queries de leitura
Atualizações são feitas com um padrão de comando, isto é, stored procedures, e dá para mapear isso para parecer métodos nativos
Os desenvolvedores precisam parar de fingir que SQL não existe e simplesmente aceitar isso
Eloquent abstrai bem esse trabalho braçal e permite focar na lógica
Estou reescrevendo um app PHP “das antigas”, que só tinha arrays e queries, para uma estrutura com modelos e Eloquent, e a sensação é de mudança total de jogo
No passado, quando usava SQLite em .NET, também criei uma biblioteca wrapper nessa linha: https://github.com/zmj/sqlite-fast
Esta thread mostra bem o domínio de mindshare dos ORMs
Em quase todos os comentários, a alternativa implícita ao ORM é SQL bruto como string literal dentro do código da aplicação
Já existiam, seis anos atrás, ferramentas que usavam SQL como uma linguagem de fato e geravam wrappers que expunham as queries como métodos na aplicação
Coisas como queryfirst, pgtyped, pugsql e sqlc
Isso era uma mudança de paradigma, ou pelo menos poderia ter sido, e fica claro que é uma forma superior de trabalhar, mas ORM e essa discussão interminável sugam todo o oxigênio e isso continua na periferia
SQL é uma linguagem, e uma linguagem de altíssimo nível
Tentar concatenar SQL a partir de uma linguagem de nível mais baixo é um caso clássico de “quando você tem um martelo, tudo parece prego”
Por exemplo, se você está programando em assembly, que é de baixo nível, para interagir com um sistema grande, e a documentação desse sistema disser que as entradas só podem ser expressas em JavaScript, você tem uma escolha
Pode trazer alguma biblioteca obscura que permita montar tudo em assembly e assim evitar dar commit em arquivos
xxx.jsno repositório, ou pode simplesmente usar a força da linguagem de alto nível e expressar isso em um arquivo.jsVocê passa a ter uma única fonte da verdade para as queries SQL em uso, e como o que você realmente escreve e mantém são as próprias queries, fica fácil fazer otimizações como criar novos índices
Colocar uma abstração em cima de strings de SQL bruto é muito útil, mas fingir que relações são objetos é um jeito complicado e embolado de fazer isso
O TopLink provavelmente também
Era assim que ele tratava o mapeamento entre stored procedures e entidades locais