Let’s order a pizza in Go — Part 3

An introduction to GORM library for Go.

Luis Masuelli
10 min readMay 14, 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 2.

Having worked on the independent models, we can add, edit, list and delete individual models (using some basic tools, but we can). There is something yet to be discussed: the relationship between workplaces (the pizza stores) and employees (the chefs).

I have, still pending, the task to describe the pizzas and their ingredients. I didn’t forget that part.

Workplace, workers, and their relationship

If you’re reading this, then it’s probably you already have some concepts of relational databases and I don’t need to dig further, but just recall some details:

  • One-to-many relationships involve a foreign key in a child model relating it to certain parent model (usually, of different types).
  • One-to-one relationships work the same, except that the foreign key is also a unique key. A special kind of one-to-one is when the foreign key is the same as the primary key (this one is good for profiling or inheritance).
  • Both relationships can imply a reverse side (specified in the target model) which will be a list of objects (for the many type) or a single object (for the one type). In both cases, an object depends or may depend on the target model via a foreign key.
  • Many-to-many relationships are different: they involve a third table we usually call join table. No foreign key is declared in either of the two models for this relationship to work: it is the join table the one having the foreign keys.

Except for the special case about inheritance, all the relationship types are supported by gorm (which is more than what other orm libraries achieved in Go). And since one workplace may have many workers, who only belong to it we can safely assume a one-to-many relationship from workplaces to workers is the appropriate one.

So let’s add a foreign key to our Worker model:

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"`
}

I added two fields in the Worker model:

  • The struct field. This field, being a non-scalar one (and also not an embedded field or a time.Time field), tells us about a foreign key: a many-to-one relationship (many workers -> one workplace).
  • The scalar field. Its name will be — for the sake of recognition— a concatenation of the struct field’s name and the name of the primary key in the target mode. That’s the reason behind WorkplaceID.

Also, I created one field in the Workplace model:

  • The slice field. This field is automagically the reverse side of a relationship. This slice must have a struct element or a pointer-to-struct element type.

This basic setup (and a new instruction in the migration) will work by default, according to the conventions.

One-to-many conventions

By default a one-to-many relationship works by convention. This means:

  • You have a source or child model. In this case, it is Worker.
  • You have a target or parent model. In this case, it is Workplace.
  • You create, inside the child model, a field of parent type. Name it. The name will matter. We will say this is the association name. In my example, the name is also Workplace. It is not mandatory to be the same name of the type.
  • You remember the primary key’s field name in the parent model. Since I’m using gorm.Model, the primary key’s field name is ID.
  • Then you concatenate both names, Workplace (association name in the child) and ID (primary key’s field name in the parent), and get a final name: WorkplaceID. Take the parent primary key’s field type: Workplace’s ID field, in my case, is uint. Now, with all that, declare a field of that type in the child model: WorkplaceID uint. Great! This one is your foreign key for the association.

That’s it! This setup will work by convention:

  • Now the object you see in the association field will match its key in the foreign key field.
  • Also, the Workers slice will work in the backwards way: they will match the reverse association (and foreign key) and list all of the children model instances.

Optional one-to-many parent

If you want the parent link to be optional, then convert the foreign keys an associations in the child model from:

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"`
}

to pointers:

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"`
}

in both fields at once. You’ll get unexpected results if one of them is a pointer and the other one is not (e.g. the association field not initializing under certain conditions). This one is in contrast to what happens to the “has many” side, where using a struct or a pointer-to-struct element type makes no difference to the gorm engine.

One-to-many customization

You can customize either side of the relationship, since they are not forcibly related once declared, unless there is just one of them on each side of the relationship.

If you want to customize the “belongs to” side, then you just add a gorm tag in the association field, like this:

type Workplace struct {
gorm.Model
Name string `gorm:"size:50;not null"`
Address string `gorm:"size:255;not null"`
IRSNumber string `gorm:"size:30;not null;unique"`
Phone *string `gorm:"size:20"`
Workers []Worker
}
type Worker struct {
gorm.Model
WpIRSNumber string `gorm:"not null"`
Workplace Workplace `gorm:"foreignkey:WpIRSNumber; association_foreignkey:IRSNumber"`
Name string `gorm:"size:61;not null"`
Birthday time.Time `gorm:"type:datetime"`
}

