Go + GORM
O melhor ORM de Go — Artigo 6
Postman collection, código do artigo, diff com o artigo 4.
Sumário: Desenvolvimento de APIs em Go
Próximo Artigo: Go + Injeção de Dependência — Árvore de Dependências, Inversão de Controle e Raiz de Composição
Artigo Anterior: Go + MySQL — Docker-compose em ação
Introdução
Agora que possuímos uma API que consegue se comunicar com um banco de dados, podemos começar a lidar com os processos de escrita e leitura. Existem muitas formas de realizar essas operações com Go, seja com SQL puro ou com ORMs (Object-Relational Mapping). Nesse artigo será abordado a utilização do GORM, que é um ORM para Go.
ORM é uma ferramenta que mapeia objetos de um modelo de domínio para tabelas em um banco de dados relacional, permitindo que você trabalhe com o banco de dados usando objetos e métodos familiares em vez de escrever consultas SQL diretamente.
O GORM simplifica a interação com bancos de dados em aplicativos Go, fornecendo uma camada de abstração que permite criar, ler, atualizar e excluir registros do banco de dados usando uma sintaxe simples e intuitiva. Ele suporta vários bancos de dados, como MySQL, PostgreSQL, SQLite e SQL Server.
Com o GORM, você pode definir modelos de dados como structs em Go e mapeá-los para tabelas no banco de dados. Ele fornece recursos como migrações de banco de dados, consultas complexas, relacionamentos entre tabelas, pré-carregamento de dados relacionados e muito mais.
Como sempre, não existe bala de prata, então embora o GORM seja geralmente considerado rápido o suficiente para a maioria dos casos de uso, ele pode não ser a opção mais eficiente para operações de alto desempenho e cargas extremas. Em alguns casos, escrever consultas SQL personalizadas pode ser mais eficiente em termos de desempenho.
Utilizando o GORM
O primeiro passo necessário para começar a utilizar o GORM em nossa API é utilizar o método de conexão com o banco de dados fornecido pela biblioteca. Nesse método, passamos os valores carregados dos arquivos de configuração, conforme mostrado no artigo anterior.
Além dos valores carregados dos arquivos YAML, na própria string de conexão, definimos algumas configurações adicionais. Primeiramente, definimos (charset=utf8mb4) para garantir a conformidade total com o padrão UTF8. Em seguida, definimos (parseTime=true) para permitir que a implementação do GORM faça o parse do objeto (time.Time) do Go para o formato de data e hora utilizado pelo MySQL. Por fim, definimos (loc=Local) para ser utilizado o fuso horário da região do banco de dados no tratamento das datas quando necessário.
A entidade de domínio que será armazenada e recuperada do MySQL é o (product), conforme definido anteriormente. É importante observar que não é necessário adicionar nenhuma anotação específica do GORM nessa entidade, pois a forma como criamos a tabela no episódio anterior segue os padrões da biblioteca.
Note que embora a entidade esteja livre de “detalhes técnicos” do banco de dados, ainda sim existem detalhes do protocolo utilizado da API. Vamos resolver isso mais a frente, quando falarmos de Arquitetura em Camadas.
Create
Como a tabela no MySQL foi criada seguindo os padrões do GORM, conseguimos realizar a operação de criação de dados em apenas 4 linhas de código. Utilizamos a própria entidade de domínio para adicionar os dados no banco de dados.
Note que a operação de escrita acima não utiliza o artifício de transactions, pois se trata de uma entidade simples e não um agregado. No futuro, quando a lógica de negócio da nossa API se tornar mais complexa, poderemos adicionar um objeto de valor associado à entidade (product) para lidar com transações de um agregado.
GetByID
A operação de leitura também é simples e requer poucas linhas de código. No entanto, neste caso, ocupa mais linhas do que o necessário, pois adicionei um tratamento de erros para seguir um padrão de Result da programação funcional. Nesse padrão, se houver um erro, não haverá um objeto de resposta além do erro. Por outro lado, se não houver erro, será retornado um objeto de resultado.
Search
A operação de busca (search) é um pouco mais complexa nesse cenário. Para evitar que o código se torne idêntico ao do método GetByID, já o deixei em um padrão que nos permitirá construir consultas personalizadas ao banco de dados no futuro. Basicamente, criamos um array de strings chamado (query) e um array de interface{} chamado (args). Acrescentamos o filtro de tipo (type) a esses arrays e passamos esses valores para o método Where do GORM que realizará a ligação entre esses arrays. Isso nos permitirá realizar consultas mais avançadas no banco de dados.
Note que também aplicamos um limit em nossa query. Isso se deve ao fato de que, ao trabalharmos com um banco de dados, não podemos permitir que consultas sejam executadas sem algum tipo de tratamento. Esse limite é o início de uma funcionalidade de paginação que será implementada no futuro.
Hoje nosso search funciona somente por type, mas no futuro poderemos adicionar mais objetos e mais formas de filtro, possibilitado queries como:
SELECT id, name, type, quantity FROM products WHERE type='clothing' AND quantity > 0;
Update
Nosso update poderia ser praticamente igual ao create, utilizando apenas o método Update do GORM. No entanto, como o software é desenvolvido para resolver problemas do mundo real, nem sempre é uma boa ideia atualizar todos os dados no banco de dados, o que aconteceria com o método Update do GORM. Às vezes, é necessário adicionar campos que mapeiam um estado na entidade, como uma data em que algo ocorreu ou um booleano para indicar algum cenário em que aquela entidade se encontra. Por isso, optei por utilizar o método Updates, pois nele posso definir exatamente o que desejo atualizar na entidade. Isso será útil ao longo da série.
Delete
Já o nosso delete é tão simples quanto poderia ser. Basicamente, é um método de apenas 4 linhas que utiliza a lib diretamente, sem nenhum outro tipo de tratamento adicional.
Conclusões e Próximas Etapas
Neste artigo, explicamos o funcionamento dos métodos de CRUD do GORM, além de detalhar aspectos importantes, como o tratamento de erros, search computado em tempo de execução e atualização parcial de entidades. Esses detalhes adicionais fornecem um valor adicional à compreensão do uso efetivo do GORM em nossa API.
O GORM é uma biblioteca muito útil e confiável para aplicações em todos os níveis. Já trabalhei em APIs que lidam com um alto throughput e que utilizam essa biblioteca com sucesso. No entanto, gostaria de ressaltar um ponto importante: o GORM faz uso extensivo de reflections no Go, o que pode adicionar um certo peso computacional devido as conversões entre tipos. No entanto, esse custo adicional geralmente não é perceptível, a menos que você esteja lidando com grandes volumes de dados em cada requisição, como retornar uma coleção muito grande do banco de dados ou inserir uma coleção muito grande. Para a maioria dos casos, o uso do GORM não deve causar problemas de desempenho.
No próximo artigo, iremos abordar o princípio da Injeção de Dependência, que foi aplicado no artigo anterior. Este próximo artigo será importante para aprofundarmos nossa compreensão sobre como certos princípios de desenvolvimento de software podem ser aplicados em Go.