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:
- the involved table
- the involved association of the previous table
- how do you want to name the foreign key constraint
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!