This customization implies:

  • You create a new key called IRSNumber in the parent model and make it a unique key.
  • In the child model you edit the “belongs to” side like I did to specify which one will be the association_foreignkey (otherwise, the Workplace’s primary key will be used): the remote side (i.e. in the parent model) of the foreign key you’ll use.
  • Then, editing the foreignkey field is the last step. If you don’t, then you better already have a field whose name is a concatenation of both the association name and the field name you gave toassociation_foreignkey (or the remote primary key) in your child model.

You can also customize the “has many” side: the slice field.

  • Again, you create (or use) the same key IRSNumber, unique, in Workplace.
  • Then you setup the association_foreignkey with the same value as in the child model: the meaning, now, will be: the field name to use as key in the parent model, where we are defining the slice.
  • Then you setup the foreignkey which will be the field to use in the child model to work as foreign key.

But, again, this time in the slice field.

type Workplace struct {
gorm.Model
Name string `gorm:"size:50;not null"`
Address string `gorm:"size:255;not null"`
IRSNumber string `gorm:"size:30;not null;unique"`
Phone *string `gorm:"size:20"`
Workers []Worker `gorm:"foreignkey:WpIRSNumber; association_foreignkey:IRSNumber"`
}
type Worker struct {
gorm.Model
WpIRSNumber string `gorm:"not null"`
Workplace Workplace `gorm:"foreignkey:WpIRSNumber; association_foreignkey:IRSNumber"`
Name string `gorm:"size:61;not null"`
Birthday time.Time `gorm:"type:datetime"`
}

While the involved foreign key field is the same (and required, and you must setup it appropriately), you don’t need to create an association “belongs to” field in the child model if you want to use a “has many” field in the parent.

In the converse way: you don’t need to create a “has many” side if you just need a “belongs to” side (and yo created and appropriately setup the foreign keys).

Migrations with foreign keys’ constraints

Okay, now we have our new fields, which we must also migrate. To do that, just blow your database and start over. We’ll be using new code here.

What you have to learn here is that the foreign keys’ constraints are not auto-migrated (please also remember that auto-migration is in general a bad idea). You have to manually add them straight to the columns — considering the naming conventions and column setup in your model. In our example that means changing our migrate function from this:

func Migrate(db *gorm.DB) {
workplacePrototype := &Workplace{}
workerPrototype := &Worker{}
db.AutoMigrate(workplacePrototype, workerPrototype)
}

to this:

func Migrate(db *gorm.DB) {
workplacePrototype := &Workplace{}
workerPrototype := &Worker{}
db.AutoMigrate(workplacePrototype, workerPrototype)
db.Model(workerPrototype).AddForeignKey("workplace_id", "workplaces(id)", "RESTRICT", "CASCADE")
}

The db.Model takes a model instance. Depending on what you’ll do later with it, the instance will be taken as a prototype of the table or as an object to affect.

Now your foreign keys constraints will be also installed and the code will work… after we change our example seeds.

A new seed with foreign keys

Our former seed fills two different tables. They have no relation between them. Our new example (which btw is at the beginning of part 2) will work appropriately: Change the seed you used at the end of the 2nd part, with the one you saw at the beginning. I mean, this one:

func Seed(db *gorm.DB) {
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),
},
{
Name: "Donald Trump",
Birthday: time.Date(1946, 6, 14, 12, 0, 0, 0, time.UTC),
},
},
}
db.Save(workplace1)
db.Save(workplace2)
fmt.Printf("Workplaces created:\n%v\n%v\n", workplace1, workplace2)
}

You can see how the second call to db.Save also stores nested objects. Yes: nested objects work appropriately in “has many” sides but they only work if your intention is to create or update specific children objects — There is no place to delete items or replace the whole slice at database level by this mean. I’m covering full associations management later.

