Qualquer aplicativo que trabalhe com consultas SQL pode se beneficiar do uso de um construtor de consultas para melhorar a legibilidade, a manutenção e a segurança do código. Na verdade, existem muitas bibliotecas diferentes que fazem exatamente isso em Golang. Aqui na Vaunt, tentamos muitas opções diferentes antes de finalmente decidirmos criar uma. Em última análise, queríamos algo que fosse seguro e fornecesse substituição de variáveis para evitar injeção de SQL e ao mesmo tempo ser legível e capaz de ter instruções condicionais. Então criamos uma nova biblioteca chamada tqla, lançada e anunciada no final do ano passado. Você pode ler mais sobre isso neste artigo.
Antes de construirmos o tqla, usávamos principalmente o Squirrel para nossa lógica de construção de consulta SQL - e é altamente recomendável. Ainda usamos o Squirrel em algumas áreas, mas gradualmente começamos a substituir e implementar uma nova lógica de construção de consultas com tqla. Encontramos muitos casos em que o tqla melhorou nossa capacidade de manter nosso código e corrigir problemas que encontramos ao usar outros construtores de instruções.
Na Vaunt, recentemente passamos por uma migração de banco de dados do CockroachDB para o TiDB. Embora o CockroachDB fosse de alto desempenho e confiável, finalmente enfrentamos a decisão de adicionar ao nosso techstack para oferecer suporte a um banco de dados OLAP. A necessidade disso era apoiar nossa carga de trabalho analítica em nosso produto de insights da comunidade de código aberto. Para manter nossa pegada tecnológica pequena, decidimos avançar com o TiDB e aproveitar as vantagens da arquitetura HTAP do banco de dados.
CockroachDB é amplamente compatível com PostgreSQL e usamos a sintaxe PostgreSQL para muitas de nossas consultas SQL. Para mudar para o TiDB, tivemos que alterar algumas de nossas tabelas e atualizar consultas para usar a sintaxe do MySQL. Em alguns locais durante a migração, descobrimos que estávamos usando indevidamente instruções de construção de consulta condicional e não tínhamos os testes adequados para detectar que as instruções estavam sendo geradas incorretamente.
No README do Squirrel, há um exemplo de como você pode usar a construção de consulta condicional para atualizar instruções com filtros opcionais:
if len(q) > 0 { users = users.Where("name LIKE ?", fmt.Sprint("%", q, "%")) }
Aqui está um exemplo real, mas simplificado, de como atualizamos uma de nossas consultas para unir tabelas condicionalmente e adicionar um filtro opcional:
psql := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Question) statementBuilder := psql.Select(`i.id`). From("vaunt.installations i"). Where(`entity_name = ?`, name) if len(provider) > 0 { statementBuilder.Where(`provider = ?`, provider) } if len(repo) > 0 { statementBuilder.Join(`repositories as r on JSON_CONTAINS(i.repositories, CONCAT('["', r.id, '"]'))`) statementBuilder.Where(`r.name = ?`, repo) }
Você consegue identificar o problema com o código? Caso contrário, não se preocupe – é algo que também passou despercebido em nossas próprias revisões de código até executarmos nossos testes.
O problema aqui é que esquecemos de atualizar o construtor de instruções com o resultado das funções do construtor. Por exemplo, o filtro de condição do provedor deve ser:
if len(provider) > 0 { statementBuilder = statementBuilder.Where(`provider = ?`, provider) }
Este é um erro relativamente simples de cometer e pode ser facilmente detectado com casos de teste suficientes, mas como não é um código tecnicamente inválido, pode demorar um pouco para perceber o que está acontecendo imediatamente.
Outro problema de legibilidade com esta configuração é que a junção condicional é separada da instrução select inicial. Poderíamos reorganizar o construtor para colocar cada peça onde deveria ir, mas isso exigiria várias verificações de instruções condicionais duplicadas e ainda sofreria de alguns problemas de legibilidade.
A demonstração acima usando Squirrel foi reescrita e o equivalente em tqla se parece com isto:
t, err := tqla.New(tqla.WithPlaceHolder(tqla.Question)) if err != nil { return nil, err } query, args, err := t.Compile(` SELECT i.id FROM vaunt.installations as i {{ if .Repo }} JOIN vaunt.repositories as r on JSON_CONTAINS(i.repositories, CONCAT('["', r.id, '"]'), '$') {{ end }} WHERE entity_name = {{ .Name}} {{ if .Provider }} AND i.provider = {{ .Provider }} {{ end }} {{ if .Repo }} AND r.name = {{ .Repo }} {{ end }} `, data) if err != nil { return nil, err }
Como você pode ver, a sintaxe do modelo para tqla torna a incorporação de cláusulas condicionais muito simples. Tqla substitui automaticamente as variáveis que estamos definindo pelos nossos espaços reservados especificados e fornece os argumentos que podemos usar com nosso driver sql para executar a instrução.
Semelhante ao Squirrel, essa abordagem de construção de instruções é fácil de testar, pois podemos criar diferentes conjuntos de objetos de dados para passar ao construtor de modelo e validar a saída.
Você pode ver que podemos facilmente adicionar partes condicionais da consulta onde elas se encaixariam melhor. Por exemplo, aqui temos um JOIN condicional diretamente após a instrução FROM – e embora ainda tenhamos múltiplas verificações de condição, isso não complica excessivamente o modelo.
Outro recurso interessante do tqla que ajuda a melhorar a capacidade de manutenção de nossos construtores sql é a capacidade de definir funções personalizadas que poderíamos usar nos modelos para abstrair alguma lógica de transformação.
Aqui está um exemplo de como usamos uma função para converter o valor time.Time do Golang em um sql.NullTime para nos permitir fazer uma inserção com nossos objetos de dados sem precisar convertê-los antecipadamente:
funcs := template.FuncMap{ "time": func(t time.Time) sql.NullTime { if t.IsZero() { return sql.NullTime{Valid: false} } return sql.NullTime{Time: t, Valid: true} }, } t, err := tqla.New(tqla.WithPlaceHolder(tqla.Question), tqla.WithFuncMap(funcs)) if err != nil { return err }
Com esta função definida em nosso mapa de funções tqla, agora podemos usá-la livremente em nossos modelos de consulta, fornecendo a ela um parâmetro do objeto de dados que é um campo time.Time. Podemos até chamar essa função várias vezes no mesmo modelo com campos diferentes.
Aqui está um exemplo simplificado:
statement, args, err := t.Compile(` INSERT INTO events (name, created_at, merged_at, closed_at) VALUES ( {{ .Name }}, {{ time .CreatedAt }}, {{ time .MergedAt }}, {{ time .ClosedAt }} )`, eventData)
Concluindo, acreditamos que o uso de tqla pode ajudar a melhorar a capacidade de manutenção da lógica de construção de consultas, ao mesmo tempo que oferece algum utilitário poderoso para a criação de consultas dinâmicas. A simplicidade da estrutura do modelo permite uma legibilidade limpa do código e pode tornar mais rápida a depuração de possíveis erros.
Criamos tqla de código aberto para compartilhar esta biblioteca na esperança de que ela forneça uma boa opção para outros usuários que desejam uma maneira simples, sustentável e segura de criar consultas SQL em muitos tipos diferentes de aplicativos.
Se você estiver interessado, confira o repositório e dê uma estrela se isso te ajudar de alguma forma. Sinta-se à vontade para fazer solicitações de recursos ou relatórios de bugs!
Estamos sempre abertos a receber feedback e contribuições.
Para ficar por dentro do desenvolvimento futuro, siga-nos no X ou junte-se ao nosso Discord!
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3