Let’s order a pizza in Go — Part 2

An introduction to GORM library for Go.

Luis Masuelli
12 min readMay 13, 2019

This is part of a series of tutorial involving GORM. If you want toread from the beginning, go to part 1.

Our connection to the database is done appropriately. Now we have to define our models.

The first models we will care about are the workplace and the worker. The workers will be the one who can prepare specific pizzas — we will add that code later. Now, I created a stub for those models and will add the corresponding explanation:

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", "username:password@/pizzas2?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 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)
}
}

In this article, I will explain a majority of what is involved in this snippet. Everything related to the relationship between workspaces and workers will be covered in the next article.

Just to be sure: I hope there is no need to explain either the main or the Connect functions. They were explained in the previous article. Anyway, you can easily take them for granted for the purpose of this article and still move on.

Model definition

Creating the model

All you need to define a model is to create a struct type. Seriously. A struct type is enough to define a model. This means, this hypothetical chunk would also define a model:

type Foo struct {  
}

I think you’re in need of an actual model with actual fields inside. After all, models must have at least one field when the tables are being created. So minimally you’d create something like:

type Foo struct {
ID uint
}

instead. This will create our table with a single field named ID which is the primary key of the table (Why? Because… magic). The primary key may be of any database-compatible type (we will dig into that later) and, as long as no primary key is setup manually in the type, if the field is named ID it will be setup as the primary key automagically. If your primary key has another name, then it’d be declared as follows:

type Foo struct {
MyPK string `gorm:"primary_key"`
}

Where the name and type is arbitrary (wait! By being a non-integer field, it will lack of an auto incrementfeature some databases have: either literally as in MySQL, or via sequences as in PostgreSQL).

Adding primitive fields

Let’s create the first model: the workplace.

type Workplace struct {
ID uint
Name string `gorm:"size:50;not null"`
Address string `gorm:"size:255;not null"`
Phone *string `gorm:"size:20"`
}

This is a quite dummy model with two string fields (by default, they are mapped as varchar(255) fields in most database engines), one optional string field, and a by-convention primary key.

Do you notice the stuff between backquotes? That’s actually a Go feature called tags. Tags add metadata to the fields (and ONLY to the fields). The backquotes delimiter defines a string with no escape sequences: it is useful when you need a string also containing the " character as in this example (otherwise, using backquotes or double quotes make no difference: the tag is a string). Frameworks and standard libraries (like json) may make (and often they actually do) use of that feature to read annotations from the fields.

For a deep knowledge of what tags are, read this official article. Then come back and keep in mind that our column setup will be done using the gorm:"..." lookup in the tag.

Since strings are mapped to varchar(255) by default, a size constraint was added in the setup. Also, another definition was added (definitions are separated by ;) stating that the underlying column is not null.

A note on null values: underlying columns are nullable by default, except for the primary key, unless told otherwise. This is true even for non-pointer primitive scalar Go types (which cannot have nil as value). When using non-nullable fields, add the not null constraint to not mess your life. If you want to have one or more nullable columns intentionally, read this article about null fields.

Lifecycle timestamps

There are some fields that work out of convention and you can add to every model type to track certain moments of the interaction with a model instance. Those fields have conventional names:

  • CreatedAt: Its value is the moment of the insertion of the instance.
  • UpdatedAt: Its value is the moment of the last update of the instance.
  • DeletedAt: Its value is the moment of the deletion of this instance. This field is the implementation of model instances’ soft delete.

The first two fields are configured as time.Time while the third one must be configured as *time.Time, since it may be empty (not deleted) or set (deleted). See the same article of null fields for more details.

Your models can have one, two, or all of these fields. The ORM will work accordingly with the available fields.

Model definition shortcuts

If you want your model to have a standard primary key and the three lifecycle fields, you may be tempted to do this:

type Workplace struct {
ID uint
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Name string `gorm:"size:50;not null"`
Address string `gorm:"size:255;not null"`
Phone *string `gorm:"size:20"`
}

While this is perfectly allowed, there is a better solution for your needs:

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

The gorm.Model embed does exactly that. While not being mandatory, you can make use of it as much as you need. Also, this teaches us something: embedded fields feature in Go is quite helpful when embedding other stuff as well: We may need our own trait using an uint64 instead of uint primary key.

So far, so good. Our Workplace is now a model. Let’s create the workers with the same principle:

type Worker struct {
gorm.Model
Name string `gorm:"size:61;not null"`
Birthday time.Time `gorm:"type:datetime"`
}

Please pay attention to a special consideration with the Birthday field: its type was explicitly set to datetime. By default, time.Time is mapped against timestamp fields, but timestamps are good for recording real-time events, and not so good for past-dates recordings (like, say, your grandma’s birthday).

Model migration