Please bear in mind that you may have errors when storing nested children (e.g. the child table having a unique key constraint being violated). Perhaps you’d like to invoke nested save operations inside a transaction. We’re also covering that part later.

Now we can proceed with listing the data.

Listing table data — Preloading associations

Our current listing method is this one:

func ListEverything(db *gorm.DB) {
workplaces := []Workplace{}
db.Find(&workplaces)
workers := []Worker{}
db.Find(&workers)
fmt.Printf("Found workplaces:\n%v\nFound workers:\n%v\n", workplaces, workers)
}

Which used to work, since we listed (and iterated over) two unrelated tables.

We may now use a single sentence to list the appropriate hierarchy of the workplaces. This means: each workplace, with its workers.

Since in our new query we’d like to fetch each workplace with its workers, then we will use the preloading feature, which does exactly that. Our query will become:

func ListEverything(db *gorm.DB) {
workplaces := []Workplace{}
db.Preload("Workers").Find(&workplaces)
fmt.Printf("Found workplaces:\n%v\n", workplaces)
}

Try it (replace the calls in the main function to execute this one). It will work. You’ll later learn how to customize the Preload operation to add conditions, but so far it will work and get you into these relationships.

Saving / clearing the data

Although listing/preloading will work like a charm, saving and deleting operations will have their caveats.

Notes on creation

You can create literal nested objects inside the “has many” side.

Notes on listing

Nested relationships (“has many” fields) are not retrieved by default. Use Preload to do that.

Also, you can call Preload many consecutive times involving different levels. See the official docs regarding that.

Remember that while the foreign key and the association field must match their pointer-status (i.e. being a pointer or not) in the “belongs to” side, you have free choice when declaring the “has many” side: Either []Worker or []*Worker will do what you need.

Notes on deletion

Try adding this clearing function:

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

Replace the call in main, and run. Ensure no error is printed. Then go again to your favorite database client (I still use my Docker command line) and check whether there are no records in the database.

What the f…! Seems there are records in the database!

Yes. This is because since we have the soft-delete field (DeletedAt), parent fields are not really deleted, although we commanded to.

Also, children are also not deleted! But in this case, they don’t even have a soft-delete date being set! This is because soft-deletion does not cascade. The cascade setting goes into the database foreign key constraint (which the orm is not aware of), and there is not orm-support for higher level scenarios, like soft deletion. See this StackOverflow exchange for more details.

Notes on update

You can save a Workplace object and set new values for the Workers collection. Put this code in a new function of your choice.

workplace := &Workplace{}
db.Where("ID = ?", "1").Find(&workplace)
workplace.Workers = []Worker{
{/*... fill the data for a new worker ...*/}
}
db.Save(workplace)

What you’ll be doing here is not replacing the whole workers with a new one, but instead, adding a worker to the in-database collection.

Child elements are not deleted by removing them from a collection. You’ll have to delete them via regular database calls (db.Delete(&aWorker)). They can only be updated or created.

Said this, we’re done for now: Our one-to-many relationship works like a charm, and we can move on.

Our full source code so far

So far, I have this source code:

package main

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

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"`
}


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{}
db.AutoMigrate(workplacePrototype, workerPrototype)
db.Model(workerPrototype).AddForeignKey("workplace_id", "workplaces(id)", "RESTRICT", "CASCADE")
}


func Seed(db *gorm.DB) {
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),
},
{
Name: "Donald Trump",
Birthday: time.Date(1946, 6, 14, 12, 0, 0, 0, time.UTC),
},
},
}
db.Save(workplace1)
db.Save(workplace2)
fmt.Printf("Workplaces created:\n%v\n%v\n", workplace1, workplace2)
}


func ListEverything(db *gorm.DB) {
workplaces := []Workplace{}
db.Preload("Workers").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)
}
}
}


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)
}
}

Please, if you have any thoughts, found any bug, or have any doubt, let me know in the comments box.

See you in the next part! The pizzas are finally coming.

Go to part 4

--

--