在GORM中,是否可能自动迁移具有循环关系的表格?

huangapple go评论74阅读模式
英文:

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注释的模型,包括外键。然后当我尝试自动迁移它们时,当然会出错。

我找到的唯一解决方法是:

  1. 从Company中删除Workers字段。
  2. 生成gorm模型。
  3. 运行autoMigrate。
  4. 重构proto文件,并将Workers字段返回到Company中。
  5. 运行autoMigrate。
  6. 我们将得到具有适当外键的所有表。

我尝试在网上搜索任何想法,但似乎找不到任何解决方案。

欢迎提供任何帮助/想法!

英文:

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!

答案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!

huangapple
  • 本文由 发表于 2023年6月7日 04:41:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/76418306.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定