Let’s order a pizza in Go — Part 5

An introduction to GORM library for Go.

Luis Masuelli
7 min readMay 17, 2019

This is part of a series of tutorial involving GORM. If you want toread from the beginning, go to part 1. Please also be sure you read / know the contents of part 4.

Adding the toppings, and more…

Toppings are essential elements in the pizzas. They make a pizza… a pizza.

While the first version of pizza involved low quality bread, and some remaining tomatoe sauce and cheese, the concept evolved to what we know as pizza: the big one, which can be cut in triangular slices, and the toppings. Those amazing ingredients that make the pizza my religion. But, while I could dig into elaborating blasphemous analogies, I prefer just creating the models and related stuff.

Also, we need to add more fields (like pizzas’ prices considering different sizes and recipes). Let’s do this:

package main

import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"fmt"
"time"
"github.com/shopspring/decimal"
)


type Workplace struct {
gorm.Model
Name string `gorm:"size:50;not null"`
Address string `gorm:"size:255;not null"`
Phone *string `gorm:"size:20"`
Workers []*Worker
}


type Worker struct {
gorm.Model
WorkplaceID uint `gorm:"not null"`
Workplace *Workplace
Name string `gorm:"size:61;not null"`
Birthday time.Time `gorm:"type:datetime"`
Phone *string `gorm:"size:20"`
Recipes []*Recipe `gorm:"many2many:worker_recipes"`
}


type Recipe struct {
gorm.Model
Name string `gorm:"size:50;not null"`
Workers []*Worker `gorm:"many2many:worker_recipes"`
Toppings []*Topping `gorm:"many2many:recipe_toppings"`
}


type Topping struct {
gorm.Model
Name string `gorm:"size:20;not null"`
Pizzas []*Pizza `gorm:"many2many:recipe_toppings"`
}
type Size struct {
gorm.Model
Name string `gorm:"size:20;not null"`
}


type Pizza struct {
gorm.Model
RecipeID uint `gorm:"not null;unique_indez:pizzas"`
Recipe Recipe
SizeID uint `gorm:"not null;unique_indez:pizzas"`
Size Size
Price float64 `gorm:"not null;type:decimal(10,2)"`
}


func Connect() (*gorm.DB, error) {
return gorm.Open("mysql", "root:my-password@/pizzas?charset=utf8&parseTime=True&loc=Local")
}


func BoxString(x string) *string {
return &x
}


func Migrate(db *gorm.DB) {
workplacePrototype := &Workplace{}
workerPrototype := &Worker{}
recipePrototype := &Recipe{}
sizePrototype := &Size{}
pizzaPrototype := &Pizza{}
toppingsPrototype := &Topping{}
db.AutoMigrate(workplacePrototype, workerPrototype, recipePrototype, sizePrototype, pizzaPrototype, toppingsPrototype)
db.Model(workerPrototype).AddForeignKey("workplace_id", "workplaces(id)", "RESTRICT", "CASCADE")
db.Table("worker_recipes").AddForeignKey("worker_id", "workers(id)", "RESTRICT", "CASCADE")
db.Table("worker_recipes").AddForeignKey("recipe_id", "recipes(id)", "RESTRICT", "CASCADE")
db.Table("recipe_toppings").AddForeignKey("recipe_id", "recipes(id)", "RESTRICT", "CASCADE")
db.Table("recipe_toppings").AddForeignKey("topping_id", "toppings(id)", "RESTRICT", "CASCADE")

}


