Por que criei meu próprio banco de dados?

Sei que soa maluco e, alguns meses atrás, eu nunca teria imaginado essa façanha nem aventado tal possibilidade.

Por que hein?

Desenvolvo software há alguns anos, e já utilizei diversas soluções de banco de dados em meus projetos, como:

  • MySQL
  • PostgreSQL
  • Redis
  • MongoDB
  • E muitos outros…

Portanto, criar meu próprio banco de dados nunca foi uma necessidade nos projetos que participei em todos estes anos, tendo sempre escolhido alguma solução pronta e específica para cada caso.

Até que comecei a desenvolver o Vemto, meu projeto mais recente. O Vemto é um gerador de código para o framework Laravel (um framework PHP), com o intuito de facilitar a criação inicial dessas aplicações, automatizando diversas coisas como: geração dos models, migrations, controllers, CRUD, sugestão de tipos de campos, montagem do Schema, etc.

Demonstração do Vemto

Comecei o planejamento do Vemto no final de 2019 e o desenvolvimento iniciou-se em Abril de 2020. É um software fortemente voltado ao processamento de dados relativamente complexos. Portanto, precisaria de um banco de dados (ao invés de algo mais simples como guardar um arquivo JSON ou YAML).

Além disso, tinha um outro fator muito importante:

Exemplo de Template do Vemto

Portanto, era necessário que fosse muito simples acessar dados relacionados, de forma parecida com a que os ORM fazem. Por exemplo, se eu quisesse obter todos os campos de uma tabela eu poderia fazer algo como:

table.fields.forEach(field => {}) //…

Escolha do banco…

Como o Vemto é uma aplicação Desktop desenvolvida com Javascript no Electron, descartei logo de início soluções que precisam de instalação como MySQL, MongoDB, etc. Precisava ser um banco de dados Built-in (integrado).

A princípio, considerei utilizar o SQLite, um banco de dados relacional bastante conhecido e que já tenho experiência. Mas durante o planejamento, percebi que precisaria de algo que oferecesse um Esquema mais flexível.

Isso me possibilitaria experimentar e pivotar a ferramenta rapidamente, ao mesmo tempo que me possibilitaria rapidamente alcançar a concorrência (requisito muito importante que utilizei em outras partes do software e pretendo contar em outro post).

Pesquisei muito no início, testei diversas soluções, e a escolha ficou com o IndexedDB, por ser um banco que já vem praticamente pronto para uso no Electron (por conta do Chromium utilizado internamente), é rápido e simples de utilizar, além de ter uma grande flexibilidade, por ser um banco orientado à documentos.

A dor de cabeça…

Apesar do IndexedDB ser um banco excelente, ele não foi a escolha correta para este projeto. E eu só comecei a parceber isso por volta do segundo mês de desenvolvimento, mas naquele momento, decidi continuar, já que tinha muita coisa pronta.

Eu escolhi esse banco pela sua flexibilidade na modelagem dos dados, mas eu errei ao desconsiderar um detalhe muito importante:

E isso pesa muito na escolha pois o IndexedDB tem duas características importantes: ele não é relacional, e ele é totalmente assíncrono.

Por isso, ao utilizá-lo visando um modelo relacional de dados, você precisa lidar manualmente com coisas como:

  • Async/Await Hell — Como todas operações são assíncronas, você é obrigado a fazer uma corrente de chamadas assíncronas. Por exemplo, se você clicar em um botão para deletar um projeto, ele deverá deletar as entidades relacionadas. Para isso, você precisa chamar um método assíncrono que usa um await para obter entidades relacionadas, e isso obriga todos os métodos antecedentes serem assíncronos (desde o método chamado no clique do botão até os métodos mais internos). Se quiser realizar uma operação em uma coleção de dados, e utilizar algum resultado dessa operação, não é possível utilizar métodos funcionais comuns como .forEach, .map, etc já que os callbacks precisarão ser obrigatóriamente assíncronos, te forçando à utilizar um laço for nesses casos. EDIT: fiquei sabendo que dá para usar Promise.all() aqui através de um comentário. De qualquer forma, isso ainda seria muito complicado para os templates, mas fica como correção.
  • Operações relacionais — Coisas simples em bancos relacionais como apagar um elemento pai, e automaticamente apagar os filhos precisam ser tratadas manualmente. Além disso, é muito fácil gerar problemas do tipo N+1 pela falta de indexação adequada.
  • Carregamento prévio de dados — os dados do Vemto são organizados de uma forma que possam ser utilizados diretamente na Template Engine, conforme explicado anteriormente. Como ficaria muito complicado trabalhar com estes dados de forma assíncrona na Template Engine, era necessário carregar todos os relacionamentos manualmente antes de enviá-los para a geração de código.

