使用GORM与自定义的关联表和外键

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

Using GORM with custom join table and foreign keys

问题

我正在使用GORM创建数据库模型。我有三个模型遇到了问题。它们是User、Email和UserEmail模型。我希望在User和Email模型之间建立多对多的关系,这样我就可以跟踪用户何时更改了电子邮件以及何时更改,而无需使用日志表。由于已经存在UserEmail模型,我认为User模型没有必要保存EmailID。否则,UserEmail就变成了一个典型的日志表。

UserEmail表应该只允许任何用户同时设置一个电子邮件。因此,我希望将UserID + DeletedAt字段设置为主键。这样,只能存在一个具有NULL DeletedAt的行。

我遇到的问题是,我运行的迁移命令导致多个错误。我尝试了很多其他方法,无法一一列举,但是我尝试的其他方法无法正确生成正确的外键。我认为我想在UserEmail中有两个外键,分别是EmailID和UserID。如果可能的话,我还希望GORM的Preload功能能够正常工作。

我有一个迁移命令,运行时实际上运行了以下代码:

func main() {
	app.DB.Migrator().DropTable(&User{})
	app.DB.Migrator().CreateTable(&User{})
	app.DB.Migrator().DropTable(&Email{})
	app.DB.Migrator().CreateTable(&Email{})
	app.DB.Migrator().DropTable(&UserEmail{})
	app.DB.Migrator().CreateTable(&UserEmail{})
}

我还尝试按不同的顺序迁移每个模型。然而,每次都会出现错误。Email和UserEmail表被正确创建,但是User总是出现一些错误。

2023/07/21 16:38:50 Dropping table named 'users'...

2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:126
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface

