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:
I tried searching online for any ideas but cannot seem to find any.
Any help/ideas are appriciated!
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.
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.
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.
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.
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!