Writing complex parameterized queries
A parameterized query is a query that is built dynamically based on incoming request params. Building complex database queries can be challenging but you can achieve better results by following the recommendations presented in this article.
Divide and conquer
The first and the main recommendation is to split the whole process into isolated steps:
Parsing request params
The first think you need to do is to create a data structure that will hold incoming params, for example:
type ArticleFilter struct {
CategoryID int64
Search string
Page int
}
And a factory method that will parse the params from an http.Request
or JSON payload:
func articleFilterFromRequest(req *http.Request) (*ArticleFilter, error) {
query := req.URL.Query()
f := new(ArticleFilter)
f.Search = query.Get("search")
categoryID, err := strconv.ParseInt(query.Get("category_id"), 10, 64)
if err != nil {
return nil, err
}
f.CategoryID = categoryID
page, err := strconv.Atoi(query.Get("page"))
if err != nil {
return nil, err
}
f.Page = page
return f, nil
}
Params validation
The purpose of this step is to ensure you have enough data to build a query or to set default values:
func (f *ArticleFilter) Validate() error {
if f.CategoryID == 0 {
return errors.New("category id is required")
}
if f.Page == 0 {
f.Page = 1
} else f.Page > 1000 {
return errors.New("you can't paginate past page #1000")
}
return nil
}
Query generation
At this step you have enough data to build a query using Bun API. It is best to keep all query generation logic in a single method so it can be easily followed.
func articleFilterQuery(q *bun.SelectQuery, f *ArticleFilter) (*bun.SelectQuery, error) {
q = q.Where("category_id = ?", f.CategoryID).
Limit(10).
Offset(10 * (f.Page - 1))
if f.Search != "" {
q = q.Where("title LIKE ?", "%"+f.Search+"%")
}
return q, nil
}
Query execution
Lastly, you need to execute the generated query and, optionally, do some post-processing. The end result may look like this:
func handler(w http.ResponseWriter, req *http.Request) {
f, err := articleFilterFromRequest(req)
if err != nil {
panic(err)
}
if err := f.Validate(); err != nil {
panic(err)
}
var articles []Article
q, err := articleFilterQuery(db.NewSelect().Model(&articles), f)
if err != nil {
panic(err)
}
if err := q.Scan(req.Context()); err != nil {
panic(err)
}
if err := json.NewEncoder(w).Encode(map[string]interface{}{
"articles": articles,
}); err != nil {
panic(err)
}
}