FanXing Blog

FanXing Blog

一个热爱编程的高中生,正在努力成为一位优秀的后端工程师~

Day 3. The Path to Mastery in Go Language: GORM Foreign Keys

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 and Joins 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.

image

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.

image

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, meaning User belongs to Company, so the foreign key CompanyID is in the User table.
  • When User Has One Company: Company is the owned side, which can also be understood as Company belongs to User, so the foreign key UserID is placed in the Company.

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:

  1. 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
    • references refers to the field in the referenced model that the foreignKey points to.
      • For example: ID of User
  2. 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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.