Resumindo, quando eu comecei a trabalhar nas partes mais complexas, como o Editor de Esquema e a Geração Inicial de Código, percebi que a escolha não fora correta.

Eu estava perdendo muito tempo, além de estar gerando inúmeros bugs e ficando maluco de estresse. É sério, eu estava ficando maluco e perdendo o sono com as “gambiarras” que começaram à surgir, prejudicando inclusive a testabilidade do projeto.

Procurando uma solução…

Apesar de precisar de um banco relacional, bancos não relacionais orientados a documentos possuem algumas características das quais eu não poderia abrir mão como a flexibilidade do esquema de dados.

De acordo com tudo o que percebi até aquele momento, eu precisava de um banco com as seguintes características (todas importantes):

  • Suportar relacionamentos — isso inclui coisas como índices, CASCADE DELETE, constraints, foreign keys, etc
  • Ao mesmo tempo, todo o restante do esquema que não estivesse relacionado com outras entidades precisaria ser totalmente flexível. Eu deveria poder adicionar novos campos ao esquema à qualquer momento, assim como em bancos orientados à documentos (MongoDB, IndexedDB, etc)
  • Deveria me permitir escolher quando realizar operações síncronas ou assíncronas (as operações assíncronas também são importantes, já que no Electron, operações síncronas bloqueiam o processo principal)
  • Deveria ter um ORM para facilitar o uso, sem a necessidade do uso de queries (principalmente por conta do uso na Template Engine)
  • Deveria ser Built-in e compatível com Electron, não necessitando de instalação, podendo ser compilado juntamente com a aplicação (assim como o SQLite)

Ou seja, eu precisava daquilo que conhecemos por Banco de Dados Híbrido, uma categoria pouco conhecida de bancos de dados, já que o uso em aplicações comuns é bastante raro.

Mais especificamente, eu precisava de um banco object–relational database (ORD) para Electron (algo que eu não sabia até recentemente).

Procurei semanas por um banco de dados com tais características. Encontrei pouquíssima coisa, e meu último requisito, de ser Built-in e compatível com Electron era o maior empecilho para encontrar algo (realmente acredito que não existia um banco com todos estes requisitos, se alguém souber me avisa aí nos comentários por gentileza).

Neste ponto, eu estava me sentindo bastante pressionado, pois já havia uma lista de milhares de pessoas cadastradas na Landing Page aguardando para testar o Vemto, os meses estavam passando e meu cronograma estava muito atrasado, com a Pré-Alpha adiada para Setembro de 2020 (Inicialmente seria em Agosto).

Em meados de Junho de 2020, eu tive uma ideia repentina em um breve momento de descanso.

A ideia parecia boa, e naquele momento eu consegui visualizá-la “quase que completamente” na minha cabeça. Fiz algumas anotações e criei um protótipo, mas após focar nele por algum tempo, achei que daria muito trabalho e atrasaria ainda mais.

Resolvi fazer um esforço enorme para terminar o MVP com o IndexedDB mesmo, pelo menos para lançar a versão Pré-Alpha. Foi muito difícil e estressante, precisei maquiar a maioria dos bugs com “muuuita gambiarra”, mas consegui lançar essa versão, o que acabou atraindo mais gente e me deixando ainda mais desesperado por uma solução para o problema.

Eu em Setembro…

Desenvolvendo o Banco…

Eu não podia deixar o software daquela maneira.

Seria impossível manter o código e bater os concorrentes em tempo hábil com tantas dificuldades, gambiarras e com a testabilidade prejudicada.

Então, em Setembro eu tomei a decisão. Iria terminar de implementar a ideia de banco de dados que tive em Junho e reescrever todas as partes do sistema que utilizavam IndexedDB para utilizarem este novo banco (nome provisório que tornou-se definitivo por preguiça: RelaDB).

Por incrível que pareça, terminar o desenvolvimento do banco de dados foi uma tarefa até rápida, já que eu tinha um protótipo parcialmente funcional.

Levei em torno de duas semanas para terminar a implementação inicial. Todo o desenvolvimento do banco de dados foi feito através de TDD (Desenvolvimento Orientado à Testes), e eu tinha uma boa ideia de como os mecanismos deveriam funcionar.

Exemplo de alguns testes de funcionamento do banco de dados

O banco foi criado com uma camada intrínseca de ORM (fortemente inspirado no Eloquent ORM do Laravel), facilitando assim o uso.

O ORM atualmente suporta relacionamentos 1:N e 1:1, sendo que relacionamentos N:N não foram necessários até o momento então deixei para implementar futuramente (apesar que o banco internamente os suporta, já que só é necessário a criação de uma tabela Pivot).