func Seed(db *gorm.DB) {
cheese := &Topping{
Name: "Cheese",
}
tomatoeSauce := &Topping{
Name: "Tomatoe Sauce",
}
onions := &Topping{
Name: "Onions",
}
tomatoeSlices := &Topping{
Name: "Tomatoe Slices",
}
hamSlices := &Topping{
Name: "Ham Slices",
}
pepperoni := &Topping{
Name: "Pepperoni",
}

recipe1 := &Recipe{
Name: "Mozzarella",
Toppings: []*Topping{
tomatoeSauce,
cheese,
},

}
recipe2 := &Recipe{
Name: "Onions",
Toppings: []*Topping{
onions,
cheese,
},

}
recipe3 := &Recipe{
Name: "Napolitan",
Toppings: []*Topping{
tomatoeSauce,
tomatoeSlices,
cheese,
hamSlices,
},

}
recipe4 := &Recipe{
Name: "Pepperoni",
Toppings: []*Topping{
tomatoeSauce,
pepperoni,
cheese,
},

}
db.Save(recipe1)
db.Save(recipe2)
sizePersonal := Size{
Name: "Personal",
}
sizeSmall := Size{
Name: "Small",
}
sizeMedium := Size{
Name: "Medium",
}
sizeBig := Size{
Name: "Big",
}
sizeExtraBig := Size{
Name: "Extra Big",
}
db.Save(&sizePersonal)
db.Save(&sizeSmall)
db.Save(&sizeMedium)
db.Save(&sizeBig)
db.Save(&sizeExtraBig)

workplace1 := &Workplace{
Name: "Workplace One",
Address: "Fake st. 123rd",
}
workplace2 := &Workplace{
Name: "Workplace Two",
Address: "Evergreen Terrace 742nd",
Phone: BoxString("(56) 123-4789"),
Workers: []*Worker{
{
Name: "Mauricio Macri",
Birthday: time.Date(1959, 2, 8, 12, 0, 0, 0, time.UTC),
Recipes: []*Recipe{
recipe1, recipe2, recipe3,
},
},
{
Name: "Donald Trump",
Birthday: time.Date(1946, 6, 14, 12, 0, 0, 0, time.UTC),
Recipes: []*Recipe{
recipe1, recipe2, recipe4,
},
},
},
}
for sizeIndex, size := range []*Size{sizePersonal, sizeSmall, sizeMedium, sizeBig, sizeExtraBig} {
for recipeIndex, recipe := range []*Recipe{recipe1, recipe2, recipe3, recipe4} {
db.Save(&Pizza{
Size: *size,
Recipe: *recipe,
Price: decimal.NewFromFloat((float64(0.1) * float64(recipeIndex + 1) + 1) + float64(sizeIndex) * 5),
})
}
}

db.Save(workplace1)
db.Save(workplace2)
fmt.Printf("Workplaces created:\n%v\n%v\n", workplace1, workplace2)
fmt.Printf("Recipes created:\n%v\n", []*Recipe{ recipe1, recipe2, recipe3, recipe4 })
}


func ListEverything(db *gorm.DB) {
workplaces := []Workplace{}
t := db.Preload("Workers")
t = t.Preload("Workers.Recipes")
t = t.Preload("Workers.Recipes.Toppings")
t.Find(&workplaces)
for _, workplace := range workplaces {
fmt.Printf("Workplace data: %v\n", workplace)
for _, worker := range workplace.Workers {
fmt.Printf("Worker data: %v\n", worker)
for _, recipe := range worker.Recipes {
fmt.Printf("Recipe data: %v\n", recipe)
for _, topping := range recipe.Toppings {
fmt.Printf("Topping data: %v\n", topping)
}

}
}
}
}


func ClearEverything(db *gorm.DB) {
err1 := db.Delete(&Workplace{}).Error
fmt.Printf("Deleting the records:\n%v\n", err1)
}


func main() {
if db, err := Connect(); err != nil {
fmt.Printf("Dude! I could not connect to the database. This happened: %s. Please fix everything and try again", err)
} else {
defer db.Close()
// Migrate(db)
// Seed(db)
ListEverything(db)
// ClearEverything(db)
}
}

Blow — yes: again — your entire database and try it. It will work. This was essentially explained in the previous section. Now, let’s do actually some querying.

Query methods — Working with whole tables

The methods are vaguely described in the official docs (hopefully these links will never be moved — please) but, if you struggled with previous frameworks (making an emphasis in Django and Rails) those methods will not sound strange at all. However, I’m writing some notes about them, and then giving some examples.

Notes about query methods

I’d like to categorize the query methods in:

  • Queryset building methods: They usually add conditions and/or select fields. They customize the SQL query (the mostly used queryset methods are those that build the where chunk). These method calls will be chained: they produce, each, a new queryset, and don’t execute it against the database.
  • Immediate methods: They act against the database. They take what was accumulated in successive method chainings, and perform a specific action (getting one matching record, getting many matching records, deleting,…).
  • Support methods: Although this category does not exist in the official docs, I’d like to say these methods are not regular chainable ones: they do not affect how the query is executed, but instead hoy specific immediate methods are executed against the built query.
  • Schema-editing methods. I’ll write about them in future sections.

Please take a look to the official docs. I’ll not mirror that content beyond the scope of some examples.

