Database
Associations and Relationships
Associations are the Pop way to define a relation between two objects in the database. In this chapter, you’ll learn how to define associations using struct tags; and how to manipulate them with the Eager()
modifier.
Example
type User struct {
ID uuid.UUID
Email string
Password string
Books Books `has_many:"books" order_by:"title asc"`
FavoriteSong Song `has_one:"song" fk_id:"u_id"`
Houses Addresses `many_to_many:"users_addresses"`
}
type Book struct {
ID uuid.UUID
Title string
Isbn string
User User `belongs_to:"user"`
UserID uuid.UUID
}
type Song struct {
ID uuid.UUID
Title string
UserID uuid.UUID `db:"u_id"`
}
type Address struct {
ID uuid.UUID
Street string
HouseNumber int
}
type Books []Book
type Addresses []Address
Available Struct Tags
Using the above example code below is a list of available struct tags and how to use them.
-
has_many
: This tag is used to describe one-to-many relationships in the database. In the example,User
type defines a one-to-many relation withBooks
slice type through the use ofhas_many
tag, meaning aUser
can own manyBooks
. When querying to the database, Pop will load all records from thebooks
table that have a column nameduser_id
, or the column specified withfk_id
that matches theUser.ID
value. -
belongs_to
: This tag is used to describe the owner in the relationship. An owner represents a highly coupled dependency between the model and the target association field wherebelongs_to
tag was defined. This tag is mostly used to indicate that model owns its “existence” to the association field withbelongs_to
. In the example above,Book
type usebelongs_to
to indicate that it is owned by aUser
type. When querying to the database, Pop will load a record from theusers
table withid
that matches withBook.UserID
value. -
has_one
: This tag is used to describe one-to-one relationships in the database. In the example above, there is only oneFavoriteSong
within all songs records thatUser
type like the most. When querying to the database, Pop will load a record from thesongs
table that have a column nameduser_id
, or the column specified withfk_id
that matches theUser.ID
field value. -
many_to_many
: This tag is used to describe many-to-many relationships in the database. In the example above, the relationship betweenUser
type andAddresses
slice type exists to indicate anUser
can own manyHouses
and aHouse
can be owned by manyUsers
. It is important to notice that value formany_to_many
tag is the associative table that connects both sides in the relationship; in the example above this value is defined asusers_addresses
. When querying to the database, Pop will load all records from theaddresses
table through the associative tableusers_addresses
. Tableusers_addresses
MUST defineaddress_id
anduser_id
columns to matchUser.ID
andAddress.ID
field values. You can also define afk_id
tag that will be used in the target association i.e.addresses
table. -
fk_id
: This tag can be used to define the column name in the target association that matches model ID. In the example above,Song
has a column namedu_id
that references the id of theusers
table. When loadingFavoriteSong
,u_id
column will be used instead ofuser_id
. -
order_by
: This tag can be used in combination withhas_many
andmany_to_many
tags to indicate the order for the association when loading. The format to use isorder_by:"<column_name> <asc | desc>"
Loading Associations
Pop currently provides two modes for loading associations; each mode will affect the way pop loads associations and queries to the database.
Eager. Default mode. By enabling this mode, pop will perform “n” queries for every association defined in the model. This means more hits to the database in order to not affect memory use.
EagerPreload. Optional mode. By enabling this mode, pop will perform one query for every association defined in the model. This mode will hit the database with a reduced frequency by sacrifing more memory space.
-
pop.SetEagerMode
: Pop allows enabling any of these modes globally which will affect ALL queries handle performance. UseEagerDefault
orEagerPreload
as parameter to activate any of these modes. -
tx.EagerPreload | q.EagerPreload
: Pop allows developers to take control in which situations they want Pop to perform any of these modes when necessary. This method will activateEagerPreload
mode only for the query in action. -
tx.Eager | q.Eager
: Pop allows developers to take control in which situations they want Pop to perform any of these modes when necessary. This method will activateEager
mode only for the query in action.
Eager Mode
The pop.Connection.Eager()
method tells Pop to load the associations for a model once that model is loaded from the database. This mode will perform “n” queries for every association defined in the model.
for i := 0; i < 3; i++ {
user := User{ID: i + 1}
tx.Create(&user)
}
for i := 0; i < 3; i++ {
book := Book{UserID: i +1}
tx.Create(&book)
}
u := Users{}
err := tx.Eager().All(&u) // loads all associations for every user registered, i.e Books, Houses and FavoriteSong
Eager
mode will:
- Load all users.
SELECT * FROM users;
- Iterate on every user and load its associations:
SELECT * FROM books WHERE user_id=1)
SELECT * FROM books WHERE user_id=2)
SELECT * FROM books WHERE user_id=3)
EagerPreload Mode
The pop.Connection.EagerPreload()
method tells Pop to load the associations for a model once that model is loaded from the database. This mode will hit the database with a reduced frequency by sacrifing more memory space.
for i := 0; i < 3; i++ {
user := User{ID: i + 1}
tx.Create(&user)
}
for i := 0; i < 3; i++ {
book := Book{UserID: i +1}
tx.Create(&book)
}
u := Users{}
err := tx.EagerPreload().All(&u) // loads all associations for every user registered, i.e Books, Houses and FavoriteSong
EagerPreload
mode will:
- Load all users.
SELECT * FROM users;
- Load associations for all users in one single query.
SELECT * FROM books WHERE user_id IN (1,2,3))
Load Specific Associations
By default Eager
and EagerPreload
will load all the assigned associations for the model. To specify which associations should be loaded you can pass in the names of those fields to the Eager
or EagerPreload
methods and only those associations will be loaded.
err = tx.Eager("Books").Where("name = 'Mark'").All(&u) // load only Books association for user with name 'Mark'.
// OR
err = tx.EagerPreload("Books").Where("name = 'Mark'").All(&u) // load only Books association for user with name 'Mark'.
Pop also allows you to eager load nested associations by using the .
character to concatenate them. Take a look at the example below.
// will load all Books for u and for every Book will load the user which will be the same as u.
tx.Eager("Books.User").First(&u)
// OR
tx.EagerPreload("Books.User").First(&u)
// will load all Books for u and for every Book will load all Writers and for every writer will load the Book association.
tx.Eager("Books.Writers.Book").First(&u)
// OR
tx.EagerPreload("Books.Writers.Book").First(&u)
// will load all Books for u and for every Book will load all Writers. And Also it will load the favorite song for user.
tx.Eager("Books.Writers").Eager("FavoriteSong").First(&u)
// OR
tx.EagerPreload("Books.Writers").EagerPreload("FavoriteSong").First(&u)
Loading Associations for an Existing Model
The pop.Connection.Load()
method takes a model struct, that has already been populated from the database, and an optional list of associations to load.
tx.Load(&u) // load all associations for user, i.e Books, Houses and FavoriteSong
tx.Load(&u, "Books") // load only the Books associations for user
The Load
method will not retrieve the User
from the database, only its associations.
Flat Nested Creation
Pop allows you to create the models and their associations with other models in one step by default. You no longer need to create every association separately anymore. Pop will even create join table records for many_to_many
associations.
Assuming the following pieces of pseudo-code:
book := Book{Title: "Pop Book", Description: "Pop Book", Isbn: "PB1"}
tx.Create(&book)
song := Song{Title: "Don't know the title"}
tx.Create(&song)
addr := Address{HouseNumber: 1, Street: "Golang"}
tx.Create(&addr)
user := User{
Name: "Mark Bates",
Books: Books{Book{ID: book.ID}},
FavoriteSong: song,
Houses: Addresses{
addr,
},
}
err := tx.Create(&user)
-
It will notice
Books
is ahas_many
association and it will realize that to actually update each book it will need to get theUser ID
first. So, it proceeds to store firstUser
data so it can retrieve an ID and then use that ID to fillUserID
field in everyBook
inBooks
. It updates all affected books in the database using theirID
s to target them. -
FavoriteSong
is ahas_one
association and it uses same logic described inhas_many
association. SinceUser
data was previously saved before updating all affected books, it already knows thatUser
has got anID
so it fills itsUserID
field with that value andFavoriteSong
is then updated in the database. -
Houses
in this example is amany_to_many
relationship and it will have to deal with two tables in this case:users
andaddresses
. BecauseUser
was already stored, it already has itsID
. It will then use theID
s passed with theAddresses
to create the coresponding entries in the join table.
For a belongs_to
association like shown in the example below, it fills its UserID
field before being saved in the database.
book := Book{
Title: "Pop Book",
Description: "Pop Book",
Isbn: "PB1",
User: user,
}
tx.Create(&book)
Eager Creation
Pop also allows you to create models and embed the creation of their associations in one step as well.
Assuming the following pieces of pseudo-code:
user := User{
Name: "Mark Bates",
Books: Books{{Title: "Pop Book", Description: "Pop Book", Isbn: "PB1"}},
FavoriteSong: Song{Title: "Don't know the title"},
Houses: Addresses{
Address{HouseNumber: 1, Street: "Golang"},
},
}
err := tx.Eager().Create(&user)
-
It will notice
Books
is ahas_many
association and it will realize that to actually store every book it will need to get theUser ID
first. So, it proceeds to first store/create theUser
data so it can retrieve an ID and then use that ID to fill theUserID
field in everyBook
inBooks
. Later it stores all books in the database. -
FavoriteSong
is ahas_one
association and it uses same logic described in thehas_many
association. SinceUser
data was previously saved before creating all books, it already knows thatUser
has got anID
so it fills itsUserID
field with that value andFavoriteSong
is then stored in the database. -
Houses
in this example is amany_to_many
relationship and it will have to deal with two tables, in this case:users
andaddresses
. It will need to store all addresses first in theaddresses
table before saving them in the many to many(join) table. BecauseUser
was already stored, it already has anID
. * This is a special case to deal with, since this behavior is different from all other associations, it is solved by implementing theAssociationCreatableStatement
interface, all other associations by default implement theAssociationCreatable
interface.
For a belongs_to
association like shown in the example below, it will need to first create the User
to retrieve its ID value and then fill its UserID
field before being saved in the database.
book := Book{
Title: "Pop Book",
Description: "Pop Book",
Isbn: "PB1",
User: User{
Name: nulls.NewString("Larry"),
},
}
tx.Eager().Create(&book)
In the case where you feed the eager create with associated models that already exist, it will, instead of creating duplicates of them or updating the contents of them, simply create/update the associations with them.