Today I learned about GORM's association patterns, actually this was supposed to be learned yesterday, but I was delayed too long by beyond Many To Many
, so I moved it to today.
Polymorphism#
How come there's still this thing I haven't learned, in GORM, the feature of polymorphic associations is very powerful; it allows a model to belong to multiple different parent models without needing to create separate foreign key fields for each parent model.
For example, in the case of Toy
, Cat
, and Dog
, this capability is implemented based on two fields in Toy
:
- Parent model's primary key ID
- Parent model's type
GORM supports polymorphic associations for Has One
and Has Many
relationships.
Default Conventions#
When we use polymorphic associations, there are some default conventions:
- Type Field: GORM automatically creates a field in the child model table to store the parent model's type. By default, the field name is the value specified by
polymorphic
plusType
. - ID Field: GORM automatically creates a field in the child model table to store the parent model's primary key ID. By default, the field name is the value specified by
polymorphic
plusID
. - Type Value: The value stored in the type field, which defaults to the plural form of the parent model's corresponding data table.
// Define Dog model
type Dog struct {
ID int
Name string
// Dog has multiple Toys, using polymorphic association
// gorm:"polymorphic:Owner;" specifies the prefix for polymorphic association as "Owner"
Toys []Toy `gorm:"polymorphic:Owner;"`
}
// Define Toy model
type Toy struct {
ID int
Name string
// OwnerID stores the ID of the parent model (Dog)
OwnerID int
// OwnerType stores the type of the parent model (Dog), default is "dogs"
OwnerType string
}
// Create record
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
In this case, OwnerType
will be set to dogs
.
Custom Tags#
Of course, we can also use some tags
to customize our polymorphic associations:
polymorphicType
: Specifies the field name that stores the type.polymorphicId
: Specifies the field name that stores the ID.polymorphicValue
: Specifies the specific value stored in the type field.
type Dog struct {
ID int
Name string
// Custom polymorphic association:
// polymorphicType:Kind -> type field name is Kind
// polymorphicId:OwnerID -> ID field name is OwnerID
// polymorphicValue:master -> type value is "master"
Toys []Toy `gorm:"polymorphicType:Kind;polymorphicId:OwnerID;polymorphicValue:master"`
}
// Define Toy model
type Toy struct {
ID int
Name string
// OwnerID stores the ID of the parent model (Dog) (specified by polymorphicId)
OwnerID int
// Kind stores the type of the parent model (Dog) (specified by polymorphicType), value is "master" (specified by polymorphicValue)
Kind string
}
// Create record
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
Association Patterns#
GORM provides powerful features to handle associations between database models, making development efficient.
Automatic Creation and Update#
GORM automatically references or updates its associated data upon creation. For example:
type User struct {
gorm.Model
Languages []Language
}
type Language struct {
gorm.Model
Name string
UserID uint
}
user := User{
Languages: []Language{
{Name: "ZH"},
{Name: "EN"},
},
}
// Create user and its associated languages
db.Create(&user)
db.Save(&user)
With such operations, GORM will automatically create Language
and bind it to User
with a Has Many
foreign key relationship.
FullSaveAssociations#
FullSaveAssociations
is a parameter provided by GORM for Session, which can force GORM to fully save or update all associated records, not just handle relationships.
When we do not use the FullSaveAssociations
parameter, or say FullSaveAssociations:false
, the default GORM will not handle changes to the associated models, but will only check their relationships (i.e., check if the foreign key IDs are linked correctly). For example:
We now have the following models:
type User struct {
gorm.Model
Name string
Age int
BillingAddress Address // Has One relationship
BillingAddressID uint
}
type Address struct {
gorm.Model
Address1 string
Address2 string
PostCode string
}
We execute the following operations:
// Query user and its associated address
var user User
db.Preload("BillingAddress").First(&user, 1)
// user.ID = 1
// user.Name = "jinzhu"
// user.BillingAddressID = 10
// user.BillingAddress.ID = 10
// user.BillingAddress.Address1 = "Old Billing St"
// user.BillingAddress.Address2 = "Unit 1"
// user.BillingAddress.PostCode = "10000"
// Modify user and address information
user.Name = "jinzhu_updated"
user.BillingAddress.Address1 = "New Billing Avenue" // Modified field of associated object
user.BillingAddress.PostCode = "20000" // Modified field of associated object
db.Save(&user)
// or db.Updates(&user)
Without using FullSaveAssociations:true
, the information of BillingAddress
will not be updated, meaning that modifications to the BillingAddress
model on the user
model are ineffective. After using FullSaveAssociations:true
, executing db.Save(&user)
will update BillingAddress
.
In other words, the FullSaveAssociations
tag provides a more comprehensive data synchronization method, allowing modifications to associated models (whether one-to-one, one-to-many, or many-to-many) in Go code to be persisted to the database.
Skipping Automatic Creation and Update#
GORM automatically references and creates or updates associated object data upon creation (if there are associated relationships), but GORM also provides flexibility, using Select
or Omit
methods to specify which fields can be deleted or included.
Using Select to Specify Field Range#
The Select
method can choose which fields in the model should be saved, meaning only the selected fields will be included in the SQL.
var user User
user = User{
Name: "gopher",
Age: 21
}
db.Select("Name").Create(&user)
Here, at the database level, only the name
field will be populated for user
, while the age
field we defined will be ignored.
Using Omit to Exclude Fields or Associations#
Omit
allows us to specify which fields or associations to skip during create or update operations, providing more granular control over which data will be persisted.
There are several usages:
- Exclude specified fields
// Skip the "BillingAddress" field when creating user
db.Omit("BillingAddress").Create(&user)
- Exclude all associations
// Skip all associations when creating user
db.Omit(clause.Associations).Create(&user)
- Exclude many-to-many relationships
// Skip updating "Languages" association
db.Omit("Languages.*").Create(&user)
This usage will still maintain the many-to-many association between user
and language
, but will skip updating language
. That is, if language
does not exist, it will still be created and associated; however, if language
already exists, it will not be updated.
- Exclude creating associations and references
// Skip creating 'Languages' association and its references
db.Omit("Languages").Create(&user)
This will completely ignore Language
, and when creating user
, neither the association will be created nor will the association relationship be processed in the join table.
Select/Omit Associated Fields#
When creating or updating records in GORM, you can use Select
and Omit
to specify whether to include a certain associated field. For example:
- Select
// Create user and his billing address, mailing address, only include specified fields of billing address
db.Select("BillingAddress.Address1", "BillingAddress.Address2").Create(&user)
// SQL: only use Address1 and Address2 to create user and billing address
This line of code will normally handle the creation of user
, but when creating BillingAddress
, only Address1
and Address2
will be included, and other fields will not be included in the SQL for the address
table.
- Omit
// Create user and billing address, mailing address, but do not include specified fields of billing address
db.Omit("BillingAddress.Address2", "BillingAddress.CreatedAt").Create(&user)
// SQL: create user and billing address, omit 'Address2' and creation time fields
Similarly, it will normally handle the creation of user
, but when handling BillingAddress
, all fields will be included except for Address2
and CreatedAt
.
In other words, Select
specifies which to process, while Omit
specifies which to exclude.
Deleting Associations#
GORM provides a mechanism that allows us to delete associated records (one-to-one, one-to-many, many-to-many) when deleting the main model record, which is useful for maintaining data integrity and can clean up useless data.
Use Select
to specify which associations to delete:
- Delete a single association
// When deleting user, also delete that user's Account record
db.Select("Account").Delete(&user)
- Delete multiple associations
// When deleting user, also delete that user's Orders and CreditCards records
db.Select("Orders", "CreditCards").Delete(&user)
- Delete all associations
// When deleting user, delete all has one, has many, many2many type associated records of that user
db.Select(clause.Associations).Delete(&user)
- Batch delete associations
// When deleting each user in the users slice, also delete their respective Account records
db.Select("Account").Delete(&users) // users is a []*User or []User slice
This makes deletion very quick, but there is an important prerequisite: the primary key must be non-zero; GORM needs to use the primary key ID as a condition to find and delete related records.
Association Patterns#
GORM provides a series of convenient methods to handle relationships between models, allowing developers to efficiently manage data.
To start using association patterns, we need to enable association patterns, which means specifying the source model and the field names of the association relationship:
var user User // Assume User is your model and has an associated field named Languages
// Start association mode, operate on the Languages association of the user object
db.Model(&user).Association("Languages")
// You can also check for errors when starting association mode
err := db.Model(&user).Association("Languages").Error
if err != nil {
// Handle error
}
The name filled in Association is not the name of the associated model, but the field name in the main model that contains the association relationship.
Querying Associations#
Using Find
can retrieve associated records and also selectively add query conditions.
var languages []Language // Assume Language is the associated model
// Simple query: Get all Language records associated with user
db.Model(&user).Association("Languages").Find(&languages)
// Conditional query: Get Language records associated with user, but only include specified codes
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)
// Note: When using Where conditions, they will apply to the associated model's query
Here, the where
condition is applied to the associated model; if we need conditions on the user
model, for example, if I want to find Language
where user.id = 1
, how should I handle it? The code is as follows:
// Here I think we can also directly create a user type model, changing id to 1
// That is:
// user := User{ID: 1}
var user User
result := db.First(&user, 1) // Find User with primary key 1
if result.Error != nil {
// Handle error, e.g., user not found
panic("failed to find user with id 1")
}
var languages []Language
// Now the user variable represents the user with id 1
// GORM will automatically use user.ID to filter languages
db.Model(&user).Association("Languages").Find(&languages)
// If you also want to add filtering conditions on code, you can chain call as before:
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)
Appending Associations#
The Append
method is used to add new associations. The effect of Append
varies in different association relationships:
- For many-to-many relationships and one-to-many relationships,
Append
will add new associated records. - For one-to-one relationships (Has One) and belongs to relationships,
Append
will replace the current association.
// Append one or more existing Language objects
db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})
// Append a new Language object (if Language does not exist, GORM may attempt to create it, depending on your settings)
db.Model(&user).Association("Languages").Append(&Language{Name: "DE"})
// Append/replace CreditCard (assuming it's a has one or belongs to relationship)
db.Model(&user).Association("CreditCard").Append(&CreditCard{Number: "411111111111"})
When Languages
does not exist, it depends on the settings to decide whether to create it, which is essentially controlled by the FullSaveAssociations
tag, which defaults to false
, meaning it will not create.
Replacing Associations#
Replacing associations will replace all current associations of the main model with new associated records, discarding the original associations and replacing them with new ones.
// Replace user’s Languages association with languageZH and languageEN
db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})
// You can also pass multiple independent objects
db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN)
Deleting Associations#
The Delete
method here is used to remove the association between the main model and the specified parameter model, which only deletes their references, not the associated objects themselves.
// Delete the association between user and languageZH and languageEN
db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN})
// You can also pass multiple independent objects
db.Model(&user).Association("Languages").Delete(languageZH, languageEN)
Clearing Associations#
Clearing associations will remove all associations between the main model and that model.
// Clear all Languages associations of user
db.Model(&user).Association("Languages").Clear()
Counting Associations#
Association counting can be used to get the number of current associated records, and can also count with conditions:
// Get the number of all Languages associated with user
count := db.Model(&user).Association("Languages").Count()
// Conditional counting: Get the number of Languages associated with user where code is in the specified list
codes := []string{"zh-CN", "en-US", "ja-JP"}
countWithCondition := db.Model(&user).Where("code IN ?", codes).Association("Languages").Count()
Batch Data Processing#
Association patterns also support batch operations on multiple records, including querying, appending, replacing, deleting, and counting associated data.
Batch Querying Associations#
var users []User
var roles []Role
// Assume users is a slice of User, query all Roles associated with these users
db.Model(&users).Association("Role").Find(&roles)
Batch Deleting Associations#
var users []User
// Assume users is a slice of User, delete these users' association with userA's Team
db.Model(&users).Association("Team").Delete(&userA)
Batch Counting Associations#
var users []User
// Assume users is a slice of User, count the total number of Teams associated with these users (or count based on specific relationships)
count := db.Model(&users).Association("Team").Count()
Batch Appending/Replacing Associations#
var users = []User{user1, user2, user3} // Assume there are three users
// Batch append:
// - Append userA to user1's Team
// - Append userB to user2's Team
// - Append userA, userB, userC to user3's Team
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// Batch replace:
// - Replace user1's Team with userA
// - Replace user2's Team with userB
// - Replace user3's Team with userA, userB, userC
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})
Deleting Associated Content#
When using the Replace
, Delete
, and Clear
methods, it is very important to understand how they handle associated records by default, as this relates to data integrity.
Default Behavior: Only Update References#
By default, these methods mainly affect foreign key references or join table records, not the records themselves.
- Update References: These methods typically set the associated foreign keys to
NULL
(forbelongs to
,has one
,has many
) or delete the corresponding rows in the join table, thereby disconnecting the main model from the associated model. - Do Not Physically Delete Records: The actual records in the associated data table will not be deleted; these records still exist in the database, just not associated with the main model.
Using Unscoped
to Change Deletion Behavior#
If you want to disconnect the association while actually deleting the associated records, you need to use the Unscoped
method.
- Soft Delete Associated Records
If your associated model has soft delete functionality configured, you can use Unscoped()
to trigger a soft delete on associated records.
- Behavior: This will set the
deleted_at
field in the associated records to the current time, marking them as deleted, but the records still exist in the database.
// Assume Language model supports soft delete (has gorm.DeletedAt field)
// Clear associations and soft delete all Language records associated with user
db.Model(&user).Association("Languages").Unscoped().Clear()
// Also applies to Delete and Replace
// db.Model(&user).Association("Languages").Unscoped().Delete(&languageZH)
- Physically Delete Associated Records
If we want to completely physically delete associated records from the database, you need to use two Unscoped()
together:
- The first
Unscoped()
applies to thedb
context, indicating GORM to ignore soft delete logic in subsequent operations and perform deletion logic. - The second
Unscoped()
applies to theAssociation
chain, ensuring physical deletion is also used for associated records.
// Physical delete:
// 1. db.Unscoped() indicates GORM to ignore global soft delete settings and perform physical deletion.
// 2. Association("Languages").Unscoped() ensures physical deletion is performed on associated records.
// Clear associations and physically delete all Language records associated with user from the database
db.Unscoped().Model(&user).Association("Languages").Unscoped().Clear()
// Also applies to Delete
// db.Unscoped().Model(&user).Association("Languages").Unscoped().Delete(&languageZH)