Data types in query method’s arguments

You’ll notice there are times when different types are used as arguments in those methods:

  • Query conditions using escaped arguments.
  • Query conditions and initializations using (relevant) model instances.
  • Query conditions and initializations using map[string]interface{}.
  • They may share a single purpose or be different methods at all, but the caveats will be the same across different methods:
  • If you use escaped arguments, is because you are building the query chunk by hand. In such cases, you are responsible of appropriately referencing the columns by their settings or conventions. Example: db.Where('recipe_id = ?', 4).Find(&pizzas). It was the column name, and not the field name, the one to refer, because you are actually writing sql code.
  • If you use model instances, there is a big caveat here. Say you want to query something in the workers like:db.Where(&Worker{Name:"Nestor Kirchner"}).Find(&worker). You’re initializing a structure with just one field in the query. Aren’t you? Well… not so quick. That structure also has a value in each other field: their corresponding zero value. Let me give you an example: &Worker{Name:"Nestor Kirchner", Birthday: time.Now()} is actually the same (in pure Go) than &Worker{Name:"Nestor Kirchner", Phone: nil, Birthday: time.Now()}. However, the generated query will resemble the syntax in the first case: SELECT * FROM workers WHERE name = 'Nestor Kirchner' AND birthday = '...something...';. Phone will not be included in the field. Zero-value fields are never included in the conditions using this approach. In my example, the zero value of Phone is (*string)nil, and not the empty string.
  • If you use map[string]interface{} then you have the same caveats of the first point (you have to refer the fields), and not the caveat of the second point. The generated query will involve each column (in the key) comparing to their respective values, applying an AND operator to join them all.

Notes on immediate methods

Immediate methods are run on top of a query. They actually execute something against the database. Still, they generate another query, so you can chain many immediate methods. One example of an immediate method is First.

db.Where(“price < ?”, 20.0).First(&pizza)

This method will behave as follows:

  • It will base its execution on top of the former, accumulated, query. In this case, First will retrieve the first pizza (by filling the pointer argument) satisfying the condition of having a price less than 20.
  • It will execute said behavior.
  • If something went wrong, it will hold an error in the resulting queryset (in its .Error property).

Handling errors should be considered a top priority, like this:

pz := &Pizza{}
if err := db.Where("price > ?", 20.0).First(pz).Error; err != nil {
db.Delete(pz)
}

Otherwise, a code like this:

db.Where("price > ?", 20.0).First(pz)
db.Delete(pz)

Has the risk of an error in .First (at minimum, not being able to find any record above 20.0) and leaving the pizza’s pointed structure empty: invoking .Delete will remove all the pizzas in our system!

So, please: always check for errors when calling immediate methods.

Additionally: immediate methods can be chained as well! (although you lose the ability to track errors except for the last call!). By doing so, a single queryset can lead to a line with N immediate methods calls, like:

db.Where("price > ?", 20.0).Find(&pizzas).Count(&anInt)

This call will retrieve all the pizzas with price > 20 in a call, and the count of such pizzas with price > 20 in another call.

Please see the official documentation about particular methods. They are not well documented, but they work as by example.

Notes on queryset building methods

Queryset building is quite like Django’s or Rails’ one. Methods can be chained like this:

wk = &Worker{}
db.Where("phone is null").Where("name like ?", "John%").First(wk)

You have basically three querying methods: Where, Not (its semantic is “where not …”), and Or (it’s semantic is adding a new condition that is separated by an OR from the previous one(s) instead of an AND).

Notes on support (and miscellaneous) methods

There are other methods that can be chained, but do not make part of a query but instead support other specific operations (executed in immediate methods). Two examples are Attrs and Assign, who only make context on specific operations (we’ll visit them in the next part).

Enough with the boring parts!

In the next part we’re gonna see this stuff in action to some degree by making functionalities to our pizza store(s). We’ll be adding a lot of functions, and even perhaps a simple menu interaction as demonstration based on the current state of our source code so far.

Please, if you have any question(s), leave them in the comments box. I’ll try to answer them, and they could also serve to improve this tutorial.

Please accept this disclaimer: I’m still a newbie with gorm, and most of the details I learn them as I write these posts. Said this, perhaps there are examples that could be improved, or even added to documentation. Also please keep in mind that not all operations are allowed in all databases: an example is PostgreSQL not allowing to update columns in tables in an inner join product.

--

--