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
Preload
andJoins
for eager loading - Transactions, nested transactions, Save Point, Rollback To Saved Point
- Context, prepared statement mode, DryRun mode
- Bulk insert, FindInBatches, Find/Create with Map, CRUD using SQL expressions, 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 (multiple databases, 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 data in two tables. A foreign key usually points to the primary key of another table, and its main functions are:
- Relationship: Associates records in one table with records in 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, representing a one-to-one association.
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
. Therefore, it will automatically point the ID
of Company
to the User.CompanyID
field, 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 ModelName + ID
.
Why is there also a field of type Company
? Because when querying User
data, we can use GORM's eager loading 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
, where Company
is the type name and ID
is the primary key field name.
However, we can use some tags
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
, it uses the primary key ID
of Company
as the foreign key.
Similarly, we can use tags
to specify, for example:
type User struct {
gorm.Model
Name string
CompanyID string
Company Company `gorm:"references:Code"` // Use Code as the 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 usually mistakenly interpret it as 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
with 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 one model "owns" an instance of another model.
This 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 a 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: The foreign key is always placed on the "referencing" or "dependent" side.
When User Belongs to Company
:User
is the dependent side, meaningUser
belongs toCompany
, so the foreign keyCompanyID
is in theUser
table.When User Has One Company
:Company
is the owned side, which can also be understood asCompany
belongs toUser
, so the foreign keyUserID
is placed in theCompany
.
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 from the Has One
model type + primary key
, which for the Company
example is UserID
.
However, we can also use tags
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, UserName
of CreditCard
is specified 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 to 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, such a relationship is defined, indicating the relationship between a user (employee) and their manager (also a user). This model definition can serve as a data model for an affiliate referral system, where users can enter referral codes upon 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, which is different from Has One
. Has Many
indicates that one can have multiple, meaning one owns 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 is needed.
Overriding Reference#
Using references
, no further demonstration is needed.
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 Language
, and one Language
can belong to multiple User
.
// User has and belongs to many 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, for example between User
and Language
, we can query the Language
used by this User
, and we can also query which User
uses 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 many 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 example of reverse reference, where both point to the same join table user_languages
and define the association.
We can easily use eager loading to load associated data, for example:
// Retrieve the list of Users and preload Languages (forward)
func GetAllUsers(db *gorm.DB) ([]User, error) {
var users []User
err := db.Model(&User{}).Preload("Languages").Find(&users).Error
return users, err
}
// Retrieve the list of Languages and preload Users (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 overriding foreign keys because this relationship requires a join table, which must contain at least two foreign keys, one for each model. For the example above, 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, of course, you can choose to rewrite only one of them based 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, many2many
specifies the ManyToMany
relationship and the name of the join table. Then, foreignKey
specifies the field in the User
model that is associated with the user_profiles
table as Refer
, and JoinForeignKey
specifies the foreign key name as UserReferID
. References
specifies the field in the Profile
model that is associated with the user_profiles
table as UserRefer
, and JoinReferences
specifies the foreign key name as ProfilesRefer
.
So the final table will be:
// Join table created: user_profiles
// foreign key: user_refer_id, reference: users.refer
// foreign key: profile_refer, reference: profiles.user_refer
The tags
involved here are quite complex, so I will explain them 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)
- The foreign key directly exists in one of the models participating in the relationship.
- In this case,
foreignKey
refers 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.
references
refers to the field in the referenced model that theforeignKey
points to.For example: ID of User
- Indirect Relationships (Many To Many)
- The foreign keys reside in an independent join table, connecting the associated relationships of the two models.
- Here, the
tags
are more about configuring how the join table relates to the two models. - For
Many To Many
, I believe the relationship between the owning side and the owned side 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.foreignKey
refers to the field name in Model A that is referenced by the join table's foreign key.joinForeignKey
refers to the foreign key name in the join table pointing to Model A.references
refers to the field name in Model B that is referenced by the join table's foreign key.joinReferences
refers to the foreign key name in the join table pointing to Model B.
With these concepts in mind, the above examples become much easier to understand, where Model A can refer to User
, and 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 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 the join table PersonAddress
// 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 may cover association patterns.