Today I learned about foreign keys in GORM.
About GORM#
GORM is an ORM library in Go that I find very useful and developer-friendly. It has the following features:
- Full-featured ORM
- Associations (Has One, Has Many, Belongs To, Many To Many, polymorphic, single table inheritance)
- Hook methods in Create, Save, Update, Delete, Find
- Support for
PreloadandJoins - Transactions, nested transactions, Save Point, Rollback To Saved Point
- Context, pre-compiled mode, DryRun mode
- Batch inserts, FindInBatches, Find/Create with Map, CRUD using SQL expressions and Context Valuer
- SQL builder, Upsert, database locks, Optimizer/Index/Comment Hint, named parameters, subqueries
- Composite primary keys, indexes, constraints
- Auto Migration
- Custom Logger
- Flexible extensible plugin API: Database Resolver (multi-database, read-write separation), Prometheus…
- Each feature has been rigorously tested
- Developer-friendly
Foreign Key#
In relational databases, a foreign key (is a) or a set of fields used to establish a connection between the data of two tables. A foreign key usually points to the primary key of another table, and its main functions are:
- Relationship: Associates records from one table with records from another table.
- Data Integrity: Ensures that the referenced data is valid. For example: you cannot create an order for a user that does not exist, meaning the user corresponding to the order does not exist in the database.
GORM follows the principle of "convention over configuration," allowing it to automatically determine the corresponding foreign key relationships.
Note: Some databases do not automatically create indexes for foreign keys.
Belongs To#
Belongs To establishes a one-to-one association with another model, where each instance of this model "belongs to" an instance of another model, which is a one-to-one association counterpart.
For example: an employee belongs to one company.
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}
type Company struct {
ID int
Name string
}
Here, User and Company establish a Belongs To relationship. Each User can only be assigned to one Company. In the User struct, CompanyID is used to store the ID of the corresponding Company.
GORM is smart; when we have a field of type Company in the struct and also a CompanyID field, GORM will automatically infer that the developer wants to establish a Belongs To relationship between User and Company, so it will automatically point the Company's ID to the User.CompanyID field, thus establishing a foreign key relationship, which is GORM's convention.
The CompanyID field must be present in the User struct for GORM to recognize and infer it; without this field, GORM cannot link the records to the records in the Company table. The field name is also a convention and must be in the format of ModelName + ID.
Why is there also a field of type Company? Because when querying User data, you can use GORM's preloading feature to load the corresponding Company information into the User struct. GORM will query the corresponding data in the Company table using the CompanyID field.
Overriding Foreign Key#
To define a foreign key for a Belongs to relationship, by default, the foreign key field name is: owner's type name + table's primary key field name, meaning Company is the type name, and ID is the primary key field name.
However, we can use some tag to specify the foreign key name, for example:
type User struct {
gorm.Model
Name string
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"`
// Use CompanyRefer as the foreign key
}
type Company struct {
ID int
Name string
}
Overriding Reference#
In Belongs to, GORM typically uses the primary key of the data table as the foreign key reference, as in the example above with User and Company, which uses Company's primary key ID as the foreign key.
Similarly, you can also use tag to specify, for example:
type User struct {
gorm.Model
Name string
CompanyID string
Company Company `gorm:"references:Code"` // Use Code as reference
// This way, GORM will automatically fill Company.Code into CompanyID as the foreign key
}
type Company struct {
ID int
Code string
Name string
}
However, if the foreign key name happens to exist in the owner type, GORM will typically mistakenly assume it is a Has One relationship, so it needs to be specified manually, for example:
type User struct {
gorm.Model
Name string
CompanyID int
Company Company `gorm:"references:CompanyID"` // use Company.CompanyID as references
}
type Company struct {
CompanyID int
Code string
Name string
}
Foreign Key Constraints#
We can also configure foreign key constraints using constraint for OnUpdate and OnDelete, for example:
type User struct {
gorm.Model
Name string
CompanyID int
Company Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type Company struct {
ID int
Name string
}
Has One#
Has One establishes a one-to-one relationship with another model, but it is different from Belongs to. Has One indicates that each instance of a model "owns" one instance of another model.
It can be understood as follows:
// Belongs to
// User belongs to Company
// Here, User belongs to Company, meaning the user is an employee of this company
type User struct {
gorm.Model
Name string
CompanyID int // Foreign key: stores the ID of the associated Company
Company Company // Belongs To relationship declaration
}
type Company struct {
ID int // Primary key of Company
Name string
}
// Has One
// User owns one Company record
// Here, User owns a Company, meaning this user has founded a company
type User struct {
gorm.Model
Name string
Company Company // Has One relationship declaration
}
type Company struct {
ID int // Primary key of Company
Name string
UserID uint // Foreign key: stores the ID of the associated User
}
Core rule: Foreign keys are always placed on the "referencing" or "dependent" side.
When User Belongs to Company:Useris the dependent side, expressing thatUserbelongs toCompany, so the foreign keyCompanyIDis in theUsertable.When User Has One Company:Companyis the owned side, which can also be understood asCompanybelongs toUser, so the foreign keyUserIDis placed inCompany.
Based on my current understanding, I believe the difference between Has One and Belongs to is merely semantic; both are essentially one-to-one relationships. However, to express different relationships more clearly and facilitate developers in defining different models, the terms "contains" and "belongs" are semantically distinguished.
Overriding Foreign Key#
Since both are one-to-one relationships, Has One and Belongs to must also have a foreign key field. By default, it is generated by the Has One model type + primary key, which for the Company example is UserID.
However, we can also use tag to specify another field as the foreign key:
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"foreignKey:UserName"` // Use UserName as the foreign key
}
type CreditCard struct {
gorm.Model
Number string
UserName string
}
Here, we specify the UserName field of CreditCard as the foreign key.
Overriding Reference#
We can use foreignKey to specify the foreign key, which is the field in the corresponding model that serves as the foreign key. Of course, we can also specify the reference in the current model, which is the field filled into the foreign key model:
type User struct {
gorm.Model
Name string `gorm:"index"`
CreditCard CreditCard `gorm:"foreignKey:UserName;references:Name"`
}
type CreditCard struct {
gorm.Model
Number string
UserName string
}
Here, we use UserName of CreditCard as the foreign key, and the Name field of User is filled into UserName, binding User.Name with CreditCard.UserName.
Self-referencing Has One#
We often encounter scenarios where we need to associate data from the same table with another record in the same table; this is self-referencing. When this relationship is one-to-one, we can call it: self-referencing Has One.
An example is as follows:
type User struct {
gorm.Model
Name string
ManagerID *uint
Manager *User
}
In this case, we define such a relationship, indicating the relationship between a user (employee) and their manager (also a user). This model definition can serve as a data model for AFF referral commissions, where users can write referral codes during registration, corresponding to their superiors, and then bind to their superiors through self-referencing, ultimately achieving profit sharing and hierarchical binding.
Has Many#
Has Many establishes a one-to-many relationship, unlike Has One, Has Many indicates having multiple, meaning one has many. For example:
// User has multiple CreditCards, UserID is the foreign key
type User struct {
gorm.Model
CreditCards []CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
Here is a classic Has Many relationship, where one User can have multiple CreditCards.
Overriding Foreign Key#
Still using foreignKey, no further demonstration.
Overriding Reference#
Using references, no further demonstration.
Self-referencing Has Many#
Self-referencing Has Many is similar to self-referencing Has One, which creates associations between different records in the same data table. The difference is that Has Many indicates that one record can associate with multiple other records of the same type.
For example:
type User struct {
gorm.Model
Name string
ManagerID *uint
Team []User `gorm:"foreignkey:ManagerID"`
}
This example shows that a user management layer (User) can have multiple subordinates (User).
The difference from self-referencing Has One is that Has One focuses more on finding the user's superior, while Has Many focuses more on finding the user's subordinates.
Many To Many#
Many To Many relationships create a join table between two tables, which is a solution for handling many-to-many relationships.
For example, in our product, there are Language and User, where one User can have multiple Languages, and one Language can belong to multiple Users.
// User has and belongs to multiple languages, `user_languages` is the join table
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
}
Here, a user_languages join table will be created to handle the many-to-many relationship between Language and User. GORM will automatically create this join table.
Reverse Reference#
Reverse reference is not a technical term, but this term can well describe the bidirectionality of Many To Many. When we define a many-to-many relationship, such as between User and Language, we can query the Language used by this User, and we can also query which Users use that language. This ability to "query in reverse," loading data from the "other end," is called reverse reference.
For example:
// User has and belongs to multiple languages, `user_languages` is the join table
type User struct {
gorm.Model
Languages []*Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
Users []*User `gorm:"many2many:user_languages;"`
}
This is a classic reverse reference case, where both point to the same join table user_languages and define association relationships.
We can easily use preloading to load associated data, for example:
// Retrieve User list and preload Language (forward)
func GetAllUsers(db *gorm.DB) ([]User, error) {
var users []User
err := db.Model(&User{}).Preload("Languages").Find(&users).Error
return users, err
}
// Retrieve Language list and preload User (reverse)
func GetAllLanguages(db *gorm.DB) ([]Language, error) {
var languages []Language
err := db.Model(&Language{}).Preload("Users").Find(&languages).Error
return languages, err
}
The reverse reference feature also illustrates that the Many To Many relationship in GORM is symmetric. By using Many To Many between two models and pointing to the same join table, GORM gains the ability to query associated data from either side.
Overriding Foreign Key#
For Many To Many, overriding foreign keys is different from general foreign key overrides because this relationship requires a join table, which must contain at least two foreign keys, one for each model. For example, in the above case, the user_languages join table will have the following two foreign keys:
// Join table: user_languages
// foreign key: user_id, reference: users.id
// foreign key: language_id, reference: languages.id
If you need to override, you must rewrite the reference and foreignkey for both foreign keys, or you can choose to rewrite only one of them, depending on your needs.
type User struct {
gorm.Model
Profiles []Profile `gorm:"many2many:user_profiles;foreignKey:Refer;joinForeignKey:UserReferID;References:UserRefer;joinReferences:ProfileRefer"`
Refer uint `gorm:"index:,unique"`
}
type Profile struct {
gorm.Model
Name string
UserRefer uint `gorm:"index:,unique"`
}
Here, we use many2many to specify the ManyToMany relationship and the join table name. Then we use foreignKey to specify the field in the User model associated with the user_profiles table as Refer, and use JoinForeignKey to specify the foreign key name as UserReferID. We use References to specify the field in the Profile model associated with the user_profiles table as UserRefer, and use JoinReferences to specify the foreign key name as ProfilesRefer.
So the final table will be:
// Join table will be created: user_profiles
// foreign key: user_refer_id, reference: users.refer
// foreign key: profile_refer, reference: profiles.user_refer
The tag involved here is quite complex, so I will explain it in detail.
In GORM, several tags are provided to support the functionality of overriding foreign keys, but these tags have subtle differences in meaning across different relationships, which we can understand from direct and indirect perspectives:
- Direct relationships (Has One, Belongs To, Has Many)
- Foreign keys exist directly in one of the models participating in the relationship
- In this case,
foreignKeyrefers to the foreign key field name in the model holding the foreign key- Has One: refers to the foreign key field name of the owned model
For example: UserID of Company
- Belongs To: refers to the foreign key field name of the owning model
For example: CompanyRefer of User
- Has One: refers to the foreign key field name of the owned model
referencesrefers to the field in the referenced model thatforeignKeypoints to.For example: ID of User
- Indirect relationships (Many To Many)
- Foreign keys exist in an independent join table, connecting the association between the two models
- The
tags here are more used to configure how the join table relates to the two models - For
Many To Many, I believe the relationship between the owner and the owned is quite ambiguous, so instead of using these terms to refer to the two models, we adopt the method of Model A and Model B.foreignKeyrefers to the field name in Model A that is referenced by the join table foreign keyjoinForeignKeyrefers to the foreign key name in the join table pointing to Model Areferencesrefers to the field name in Model B that is referenced by the join table foreign keyjoinReferencesrefers to the foreign key name in the join table pointing to Model B
With these concepts in mind, looking back at the above case becomes much clearer; Model A can refer to User, while Model B can refer to Profile.
Self-referencing Many2Many#
Many To Many relationships refer to a model establishing a many-to-many relationship with itself. This feature is most commonly used in friend functionalities, where a user can have multiple friends and can be friends with many individuals.
type User struct {
gorm.Model
Friends []*User `gorm:"many2many:user_friends"`
}
// Join table will be created: user_friends
// foreign key: user_id, reference: users.id
// foreign key: friend_id, reference: users.id
Custom Join Table#
A join table can be a fully functional model, supporting features like soft delete, hook functions, etc., and can have more fields. We can set this up using the SetupJoinTable function, for example:
Custom join tables require foreign keys to be composite primary keys or composite unique indexes.
type Person struct {
ID int
Name string
Addresses []Address `gorm:"many2many:person_addresses;"`
}
type Address struct {
ID uint
Name string
}
type PersonAddress struct {
PersonID int `gorm:"primaryKey"`
AddressID int `gorm:"primaryKey"`
CreatedAt time.Time
DeletedAt gorm.DeletedAt
}
func (PersonAddress) BeforeCreate(db *gorm.DB) error {
// ...
}
// Modify the Addresses field of Person to use PersonAddress as the join table
// PersonAddress must define the required foreign keys, otherwise an error will occur
err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})
Summary#
No summary, actually wanted to write something, but the technology is too powerful too useless.
The next issue might cover association patterns.