If you come straight from other frameworks, the reality regarding migrations is quite diverse. There are frameworks/libraries supporting migrations (like Django, Rails or Laravel) and others not supporting them by default (e.g. Hibernate). There are also different mechanism involved in migrations generation among different frameworks, and — fundamentally — a different concept of migration among frameworks.

So, to say, if you come from Django, Laravel or Rails, then you have a broader concept of migrations than what is offered by this library: while migrations are depicted as reversible tasks involving several database operations which are consistent along time and their reverse counterparts, what is supported in gorm (by default) is just a bunch of database operations to add/remove tables, add/remove columns, add/remove indices,… and you have to invoke these operations explicitly.

Just for now, we will declare a little Migrate function we will use to migrate our code appropriately (since we are doing a gradual approach, I will not directly copy the whole Migrate function):

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

Two things matter us here:

  • There is a method called AutoMigrate that helps us with all the magic. Don’t be so happy: it is actually a bad idea in my opinion. But it is good for quick prototyping.
  • We specify which tables to work with by instantiating some prototype pointers. We use those prototypes because they are a mean to extract the appropriate types to work with (which, in this case, will be the desired models). This is mainly because Golang has neither a typeof operator, generics (template classes), or types-as-objects.

Now let’s add another function: the seeder (again: this is a gradual approach — I’ll write a simpler function we will modify later while explaining why).

func BoxString(x string) *string {
return &x
}
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"),
}
db.Save(workplace1)
db.Save(workplace2)
worker1 := &Worker{
Name: "Mauricio Macri",
Birthday: time.Date(1959, 2, 8, 12, 0, 0, 0, time.UTC),
}
worker2 := &Worker{
Name: "Donald Trump",
Birthday: time.Date(1946, 6, 14, 12, 0, 0, 0, time.UTC),
}
fmt.Printf("Workplaces created:\n%v\n%v\nWorkers created:\n%v\n%v\n", workplace1, workplace2, worker1, worker2)
}

What we have so far

Our source code so far now should look like this:

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

type
Worker struct {
gorm.Model
Name string `gorm:"size:61;not null"`
Birthday time.Time `gorm:"type:datetime"`
}
func Connect() (*gorm.DB, error) {
return gorm.Open("mysql", "username:password@/pizzas2?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)
}
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"),
}
db.Save(workplace1)
db.Save(workplace2)
worker1 := &Worker{
Name: "Mauricio Macri",
Birthday: time.Date(1959, 2, 8, 12, 0, 0, 0, time.UTC),
}
worker2 := &Worker{
Name: "Donald Trump",
Birthday: time.Date(1946, 6, 14, 12, 0, 0, 0, time.UTC),
}
db.Save(worker1)
db.Save(worker2)
fmt.Printf("Workplaces created:\n%v\n%v\nWorkers created:\n%v\n%v\n", workplace1, workplace2, worker1, worker2)
}
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)
}
}

Let’s test our brand new code. A quick setup of a MySQL database comes by using Docker:

$ docker run --name mysql-default -e MYSQL_ROOT_PASSWORD=my-password -p 3306:3306 -d mysql$ docker exec -ti mysql-default mysql --user root --password
# Here is where you are prompted for a password and write: my-password
mysql> CREATE DATABASE pizzas;

And a slight change in the connection string to "root:my-password@/pizzas?charset=utf8&parseTime=True&loc=Local" will do the trick.

Provided you downloaded gorm, just build with a standard go build myfile.go and run it. You will see an output like this:

Workplaces created:
&{{1 2019–05–13 10:24:16.819144 -0500 -05 m=+0.120648696 2019–05–13 10:24:16.819144 -0500 -05 m=+0.120648696 <nil>} Workplace One Fake st. 123rd <nil>}
&{{2 2019–05–13 10:24:16.876672 -0500 -05 m=+0.178176695 2019–05–13 10:24:16.876672 -0500 -05 m=+0.178176695 <nil>} Workplace Two Evergreen Terrace 742nd 0xc0000126c0}
Workers created:
&{{1 2019–05–13 10:24:16.892147 -0500 -05 m=+0.193651616 2019–05–13 10:24:16.892147 -0500 -05 m=+0.193651616 <nil>} Mauricio Macri 1959–02–08 12:00:00 +0000 UTC}
&{{2 2019–05–13 10:24:16.900379 -0500 -05 m=+0.201883724 2019–05–13 10:24:16.900379 -0500 -05 m=+0.201883724 <nil>} Donald Trump 1946–06–14 12:00:00 +0000 UTC}

Just go again to the database client inside the Docker container, and…

$ docker exec -ti mysql-default mysql --user root --password
# Here is where you are prompted for a password and write: my-password
mysql> USE pizzas;
mysql> SELECT * FROM workplaces;
mysql> SELECT * FROM workers;

In both select statements, you will notice the data is stored appropriately.

Perhaps you noticed this: The primary key is automagically populated in the model instance after the model is saved. You don’t have to worry there, and you’ll notice this is also true when adding nested objects by relationships. We’ll see the relationships in the next part when we tackle the relationship between the worker and the workplace.

