gogo-gormgolang-migrate

Is it possible to auto migrate tables with circular relationships in GORM?


I Been trying to implement the GORM Orm in our Golang project but it seems that I have a slight problem.

one of the structs has a circular dependency, so now when I try to AutoMigrate for the tables creation, I get errors since GORM is trying to create the tables by order.

example proto:

message Person{
  optional string name= 1;
  optional Company company = 2;
}

message Company{
  optional string name= 1;
  optional Workers workers= 2;
}

message Workers {
  optional string name= 1;
  optional Person person= 2;
}

this is a simplyfied example but is exactly how my circular dependency is. When I generate the proto using the gorm plugin, it generates the models with all the gorm annotations including the foregin keys. and then ofcourse it breaks when I try to autoMigrate them all.

the only way I found to solve this is:

  1. remove the Workers field from Company.
  2. generate the gorm models.
  3. run autoMigrate.
  4. refactor the proto file and return the Workers field back to Company.
  5. run autoMigrate.
  6. we have all tables with the appropriate FK's.

I tried searching online for any ideas but cannot seem to find any.

Any help/ideas are appriciated!


Solution

  • I was able to achieve what you need by following this approach. Unfortunately, I'm not too familiar with proto messages so I share only the relative Go code you should use. If I'm not wrong the association you defined in the proto message is translated into belongsTo within GORM. Otherwise, you should have used the repeated keyword (am I right?).
    After the premise, I'm gonna share the code and, then, the explanation.

    package main
    
    import (
        "github.com/samber/lo"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
    )
    
    type Person struct {
        ID        int
        Name      string
        CompanyID *int
        Company   *Company
    }
    
    func (p Person) TableName() string {
        return "people"
    }
    
    type Company struct {
        ID       int
        Name     string
        WorkerID *int
        Worker   *Worker
    }
    
    type Worker struct {
        ID       int
        Name     string
        PersonID *int
        Person   *Person
    }
    
    func main() {
        dsn := "host=localhost port=54322 user=postgres password=postgres dbname=postgres sslmode=disable"
        db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
            DisableForeignKeyConstraintWhenMigrating: true,
        })
        if err != nil {
            panic(err)
        }
    
        db.AutoMigrate(&Person{}, &Company{}, &Worker{})
        db.Migrator().CreateConstraint(&Company{}, "Worker")
        db.Migrator().CreateConstraint(&Company{}, "fk_companies_people")
        db.Migrator().CreateConstraint(&Person{}, "Company")
        db.Migrator().CreateConstraint(&Person{}, "fk_people_companies")
        db.Migrator().CreateConstraint(&Worker{}, "Person")
        db.Migrator().CreateConstraint(&Worker{}, "fk_workers_people")
    
        db.Create(&Person{ID: 1, Name: "John", Company: &Company{ID: 1, Name: "ACME", Worker: &Worker{ID: 1, Name: "Worker 1"}}})
        db.Model(&Person{ID: 1}).Update("company_id", lo.ToPtr(1))
        db.Model(&Company{ID: 1}).Update("worker_id", lo.ToPtr(1))
        db.Model(&Worker{ID: 1}).Update("person_id", lo.ToPtr(1))
    
        // WRONG section!!!!!! uncomment any of these to try
        // db.Model(&Worker{ID: 1}).Update("person_id", lo.ToPtr(2)) // id "2" breaks as it doesn't exist
        // db.Model(&Person{ID: 1}).Update("company_id", lo.ToPtr(2)) // id "2" breaks as it doesn't exist
        // db.Model(&Company{ID: 1}).Update("worker_id", lo.ToPtr(2)) // id "2" breaks as it doesn't exist
    }
    

    Alright, let me walk you through the relevant sections.

    The structs definition

    Here, you must forecast that every association could be NULL. That's why I used pointers to define all of them. Thanks to this, you can create a circular dependency like this:

    Person => Company => Worker => Person => ....

    Plus, I overrode the table name for the struct Person by setting people. Probably, GORM is smart enough to do this by itself but never tried it.

    SQL objects definition

    When you instantiate a gorm client, you've to be sure that the Foreign Keys don't get created when you migrate. To achieve this, you've to set the field DisableForeignKeyConstraintWhenMigrating to true in the gorm.Config struct. Thanks to this, the foreign keys creation is up to you. The latter is done through the CreateConstraint method in which you specify:

    Lastly, you can notice that I run the AutoMigrate method to create the tables without the foreign keys.

    The writing logic

    Due to the layout of the tables, the INSERT logic must be divided into two parts. In the first one, you insert the records in their own table (e.g. Person into the people table, Company into companies, and so on). We have deliberately left the foreign keys to NULL, otherwise we got an error. The first will always raise an error if the related record hasn't been inserted yet.
    Then, we set each foreign key to the right value by using the Update method.

    Final thoughts

    I left in the code some commented statements to prove that if you try to assign some not-existent value as the foreign key, it breaks. That means you're allowed to either insert NULL or a right value in these columns.
    I used this package "github.com/samber/lo" to easily get a pointer value starting from a literal (e.g. 1).

    Let me know if this helps solve your issue, thanks!