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
- 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
:User
is the dependent side, expressing thatUser
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 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 CreditCard
s.
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 Language
s, and one Language
can belong to multiple User
s.
// 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 User
s 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 tag
s are provided to support the functionality of overriding foreign keys, but these tag
s 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,
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 thatforeignKey
points 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
tag
s 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.foreignKey
refers to the field name in Model A that is referenced by the join table foreign keyjoinForeignKey
refers to the foreign key name in the join table pointing to Model Areferences
refers to the field name in Model B that is referenced by the join table foreign keyjoinReferences
refers 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.