Advanced Querying

Joins

Joins may be not be considered advanced, but it seemed like a better fit here because we'll be creating another model to demonstrate the use of joins.

Joins are a way of combining two data sets together given a common key. For example in our users table we have an ID (that's automatically generated by pop) that can be used as a foreign key in another table. I don't want to cover the ins and outs of relational databases so here's a link to a great Wikipedia article on relational databases.

First, let's create a new model using soda and migrate it to create the table in the database

$ soda generate model favorite_food user:uuid food:text -e development
v3.41.1

--> models/favorite_food.go
--> models/favorite_food_test.go
--> goimports -w main.go models/favorite_food.go models/favorite_food_test.go models/user.go models/user_test.go
> migrations/20180110140002_create_favorite_foods.up.fizz
> migrations/20180110140002_create_favorite_foods.down.fizz
$ soda migrate up -e development
v3.41.1

> create_favorite_foods

0.0076 seconds
dumped schema for ./development.sqlite

Now we'll write a program to pull the ID for the user, and populate the table with their favorite food

package main

import (
    "bitbucket.org/pop-book/models"
    "github.com/gobuffalo/pop"
    "log"
)

func main() {
    tx, err := pop.Connect("development")
    if err != nil {
        log.Panic(err)
    }

    foods := [3]string{"cake", "steak", "beer"}
    users := []models.User{}
    err = tx.All(&users)
    for i := 0; i < len(users); i++ {
        user := users[i]
        favoriteFood := models.FavoriteFood{User: user.ID, Food: foods[i]}
        _, err = tx.ValidateAndSave(&favoriteFood)
        if err != nil {
            log.Panic(err)
        }
    }
}

Verifying the data is there and good:

sqlite> SELECT * FROM favorite_foods;
113291c8-b4f4-4e8e-ba8a-821a6299c4d9|bf3ba75b-8dfe-4619-b832-31c4a087a589|cake|2018-01-10 09:21:21.380048-05:00|2018-01-10 09:21:21.380049-05:00
6bc22cfb-2c84-4175-a03b-d4396576ccf3|61b2db41-a66b-4093-8580-be0b114ec04e|steak|2018-01-10 09:21:21.381272-05:00|2018-01-10 09:21:21.381282-05:00
4ecff328-3c5d-4e8a-974f-9852d9626b91|0ce8fedf-f875-4240-b8c3-cf6291cd7474|beer|2018-01-10 09:21:21.381904-05:00|2018-01-10 09:21:21.381905-05:00
sqlite> SELECT * FROM users;
bf3ba75b-8dfe-4619-b832-31c4a087a589|Mrs.|Jessica|Jones|Private security, super hero.|2017-12-19 14:06:20.473953-05:00|2017-12-28 21:57:45.771782-05:00|NYC, NY|1900
61b2db41-a66b-4093-8580-be0b114ec04e|Mr.|Luke|Cage|Hero for hire.|2017-12-19 14:06:20.476127-05:00|2017-12-28 21:50:00.096033-05:00|NYC, NY|1900
0ce8fedf-f875-4240-b8c3-cf6291cd7474|Mr.|Frank|Castle|USMC, badass.|2018-01-09 16:25:33.702813-05:00|2018-01-09 16:25:33.702816-05:00|Hoboken, NJ|1900

Now, let's write a program to join the two tables together, so that we can print a list of the full name and their favorite food:

package main

import (
    "bitbucket.org/pop-book/models"
    "github.com/gobuffalo/pop"
    "log"
    "fmt"
)

func main() {
    tx, err := pop.Connect("development")
    if err != nil {
        log.Panic(err)
    }

    type Favorite struct {
        FirstName string `json:"first_name" db:"first_name"`
        LastName  string `json:"last_name" db:"last_name"`
        Food      string `json:"food" db:"food"`
    }

    favorites := []Favorite{}
    all_foods := tx.Where("favorite_foods.food IS NOT NULL")
    query := all_foods.LeftJoin("users", "favorite_foods.user=users.id")

    sql, args := query.ToSQL(&pop.Model{Value: models.FavoriteFood{}}, "favorite_foods.food",
        "users.first_name", "users.last_name")
    err = all_foods.RawQuery(sql, args...).All(&favorites)
    for i := 0; i < len(favorites); i++ {
        fmt.Printf("%s %s really loves %s\n", favorites[i].FirstName, favorites[i].LastName, favorites[i].Food)
    }
}

Compile and run:

$ ./main
Jessica Jones really loves cake
Luke Cage really loves steak
Frank Castle really loves beer

Let's have a bit of an explanation of what went on there. First, we created a struct in line to represent the joined data model. This is because the query needs a model to put the data into. Now, we could generate a model the soda way, but for this small use case that would be unnecessary since it would also create another database table and migration set.

Next, we set a Where clause, primarily to ensure that we're not getting empty data and so that we can get back a query object. Next, we run a left join between the users table and the favorite_foods table. Then we force that query to a SQL string, then return all the matches in All. The data set returned is in the format of FirstName + " " LastName + " " really loves " + Food.

Raw Queries

Sometimes it's just faster or more convenient to write a SQL statement than it is to use query building parts of any ORM. pop gives you this ability as well. Suppose you want to just want to write a quick and dirty lookup:

package main

import (
    "bitbucket.org/pop-book/models"
    "github.com/gobuffalo/pop"
    "log"
    "fmt"
)

func main() {
    tx, err := pop.Connect("development")
    if err != nil {
        log.Panic(err)
    }
    users := []models.User{}
    err = tx.RawQuery("SELECT * FROM users WHERE title=?", "Mr.").All(&users)
    if err != nil {
        log.Panic(err)
    } else {
        for i := 0; i < len(users); i++ {
            fmt.Printf("%s %s %s\n", users[i].Title, users[i].FirstName, users[i].LastName)
        }
    }
}

Compile and run:

./main
Mr. Luke Cage
Mr. Frank Castle

This is basically building out the same thing as what is happening under the hood with the query builder.

results matching ""

    No results matching ""