Escrevendo consultas SQL dinâmicas com Go
Você precisa escrever consultas SQL nativas que recebem um número variável de parâmetros de busca e quer evitar o inferno que é ficar concatenando strings. Aprenda a fazer isto com a lib SQL Builder for Go!
Você precisa escrever consultas SQL nativas que recebem um número variável de parâmetros de busca e quer evitar o inferno que é ficar concatenando strings.
Ingredientes
- Go 1.23, mas você pode usar versões anteriores também.
- SQL Builder for Go (https://github.com/huandu/go-sqlbuilder)
O problema
Há situações nas quais você precisa escrever consultas dinâmicas usando SQL nativo, o que pode ser uma codificação bastante complicada e propensa a erros quando as implementamos usando concatenação de strings. Para ilustrar a situação, vamos a um caso real: a busca de posts implementada no projeto /dev/All.
Observe o nosso modelo de dados:
Todo post pertence a um site. Queremos implementar uma busca que pode levar em consideração ao menos três parâmetros:
- Título do post
- Resumo do post
- Nome do site em que o post foi publicado
Com base nesta pequena variação, por análise combinatória, temos ao menos oito variações de SQL que podem ser escritas dependendo do fato do parâmetro de busca ter sido ou não preenchido:
Isto sem mencionar que você precisará lidar também com a ordem em que os parâmetros serão inseridos na sua consulta quando esta for realizada. Trabalhão hein? E bem fácil de se cometer alguns erros. Bora resolver?
Passo a passo
Para esta receita vamos usar a biblioteca SQL Query Builder. Há outras alternativas, mas usaremos esta por estar sendo atualizada com bastante frequência. O objetivo desta receita não é que você se aprofunde na ferramenta, mas sim saber que ela existe para que na sequência possa buscar mais a respeito, ok?
Instale a lib no seu projeto Go e a importe
Inclua uma instrução import padrão e na sequência fazer o download da mesma.
import sqlbuilder "github.com/huandu/go-sqlbuilder"
// é bom você usar um alias para a importação
// pra evitar conflito com o pacote database/sql
go get github.com/huandu/go-sqlbuilder
Passo 2 - uma consulta bem simples
Acho importante você ter um primeiro gostinho da lib pra poder começar. Então vamos usar apenas a tabela de sites. Queremos uma consulta que nos retorne um site caso as seguintes condições sejam satisfeitas:
- O site deve estar ativo
- O site não pode ter sido bloqueado
- E possua o id que iremos passar como parâmetro
Bora pro código final pra começar e na sequência dissecar? Lá vai.
func GetSite(id int) (*Site, *PostError) {
query := sqlbuilder.Select("id, nome, endereco, rss, sobre")
.From("site")
query.Where(
query.Equal("id", id),
query.Equal("bloqueado", false),
query.Equal("ativo", true))
sqlString, parametros := query.Build()
var site Site
var tx, errTx = data.BeginTransaction()
if errTx != nil {
return nil, &PostError{Code: "500", Message: errTx.Error()}
}
errScan := tx.QueryRow(sqlString, parametros...).Scan(&site.Id, &site.Name, &site.Url, &site.Rss, &site.About)
if errScan != nil {
tx.Rollback()
if errScan == sql.ErrNoRows {
return nil, &PostError{Code: "404", Message: "Site not found"}
}
return nil, &PostError{Code: "500", Message: errScan.Error()}
}
tx.Commit()
return &site, nil
}
Vamos para as primeiras linhas que são o que realmente interessa pra começar?
query := sqlbuilder.Select("id, nome, endereco, rss, sobre")
.From("site")
Lembra que o nome é "Query Builder"? É por que aqui aplicamos o padrão de projeto Builder. Observe que o alias que passamos para o import
aqui se paga: evitamos um conflito com o pacote database/sql
.
A primeira chamada diz quais os campos que precisamos em nossa consulta. A segunda nos diz o nome da tabela. Tudo isto é armazenado na struct query
que contém a estrutura do SQL que precisamos criar.
Finalmente, temos a passagem dos parâmetros. Bora ver?
query.Where(
query.Equal("id", id),
query.Equal("bloqueado", false),
query.Equal("ativo", true))
Observe que usamos a função Where
do builder e na sequÊncia passamos nossos parâmetros. Hmm... quando termina o uso da função? Aqui:
sqlString, parametros := query.Build()
A função Build
nos retorna uma tupla: o primeiro componente é uma string contendo o SQL que será enviado para o banco de dados (já com todos os parâmetros definidos), o segundo é um slice contendo todos os parâmetros que passamos: o id, false e true.
Daí pra frente é só usar o pacote database/sql
que você já deve dominar:
var site Site
var tx, errTx = data.BeginTransaction()
if errTx != nil {
return nil, &PostError{Code: "500", Message: errTx.Error()}
}
// olha aqui o sqlbuilder na prática
errScan := tx.QueryRow(sqlString, parametros...).Scan(&site.Id, &site.Name, &site.Url, &site.Rss, &site.About)
if errScan != nil {
tx.Rollback()
if errScan == sql.ErrNoRows {
return nil, &PostError{Code: "404", Message: "Site not found"}
}
return nil, &PostError{Code: "500", Message: errScan.Error()}
}
tx.Commit()
return &site, nil
Passo 3 - Vamos pra consulta complicada que mencionei no início?
Agora, como ficaria a consulta que pode ter oito variações? E ainda mais: como podemos montar nossa consulta usando joins
? Assim:
query := sqlbuilder.Select("blog_post.id, blog_post.titulo, blog_post.resumo, blog_post.url, blog_post.data_inclusao, blog_post.data_publicacao, site.id, site.nome, site.endereco, site.rss")
// no from inserimos o join
.From("blog_post inner join site on blog_post.site_id = site.id")
// as três condições
if titulo != "" {
query.Where(
query.Like("blog_post.titulo", "%"+titulo+"%")
)
}
if resumo != "" {
query.Where(
query.Like("blog_post.resumo", "%"+titulo+"%")
)
}
if site != "" {
query.Where(
query.Like("site.nome", "%"+site+"%")
)
}
query.Where(query.Equal("site.bloqueado", false),
query.Equal("site.ativo", true))
numPosts := 20
// dá pra paginar também!
query.Limit(numPosts).Offset((page - 1) * numPosts)
query.OrderBy("data_publicacao desc")
sqlString, arguments := query.Build()
Observe:
- O
join
é definido no where - Os parâmetros são inseridos dinamicamente de acordo com seu preenchimento ou não
- E ao final ainda colocamos a paginação
Se quiser tornar melhorar a receita...
Acesse o site oficial do SQL Builder for Go: https://github.com/huandu/go-sqlbuilder
É importante lembrar que esta não é a única opção para consultas dinâmicas. Há alternativas como sqlc e outros query builders que você pode usar. O importante é saber que eles existem.