Ele não suporta linguagens de consulta conhecidas como o SQL, pois resolvi implementar minha própria solução simples no baixo nível, já que eu tinha requisitos bem definidos, e decidi que a principal interface de acesso ao banco seria o próprio ORM, através de classes de Model.

Exemplo de um Model

Como é um tipo de banco de dados que pode utilizar outros bancos do tipo chave/valor para funcionar, eu desenvolvi primeiramente simulando o LocalStorage do navegador com arrays na memória.

Ele possui o conceito de “Drivers”, então, para adaptá-lo à um novo banco de chave/valor, basta criar um novo Driver que conta com poucas linhas de código (isso possibilita utilizá-lo com LocalStorage, Redis, ou seu próprio driver nativo).

Com isso em mente, finalizei criando um pequeno banco chave-valor inspirado no ElectronJsonStorage que salva os dados no disco, e depois criei um Driver nativo simples utilizando essa implementação.

Driver que permite salvar arquivos JSON no disco

No pequeno vídeo abaixo você pode conferir um exemplo de uso do RelaDB em uma aplicação Electron:

Trocando o banco…

Essa parte sim eu diria que foi muito difícil!

Comecei a refatoração de código em 28 de Setembro de 2020:

Dia que comecei à implementar o banco no Vemto

Levei em torno de três semanas. Foram dias muito, muito intensos.

Provavelmente uma das tarefas mais difíceis da minha vida profissional, já que eu queria fazer o lançamento até Novembro de 2020, então foi uma pressão enorme.

Tive que revisar praticamente todo o código da aplicação, muita coisa foi reescrita e muita coisa foi jogada fora (principalmente as tarefas manuais que citei anteriormente, já que agora o banco de dados cuidava de tudo automaticamente). Também fui descobrindo diversos pequenos bugs no RelaDB nesse ínterim que foram sendo resolvidos em paralelo.

E então, em 20 de Outubro de 2020, eu finalmente fiz esse merge monstruoso (29.815 inserções e 8.352 deleções apenas de refatoração).

Minha jornada desenvolvendo meu próprio banco de dados parecia estar próxima do fim:

E por fim, no dia 29 de Outubro, muito mais tranquilo e feliz pelo resultado de todo o trabalho, eu finalmente lancei o Vemto oficialmente:

Problemas após o lançamento…

Tive apenas dois problemas com o banco de dados após o lançamento:

  • Problema na ordenação de números — ao realizar uma query ordenando por um campo numérico, por exemplo: Project.orderBy(‘position’).get(), o sistema estava ordenando alfabeticamente. Foi um bug bem rápido de resolver.
  • Problemas de performance — Esse foi bem complicado, só consegui terminar de resolver em 15 de Novembro de 2020, mais de duas semanas após o lançamento. O problema acontecia quando haviam muitas entidades (modelos e tabelas) em um projeto. Apesar de ser um problema complicado, apenas uma pessoa reclamou, pois estava criando um projeto muito grande com o Vemto. Precisei fazer diversas melhorias na forma que o RelaDB implementa as queries e também criei uma camada de Buffer/Cache. Assim o banco de dados realiza todas as operações mais pesadas diretamente na camada de Cache, que é extremamente rápida por estar na memória RAM, e depois despacha essas operações como transações assíncronas no Background, por meio de um processo separado.

Após resolver estes dois, não tive mais problemas com o banco de dados.

Conclusão

Desenvolver meu próprio banco de dados foi com certeza a melhor decisão que tomei no contexto deste projeto. Eu nunca faria isso se estivesse desenvolvendo uma aplicação comum, mas nesse caso, foi um divisor de águas.

É tão simples adicionar novos dados e refletí-los diretamente na Template Engine, que percebi que o conjunto dessas duas partes do sistema é a razão para conseguir lançar novas features rapidamente (em poucos meses, o Vemto não só consegue resolver a maioria dos problemas que os concorrentes resolvem, como traz algumas “coisinhas a mais”).

Por este motivo, eu resolvi manter o Banco de Dados como Closed-Source por um tempo. Mas, no futuro, com ele melhorado e devidamente documentado (minha documentação hoje são os testes), tenho a pretensão de deixá-lo como Open-Source.

Agradeço muito por ter me acompanhado até aqui, eu queria escrever muito mais detalhes mas o texto está ficando muito longo.

Estou sempre postando minhas aventuras de código lá no Twitter. Se quiser ficar por dentro, é só me acompanhar: https://twitter.com/Tiago_Ferat

Grande abraço e até a próxima!!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store