Today we continue learning about declaring models in GORM. In this topic, you'll learn about different relation types, how to establish relationships between models, and a unique method that allows you to set up "association mode" between models.
Types of relationships between models
GORM makes it easy to establish and work with relationships between models. It supports three types of relationships:
- One-to-one
- One-to-many
- Many-to-many
Establishing relationships is essential as it helps to express the connections between different models in your application. Now let's go ahead and learn how to connect models together using GORM.
One-to-one relationship
A one-to-one relationship is when one record in a table corresponds to exactly one record in another table. One-to-one relationships are less common than one-to-many or many-to-many relationships, mainly due to the evolving nature of real-world relationships.
Take a brand-new social media app as an example. Initially, it may only allow a user to have a single profile. However, as the app gets updated over time, it may let users have multiple profiles, like personal and professional ones, transforming the relationship between user and profile from one-to-one to one-to-many.
A feasible scenario for one-to-one relationships is to separate optional columns from a table; consider that a user in an application can have a single set of custom settings like theme and language. Initially, these settings could be part of the users table; however, if you only wanted to keep the users table with general user data, you could move them into a new settings table, and establish a one-to-one relationship between users and settings.
To set up this one-to-one relationship using GORM, you need a field named after the child model in the parent model (Setting) and a field with the parent model's name plus "ID" (UserID) that has the `gorm:"unique;not null"` struct tag in the child model.
Below is an example of declaring a one-to-one relationship between the User and Setting models:
type User struct { // Parent model
gorm.Model
Name string
Email string
Setting Setting // One-to-one relationship with `Setting` model
}
type Setting struct { // Child model
gorm.Model
Theme string
Language bool
UserID uint `gorm:"unique;not null"` // Foreign key for the `User` model
}One-to-many relationship
A one-to-many relationship is when one record in a table is associated with multiple records in another table. One-to-many relationships are often the most common type of database relationship because they mirror many real-world interactions.
For example, in an e-commerce app, a category can have many products, or a customer can place multiple orders. And in a blog app, a single user can write many posts.
To establish one-to-many relationships, GORM looks for a field with a slice of the child model ([]Post) in the parent model and a field with the parent model's name plus "ID" (UserID) in the child model. Here's an example of declaring a one-to-many relationship between the User and Post models:
type User struct { // Parent model
gorm.Model
Name string
Email string
Posts []Post // One-to-many relationship with `Post` model
}
type Post struct { // Child model
gorm.Model
Title string
Content string
UserID uint // Foreign key for the `User` model
}
Sometimes, you might want to use a different naming convention for foreign key fields or define a custom foreign key relationship. In these scenarios, you can use the `gorm:"foreignKey:..."` tag to specify the foreign key field explicitly; this gives you more control and flexibility when defining relationships between your models. However, it's generally unnecessary if your model's fields follow the above naming conventions.
Many-to-many relationships
A many-to-many relationship is when multiple records in a table are associated with multiple records in another table. Consider a scenario where you build an application to manage users and their assigned roles for a company to illustrate many-to-many relationships.
You would declare two models: User and Role. Each user can have multiple roles, and each role can be assigned to multiple users; then, to declare a many-to-many relationship in GORM, you can use the `gorm:"many2many:..."` tag.
Below is an example of declaring a many-to-many relationship between the User and Role models:
type User struct { // `User` can have multiple `Role`s
gorm.Model
Name string
Email string
// Many-to-many relationship with `Role` model
Roles []Role `gorm:"many2many:user_roles"`
}
type Role struct { // Each `Role` can be assigned to multiple `User`s
gorm.Model
Name string
Description string
// Many-to-many relationship with `User` model
Users []User `gorm:"many2many:user_roles"`
}
Now, you might be wondering, if you have a field with a slice of []Role within the User model and a field with a slice of []User within the Role model, wouldn't that lead to infinite recursion? Not quite! The `gorm:"many2many:..."` struct tag informs GORM that these slice fields represent relationships, not paths to endless recursion. GORM does not delve into these relationships beyond one level of depth, so it manages them efficiently, facilitating the fetching of related records without complex join operations.
gorm:"many2many:..." tag and running a migration, GORM will create a join table to manage the relationship between the two models.In the above example, GORM would create the user_roles join table with the columns user_id INTEGER REFERENCES users(id) and role_id INTEGER REFERENCES roles(id):
| user_roles | |
|---|---|
user_id |
INTEGER, FK(users) |
role_id |
INTEGER, FK(roles) |
The db.Model() method
Finally, let's learn about GORM's db.Model() method. It allows you to specify the model you want to work with when performing operations on the database. It is handy when querying records, as it provides a way to target specific models and their associated tables.
To further explain how the db.Model() method works, let's go back to the scenario where you build an application to manage users and their assigned roles. Remember that you previously declared two models: User and Role.
Now, let's say you want to retrieve the roles a specific user has been assigned. You can use the db.Model() method along with the Association() method to achieve this:
... // `User`, and `Role` models declaration go here.
func main() {
db, err := gorm.Open(sqlite.Open("hyperskill.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
var user User
result := db.First(&user) // Retrieve the first user
if result.Error != nil {
log.Fatalf("cannot retrieve user: %v", result.Error)
}
// Retrieve the user's roles using `db.Model()` and `Association()`
var roles []Role
err = db.Model(&user).Association("Roles").Find(&roles)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User %s has the following roles:\n", user.Name)
for _, role := range roles {
fmt.Printf("%s — %s\n", role.Name, role.Description)
}
}
// Output:
// User Alice has the following roles:
// Admin — Administrator with full access
// Editor — Content Editor
SQLite database.In the above example, we pass a pointer to the user struct as an argument to the db.Model() method, to specify that we want to work with the User model. Next, we chain the Association() method with the "Roles" argument to target the Roles field in the User model.
The chain of db.Model() and Association() allows GORM to accurately retrieve the related Role records for the specified User. Finally, we chain Find(&roles) to fetch the associated Role records from the database and store them in the roles slice.
Conclusion
Throughout this topic, you've acquired valuable knowledge about declaring relationships between models. In specific, you've learned the following concepts:
- Understand and work with relationships between models, such as one-to-one, one-to-many, and many-to-many relationships;
- Leverage the
db.Model()method andAssociation()method to efficiently work with related models and their data.
With this solid foundation of relationships between models, you can dive deeper into GORM and explore other essential features like Migrations, CRUD operations, advanced querying, and more. Keep practicing and building on your understanding, and remember to consult the official GORM documentation when needed.
Now, it's time to apply your new knowledge to some theory and coding tasks; let's go!