2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:126
[error] failed to parse value &model.User{ID:0x0, Email:model.Email{ID:0x0, Address:"", IsBanned:false, IsRegistered:false, IsVerified:false, CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, PreviousEmails:[]model.Email(nil), Name:"", PasswordHash:"", CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, got error invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface

2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:128
[0.515ms] [rows:0] SET FOREIGN_KEY_CHECKS = 0;

2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:130
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface

2023/07/21 16:38:50 Creating table named 'users'...

2023/07/21 16:38:50 [...]/api/command/commandMigrate.go:132
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface

2023/07/21 16:38:50 [...]/api/command/commandMigrate.go:132
[error] failed to parse value &model.User{ID:0x0, Email:model.Email{ID:0x0, Address:"", IsBanned:false, IsRegistered:false, IsVerified:false, CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, PreviousEmails:[]model.Email(nil), Name:"", PasswordHash:"", CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, got error invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface

2023/07/21 16:38:50 [...]/api/command/commandMigrate.go:132
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface

以下是当前模型的样子,可以看到所需的功能。

func init() {
	app.DB.SetupJoinTable(&User{}, "Emails", &UserEmail{})
}

// 用户表不应该有任何EmailID列,这就是UserEmail表的作用,但是如果能够预加载电子邮件值,那将是很好的
// Email字段应该是UserEmails中具有NULL DeletedAt的电子邮件
// PreviousEmails字段应该是UserEmails中具有非NULL DeletedAt的所有电子邮件
type User struct {
	ID             uint            `json:"id" gorm:"primarykey"`
	Email          Email           `json:"email"`
	PreviousEmails []Email         `json:"previous_emails"`
	Name           string          `json:"name" gorm:"type:varchar(255);not null"`
	PasswordHash   string          `json:"password_hash,omitempty" gorm:"type:binary(60);not null"`
	CreatedAt      time.Time       `json:"created_at" gorm:"type:DATETIME;default:CURRENT_TIMESTAMP;not null"`
	UpdatedAt      time.Time       `json:"updated_at" gorm:"type:DATETIME"`
	DeletedAt      *gorm.DeletedAt `json:"deleted_at" gorm:"type:DATETIME;index"`
}

func (User) TableName() string {
	return "users"
}
// 邮件不会被删除,它们会永久存储
type Email struct {
	ID           uint            `json:"id" gorm:"primarykey"`
	Address      string          `json:"address" gorm:"type:varchar(320);unique;not null"`
	IsBanned     bool            `json:"is_banned" gorm:"type:bit;default:0;not null"`
	IsRegistered bool            `json:"is_registered" gorm:"type:bit;default:0;not null"`
	IsVerified   bool            `json:"is_verified" gorm:"type:bit;default:0;not null"`
	CreatedAt    time.Time       `json:"created_at" gorm:"type:DATETIME;default:CURRENT_TIMESTAMP;not null"`
	UpdatedAt    time.Time       `json:"updated_at" gorm:"type:DATETIME"`
}

func (Email) TableName() string {
	return "emails"
}
// 当用户更改其电子邮件时,旧电子邮件会被软删除
type UserEmail struct {
	EmailID   uint            `json:"email_id"`
	UserID    uint            `json:"user_id" gorm:"primarykey"`
	CreatedAt time.Time       `json:"created_at" gorm:"type:DATETIME;default:CURRENT_TIMESTAMP;not null"`
	DeletedAt *gorm.DeletedAt `json:"deleted_at" gorm:"primarykey;type:DATETIME"`
}

func (UserEmail) TableName() string {
	return "user_emails"
}

如何实现所需的效果?

英文:

I'm using GORM to create database models. I have three models that I am having troubles with. There are User, Email, and UserEmail models. I want a many2many relationship between the User and Email models, so that I can keep track of when a users change an email and when, without having to use log table. Since the UserEmail model exists, I see no reason for the User model to hold an EmailID. Otherwise, the UserEmail just becomes a typical log table.

The UserEmail table should only allow any User to have one email set at a time. For that reason, I want to se the UserID + DeletedAt fields to be the primary key. That way, only one row with a NULL DeletedAt can exist.

The problem I am having is that the migrator commands I am running is causing multiple errors. I've tried so many other things that cannot recount everything, however the other things I have tried wouldn't properly produce the correct foreign keys. I think I want two foreign keys in UserEmail for EmailID and UserID. I would also like GORM's Preload functionality should work if possible.

I have a migrate command, that when run, it essentially runs this:

func main() {
app.DB.Migrator().DropTable(&User{})
app.DB.Migrator().CreateTable(&User{})
app.DB.Migrator().DropTable(&Email{})
app.DB.Migrator().CreateTable(&Email{})
app.DB.Migrator().DropTable(&UserEmail{})
app.DB.Migrator().CreateTable(&UserEmail{})
}

I have also tried migrating each model in different orders. However every time, I get errors. The Email and UserEmail tables get created properly, however User always has some errors.

2023/07/21 16:38:50 Dropping table named 'users'...
2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:126
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface
2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:126
[error] failed to parse value &model.User{ID:0x0, Email:model.Email{ID:0x0, Address:"", IsBanned:false, IsRegistered:false, IsVerified:false, CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, PreviousEmails:[]model.Email(nil), Name:"", PasswordHash:"", CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, got error invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface
2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:128
[0.515ms] [rows:0] SET FOREIGN_KEY_CHECKS = 0;
2023/07/21 16:38:50 [...]/go/pkg/mod/gorm.io/driver/mysql@v1.3.6/migrator.go:130
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface
2023/07/21 16:38:50 Creating table named 'users'...
2023/07/21 16:38:50 [...]/api/command/commandMigrate.go:132
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface
2023/07/21 16:38:50 [...]/api/command/commandMigrate.go:132
[error] failed to parse value &model.User{ID:0x0, Email:model.Email{ID:0x0, Address:"", IsBanned:false, IsRegistered:false, IsVerified:false, CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, PreviousEmails:[]model.Email(nil), Name:"", PasswordHash:"", CreatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdatedAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), DeletedAt:(*gorm.DeletedAt)(nil)}, got error invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface
2023/07/21 16:38:50 [...]/api/command/commandMigrate.go:132
[error] invalid field found for struct github.com/neekla/pmapi/api/database/model.User's field Email: define a valid foreign key for relations or implement the Valuer/Scanner interface

Here is what the current models look like, which gives a glimpse the desired functionality.

func init() {
app.DB.SetupJoinTable(&User{}, "Emails", &UserEmail{})
}
// the user table should not have any EmailID column, that's what the UserEmail table is for, but it would be nice to still be able to preload the email values
// Email field should be the email in UserEmails that has NULL for for DeletedAt
// PreviousEmails field should be all the emails in UserEmails that have a non-NULL DeletedAt
type User struct {
ID             uint            `json:"id" gorm:"primarykey"`
Email          Email           `json:"email"`
PreviousEmails []Email         `json:"previous_emails"`
Name           string          `json:"name" gorm:"type:varchar(255);not null"`
PasswordHash   string          `json:"password_hash,omitempty" gorm:"type:binary(60);not null"`
CreatedAt      time.Time       `json:"created_at" gorm:"type:DATETIME;default:CURRENT_TIMESTAMP;not null"`
UpdatedAt      time.Time       `json:"updated_at" gorm:"type:DATETIME"`
DeletedAt      *gorm.DeletedAt `json:"deleted_at" gorm:"type:DATETIME;index"`
}
func (User) TableName() string {
return "users"
}
// emails do not get deleted, they are permanently stored
type Email struct {
ID           uint            `json:"id" gorm:"primarykey"`
Address      string          `json:"address" gorm:"type:varchar(320);unique;not null"`
IsBanned     bool            `json:"is_banned" gorm:"type:bit;default:0;not null"`
IsRegistered bool            `json:"is_registered" gorm:"type:bit;default:0;not null"`
IsVerified   bool            `json:"is_verified" gorm:"type:bit;default:0;not null"`
CreatedAt    time.Time       `json:"created_at" gorm:"type:DATETIME;default:CURRENT_TIMESTAMP;not null"`
UpdatedAt    time.Time       `json:"updated_at" gorm:"type:DATETIME"`
}
func (Email) TableName() string {
return "emails"
}
// when a user changes their email, the old one gets soft deleted
type UserEmail struct {
EmailID   uint            `json:"email_id"`
UserID    uint            `json:"user_id" gorm:"primarykey"`
CreatedAt time.Time       `json:"created_at" gorm:"type:DATETIME;default:CURRENT_TIMESTAMP;not null"`
DeletedAt *gorm.DeletedAt `json:"deleted_at" gorm:"primarykey;type:DATETIME"`
}
func (UserEmail) TableName() string {
return "user_emails"
}

How can I achieve the desired effects?

答案1

得分: 0

我修复了。很简单。我只需要运行以下命令:

go get gorm.io/gorm@none
英文:

I fixed it. It was simple. All I had to do was run:

go get gorm.io/gorm@none

huangapple
  • 本文由 发表于 2023年7月22日 08:19:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76741858.html
匿名

发表评论

匿名网友

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

确定