Schema conventions overview

Perhaps you noticed this, as this is also a convention in many frameworks:

  • Table names are pluralized snake-case versions of model type names.
  • Column names are snake-case versions of model field names.

This convention is by default, but you can override them if you want:

Overriding table’s name

If we prefer our Worker model being backed by an “employees” table instead of a “workers” table, then we define like this:

func (Worker) TableName() string {
return "employees"
}

Overriding a column’s name

If we prefer the “phone” name being backed by a “telephone” field instead, then we customize its tag in the workers table like this (look at the bold part):

type Workplace struct {
gorm.Model
Name string `gorm:"size:50;not null"`
Address string `gorm:"size:255;not null"`
Phone *string `gorm:"size:20;column:telephone"`
}

Retrieving the data

Let’s create a function to list everything…

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

The Find method invocation is the perfect explanation of the difference between simple and trivial. Many things have to be accounted for:

  • We must pass a pointer of a slice as argument. Its type will be extracted.
  • The underlying SQL will be executed against the underlying table of the extracted type.
  • The rows will be populated one by one in the pointed slice. The slice will be modified in-place since we’re passing it by reference.

So, it is a kind of black magic you’ll be quickly used to.

Let’s edit our main function to invoke this one instead of migrate/seed pair:

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

This was my output:

Found workplaces:
[{{1 2019–05–13 10:24:17 -0500 -05 2019–05–13 10:24:17 -0500 -05 <nil>} Workplace One Fake st. 123rd <nil>} {{2 2019–05–13 10:24:17 -0500 -05 2019–05–13 10:24:17 -0500 -05 <nil>} Workplace Two Evergreen Terrace 742nd 0xc0001a2120}]
Found workers:
[{{1 2019–05–13 10:24:17 -0500 -05 2019–05–13 10:24:17 -0500 -05 <nil>} Mauricio Macri 1959–02–08 07:00:00 -0500 -05} {{2 2019–05–13 10:24:17 -0500 -05 2019–05–13 10:24:17 -0500 -05 <nil>} Donald Trump 1946–06–14 07:00:00 -0500 -05}]

(Just keep in mind: 0xc0001a2120 is a memory address because we have a pointer to to the (boxed) phone number — if you want the boxed value, just dereference its content as normal).

Deleting the data

This is the easiest one. Let’s create a quite dangerous function:

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

And use it in place of listing everything or migrating/seeding:

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

Now run it. It should work. When inspecting the data, you’ll see:

Due to experimentation, my data may look different than yours — mainly in the timestamps and the IDs.

You can tell the rows still exist, but now they have a value in the deleted_at field. This is the underlying meaning of the soft-delete feature: records are not actually being deleted but, instead, set its timestamp appropriately.

Deleting a single record

Now, welcome to a potential hell. Seriously. I did not gratuitously say the Delete function is dangerous: it is quite more than the DELETE FROM workers; statement by itself: in this case, you may notice you forgot a WHERE statement. It will not necessarily be the case here. To delete all the workers you use a prototype model.

db.Delete(&Worker{})

And, to delete just one, just use a regular, already saved or retrieving model:

model := &Worker{}
db.Where("ID = ?", 1).First(model)
//...
db.Delete(model)

What’s the difference with both records? The presence of a primary key. Yes: Trying to delete a model with a primary key in its zero value is as dangerous as forgetting a WHERE, but more likely. This means: you’ll never use 0 as primary key.

That’s everything for this part

With this, you have learned how to:

  • Run a simple database container to perform tests. It was not originally intended, but there you have.
  • Migrate models (in the bad way, yet — we’ll cover good ways much later).
  • Create records in your database (the same Save function works for updates).
  • Retrieve records using Find. Later, you saw how you can add Where conditions to queries (anyway, we’ll discuss that also later).
  • Retrieve one single record using First. This will work the same as Find but with a single record.
  • Delete all the records and delete one single record. And then you must move from Don’t forget to put the WHERE in the DELETE FROM to Don’t forget to ensure the deleted model has a non-zero primary key. Still I tell you: pay attention, for consequences are quite real there.

There’s still a lot to cover

Perhaps you have a lot of questions here, like:

  • How do I add more complex queries?
  • How do I detect database errors?
  • How do I bulk-update?
  • How do I detect de number of affected rows when bulk-updating and bulk-deleting?
  • How do I detect whether First did not match and fill a row?
  • What’s that about the slice-based and struct-based fields in the models?

For which I have to answer: I’ll talk about them in future articles. In particular, the stuff regarding slice-based and struct-based fields will be discussed in the following article (Part 3), while the other ones will be discussed… later.

Please be patient, for I’m also a newbie @ gorm, and an almost-newbie at Go in general.

Go to part 3

Got questions?

Please leave any question you have. I may even edit this article to add some of the questions / answers (the most relevant ones).

--

--