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,Usertype defines a one-to-many relation withBooksslice type through the use ofhas_manytag, meaning aUsercan own manyBooks. When querying to the database, Pop will load all records from thebookstable that have a column nameduser_id, or the column specified withfk_idthat matches theUser.IDvalue. -
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_totag was defined. This tag is mostly used to indicate that model owns its “existence” to the association field withbelongs_to. In the example above,Booktype usebelongs_toto indicate that it is owned by aUsertype. When querying to the database, Pop will load a record from theuserstable withidthat matches withBook.UserIDvalue. -
has_one: This tag is used to describe one-to-one relationships in the database. In the example above, there is only oneFavoriteSongwithin all songs records thatUsertype like the most. When querying to the database, Pop will load a record from thesongstable that have a column nameduser_id, or the column specified withfk_idthat matches theUser.IDfield value. -
many_to_many: This tag is used to describe many-to-many relationships in the database. In the example above, the relationship betweenUsertype andAddressesslice type exists to indicate anUsercan own manyHousesand aHousecan be owned by manyUsers. It is important to notice that value formany_to_manytag 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 theaddressestable through the associative tableusers_addresses. Tableusers_addressesMUST defineaddress_idanduser_idcolumns to matchUser.IDandAddress.IDfield values. You can also define afk_idtag that will be used in the target association i.e.addressestable. -
fk_id: This tag can be used to define the column name in the target association that matches model ID. In the example above,Songhas a column namedu_idthat references the id of theuserstable. When loadingFavoriteSong,u_idcolumn will be used instead ofuser_id. -
order_by: This tag can be used in combination withhas_manyandmany_to_manytags 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. UseEagerDefaultorEagerPreloadas 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 activateEagerPreloadmode 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 activateEagermode 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
Booksis ahas_manyassociation and it will realize that to actually update each book it will need to get theUser IDfirst. So, it proceeds to store firstUserdata so it can retrieve an ID and then use that ID to fillUserIDfield in everyBookinBooks. It updates all affected books in the database using theirIDs to target them. -
FavoriteSongis ahas_oneassociation and it uses same logic described inhas_manyassociation. SinceUserdata was previously saved before updating all affected books, it already knows thatUserhas got anIDso it fills itsUserIDfield with that value andFavoriteSongis then updated in the database. -
Housesin this example is amany_to_manyrelationship and it will have to deal with two tables in this case:usersandaddresses. BecauseUserwas already stored, it already has itsID. It will then use theIDs passed with theAddressesto 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
Booksis ahas_manyassociation and it will realize that to actually store every book it will need to get theUser IDfirst. So, it proceeds to first store/create theUserdata so it can retrieve an ID and then use that ID to fill theUserIDfield in everyBookinBooks. Later it stores all books in the database. -
FavoriteSongis ahas_oneassociation and it uses same logic described in thehas_manyassociation. SinceUserdata was previously saved before creating all books, it already knows thatUserhas got anIDso it fills itsUserIDfield with that value andFavoriteSongis then stored in the database. -
Housesin this example is amany_to_manyrelationship and it will have to deal with two tables, in this case:usersandaddresses. It will need to store all addresses first in theaddressestable before saving them in the many to many(join) table. BecauseUserwas 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 theAssociationCreatableStatementinterface, all other associations by default implement theAssociationCreatableinterface.
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.