英文:
Is it possible to auto migrate tables with circular relationships in GORM?
问题
我一直在尝试在我们的Golang项目中实现GORM ORM,但似乎遇到了一个小问题。
其中一个结构体存在循环依赖,所以当我尝试使用AutoMigrate创建表时,会出现错误,因为GORM试图按顺序创建表。
以下是一个示例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;
}
这只是一个简化的示例,但正是我的循环依赖问题。当我使用gorm插件生成proto时,它会生成带有所有gorm注释的模型,包括外键。然后当我尝试自动迁移它们时,当然会出错。
我找到的唯一解决方法是:
- 从Company中删除Workers字段。
- 生成gorm模型。
- 运行autoMigrate。
- 重构proto文件,并将Workers字段返回到Company中。
- 运行autoMigrate。
- 我们将得到具有适当外键的所有表。
我尝试在网上搜索任何想法,但似乎找不到任何解决方案。
欢迎提供任何帮助/想法!
英文:
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:
- remove the Workers field from Company.
- generate the gorm models.
- run autoMigrate.
- refactor the proto file and return the Workers field back to Company.
- run autoMigrate.
- 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!
答案1
得分: 1
我通过以下方法实现了你所需的功能。不幸的是,我对proto
消息不太熟悉,所以我只分享了你应该使用的相关Go代码。如果我没错的话,在proto
消息中定义的关联被翻译为GORM
中的belongsTo
。否则,你应该使用repeated
关键字(我说得对吗?)。
在这个前提之后,我将分享代码,然后解释一下。
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
}
好的,让我带你走一遍相关的部分。
结构体定义
在这里,你必须预测到每个关联可能为NULL
。这就是为什么我使用指针来定义它们的原因。借助这一点,你可以创建一个循环依赖关系,例如:Person => Company => Worker => Person => ...
此外,我通过设置Person
结构体的表名为people
来重写了表名。可能,GORM
自己就能够做到这一点,但我从未尝试过。
SQL对象定义
当你实例化一个gorm
客户端时,你必须确保在迁移时不会创建外键。为了实现这一点,你必须在gorm.Config
结构体中将DisableForeignKeyConstraintWhenMigrating
字段设置为true
。借助这一点,外键的创建由你来完成。通过CreateConstraint
方法来完成,你需要指定:
- 相关的表
- 前一个表中涉及的关联
- 你想要命名的外键约束
最后,你可以注意到我运行了AutoMigrate
方法来创建不带外键的表。
写入逻辑
由于表的布局,INSERT
逻辑必须分为两部分。在第一部分中,你将记录插入到它们自己的表中(例如,将Person
插入到people
表中,将Company
插入到companies
表中,依此类推)。我们故意将外键设置为NULL
,否则会出现错误。如果相关记录尚未插入,第一部分将始终引发错误。
然后,我们使用Update
方法将每个外键设置为正确的值。
最后的思考
我在代码中留下了一些被注释的语句,以证明如果你尝试将一些不存在的值分配为外键,它会出错。这意味着你可以在这些列中插入NULL
或正确的值。
我使用了这个包"github.com/samber/lo"
,以便从文字(例如1
)中轻松获取指针值。
如果这有助于解决你的问题,请告诉我,谢谢!
英文:
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!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论