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
  • 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.

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, 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 that 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 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 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:

  1. 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
    • references refers to the field in the referenced model that foreignKey points to.
      • For example: ID of User
  2. 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.
      • foreignKey refers to the field name in Model A that is referenced by the join table 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 foreign key
      • joinReferences 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.

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