使用 gorm 创建记录时如何验证 belongs-to 关系
How to validate a belongs-to relationship when creating record with gorm
我有以下型号
type PrivateGormModel struct {
ID uint `gorm:"primary_key" json:"id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt *time.Time `json:"-"`
}
type Employee struct {
PrivateGormModel
Person `gorm:"embedded" json:"person,omitempty"`
Contact `gorm:"embedded" json:"contact,omitempty"`
Address `gorm:"embedded" json:"address,omitempty"`
AltContact `gorm:"embedded" json:"privateContact,omitempty"`
BankAccount `gorm:"embedded" json:"bankAccount,omitempty"`
EmployeeGroupID uint `json:"groupID"`
EmployeeGroup `json:"group"`
EmployeeRoleID uint `json:"roleID"`
EmployeeRole `json:"role"`
}
func (e Employee) Validate() error {
return validation.ValidateStruct(&e,
validation.Field(&e.Person, validation.Required),
validation.Field(&e.Contact),
validation.Field(&e.Address),
validation.Field(&e.AltContact),
validation.Field(&e.BankAccount),
validation.Field(&e.EmployeeGroup),
validation.Field(&e.EmployeeRole),
)
}
type EmployeeGroup struct {
PrivateGormModel
Title string `json:"title" gorm:"primaryKey;unique"`
}
func (e EmployeeGroup) Validate() error {
return validation.ValidateStruct(&e,
validation.Field(&e.Title, validation.Required, validation.Length(1, 32), validation.Match(regexp.MustCompile(`^[a-zA-Z0-9_ ]*$`))),
)
}
type EmployeeRole struct {
PrivateGormModel
Title string `json:"title" gorm:"primaryKey;unique"`
}
func (e EmployeeRole) Validate() error {
return validation.ValidateStruct(&e,
validation.Field(&e.Title, validation.Required, validation.Length(1, 32), validation.Match(regexp.MustCompile(`^[a-zA-Z0-9_ ]*$`))),
)
}
我的员工组和员工角色都只是一个带有 gorm 模型和字符串类型标题的结构。我在数据库中有一个 ID 为 1 的角色,一个 ID 为 1 的组。这是创建员工的处理程序
func CreateEmployee(db *database.Database) fiber.Handler {
return func(c *fiber.Ctx) error {
employee := new(model.Employee)
if err := c.BodyParser(employee); err != nil {
fmt.Printf("%v", err)
return c.JSON(myResponse.ParsingError())
}
// if err := employee.Validate(); err != nil {
// return c.JSON(myResponse.ValidationError(err))
// }
if result := db.Omit("EmployeeRole.*").Omit("EmployeeGroup.*").Create(&employee); result.Error != nil {
return c.JSON(myResponse.RecordCreateError())
}
return c.JSON(myResponse.RecordCreateSuccess(employee))
}
}
我将传入的 JSON 解析为如下所示的模型,并尝试在数据库中创建它
{
"Person":{
"Initials":"",
"FirstName":"",
"MiddleName":"",
"LastName":"",
"DateOfBirth":"",
"Language":""
},
"Address":{
"Country":"",
"Zip":"",
"Number":"0",
"Addition":"",
"Street":"",
"State":"",
"City":""
},
"Contact":{
"Tel":"",
"Mail":"",
"URL":""
},
"BankAccount":{
"Bank":"",
"BIC":"",
"IBAN":"",
"AccountHolder":"",
"Establishment":""
},
"EmployeeRoleID":1,
"EmployeeRole":{
"Title":"Test"
},
"EmployeeGroupID":1,
"EmployeeGroup":{
"Title":"Test"
}
}
这给了我以下响应
{
"data": {
"id": 7,
"person": {
"initials": "",
"firstName": "",
"middleName": "",
"lastName": "",
"dateOfBirth": "2021-01-05T11:14:38+01:00",
"language": ""
},
"contact": {
"tel": "",
"mail": "",
"url": ""
},
"address": {
"country": "",
"zip": "",
"number": "0",
"addition": "",
"street": "",
"state": "",
"city": ""
},
"privateContact": {
"tel": "",
"mail": "",
"url": ""
},
"bankAccount": {
"bank": "",
"bic": "",
"iban": "",
"accountHolder": "",
"establishment": ""
},
"groupID": 0,
"group": {
"id": 0,
"title": ""
},
"roleID": 0,
"role": {
"id": 0,
"title": ""
}
},
"message": "record created successfully",
"status": "success"
}
即使 ID 为 1 的角色记录和 ID 为 1 的组记录不存在,它也会给我以下响应。如果角色或组不存在,它应该给我一个错误,而不是创建记录。
您是如何建立关系的并不明显,因为您似乎正在为 EmployeeGroup 和 EmployeeRole 使用匿名嵌入式结构,并且您没有包含这些代码。我将假设您已正确设置并且 gorm 乐于处理涉及匿名嵌入式结构的关系。
我还假设你的意思是 BelongsTo 关系,否则你会将外键放在哪里 link Employee table 与 Role 或 Group table秒?很明显,外键不在最后两个中。
那么你得到了一个 Employee 结构,当给出一个无效的 GroupID 或 RoleID 时,你有两个选择:要么在 ID 不存在时拒绝操作,要么创建一个新的 Role/Group给出的ID。第一种是更理智和通常的处理方式,但是 gorm 可以做任何一种。
首先,如果您的数据库中有外键检查,您可以执行以下操作:
// First make sure that EmployeeRoleID and EmployeeGroupID are set
err := db.Omit("EmployeeRole", "EmployeeGroup").Create(employee).Error
如果 EmployeeGroup.ID 或 EmployeeRole.ID 不存在,将发生外键冲突,您将得到一个错误。您可以检查错误并推断它与外键有关,然后 return 一个适当的 API 级错误。
根据您使用的数据库,您可能会发现此错误检查有点麻烦。老实说,在保存实体之前触发一堆额外的关系验证查询是非常常见的,所以在这种情况下不必对必须这样做感到惊讶。
另一方面,如果你想每次都保存一个新的Role和Group,你可以去掉Omit调用,确保每个ID都为0,然后调用Create。 Gorm 会保存 Role 和 Group,给他们新的 ID,然后将 link 保存到员工记录中的这些新 ID。
编辑
我试过 运行 你的代码,发现了一些问题:
- 首先,您的输入 JSON 具有所有 TitleCased 键名,但模型结构期望其中很多键名是小写的。您需要决定其中之一并坚持下去,否则编组和解组将不起作用。
- 同样适用于
EmployeeRoleID
/EmployeeGroupID
与 groupID
/roleID
。在 JSON 中有一个版本的名字,在 Go 结构中有一个版本(如果你删除了 json:"blah"
标签,它们也可以都是相同的 TitleCased 版本)。
- Gorm 不喜欢相关实体的嵌入式匿名结构,它不会为它们创建外键并在主 table 中创建无关字段,不要这样做。
- 您已将 Role 和 Group 的名称用作复合主键的一部分,但这会破坏键控,因为您需要在主结构中使用两个字段才能使其正常工作,例如
EmployeeGroupID
和 EmployeeGroupName
,这违背了拥有独立实体的目的。如果您想强制名称的唯一性,请改为添加唯一索引。
- 原来
Omit("Relation.*")
只适用于多对多关系。对于 belongs-to 要做的事情是填写主结构中的 RelationID
字段,并通常省略与 Omit("Relation")
. 的关系
这是您的模型的简化版本:
type Employee struct {
PrivateGormModel
Person `gorm:"embedded" json:"Person"`
// ...
RoleID uint
Role EmployeeRole `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type EmployeeRole struct {
PrivateGormModel
Title string `gorm:"uniqueIndex"`
}
这是一个展示其工作原理的测试用例,假设已配置 DB *gorm.DB
:
package main
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
var inJSON = `{
"Person": {
"FirstName": "Test"
},
"RoleID": 1
}`
func TestGORM(t *testing.T) {
require := require.New(t)
require.NoError(DB.Migrator().DropTable(&Employee{}, &EmployeeRole{}))
require.NoError(DB.Migrator().AutoMigrate(&Employee{}, &EmployeeRole{}))
emp := Employee{}
json.Unmarshal([]byte(inJSON), &emp)
// create the role to simulate that it exists
role := EmployeeRole{PrivateGormModel{ID: 1}, "Test"}
require.NoError(DB.Create(&role).Error)
// avoid re-saving emp.Role
require.NoError(DB.Omit("Role").Create(&emp).Error)
// if instead the RoleID doesn't exist
emp.RoleID = 5
require.Error(DB.Create(&emp).Error)
}
我有以下型号
type PrivateGormModel struct {
ID uint `gorm:"primary_key" json:"id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt *time.Time `json:"-"`
}
type Employee struct {
PrivateGormModel
Person `gorm:"embedded" json:"person,omitempty"`
Contact `gorm:"embedded" json:"contact,omitempty"`
Address `gorm:"embedded" json:"address,omitempty"`
AltContact `gorm:"embedded" json:"privateContact,omitempty"`
BankAccount `gorm:"embedded" json:"bankAccount,omitempty"`
EmployeeGroupID uint `json:"groupID"`
EmployeeGroup `json:"group"`
EmployeeRoleID uint `json:"roleID"`
EmployeeRole `json:"role"`
}
func (e Employee) Validate() error {
return validation.ValidateStruct(&e,
validation.Field(&e.Person, validation.Required),
validation.Field(&e.Contact),
validation.Field(&e.Address),
validation.Field(&e.AltContact),
validation.Field(&e.BankAccount),
validation.Field(&e.EmployeeGroup),
validation.Field(&e.EmployeeRole),
)
}
type EmployeeGroup struct {
PrivateGormModel
Title string `json:"title" gorm:"primaryKey;unique"`
}
func (e EmployeeGroup) Validate() error {
return validation.ValidateStruct(&e,
validation.Field(&e.Title, validation.Required, validation.Length(1, 32), validation.Match(regexp.MustCompile(`^[a-zA-Z0-9_ ]*$`))),
)
}
type EmployeeRole struct {
PrivateGormModel
Title string `json:"title" gorm:"primaryKey;unique"`
}
func (e EmployeeRole) Validate() error {
return validation.ValidateStruct(&e,
validation.Field(&e.Title, validation.Required, validation.Length(1, 32), validation.Match(regexp.MustCompile(`^[a-zA-Z0-9_ ]*$`))),
)
}
我的员工组和员工角色都只是一个带有 gorm 模型和字符串类型标题的结构。我在数据库中有一个 ID 为 1 的角色,一个 ID 为 1 的组。这是创建员工的处理程序
func CreateEmployee(db *database.Database) fiber.Handler {
return func(c *fiber.Ctx) error {
employee := new(model.Employee)
if err := c.BodyParser(employee); err != nil {
fmt.Printf("%v", err)
return c.JSON(myResponse.ParsingError())
}
// if err := employee.Validate(); err != nil {
// return c.JSON(myResponse.ValidationError(err))
// }
if result := db.Omit("EmployeeRole.*").Omit("EmployeeGroup.*").Create(&employee); result.Error != nil {
return c.JSON(myResponse.RecordCreateError())
}
return c.JSON(myResponse.RecordCreateSuccess(employee))
}
}
我将传入的 JSON 解析为如下所示的模型,并尝试在数据库中创建它
{
"Person":{
"Initials":"",
"FirstName":"",
"MiddleName":"",
"LastName":"",
"DateOfBirth":"",
"Language":""
},
"Address":{
"Country":"",
"Zip":"",
"Number":"0",
"Addition":"",
"Street":"",
"State":"",
"City":""
},
"Contact":{
"Tel":"",
"Mail":"",
"URL":""
},
"BankAccount":{
"Bank":"",
"BIC":"",
"IBAN":"",
"AccountHolder":"",
"Establishment":""
},
"EmployeeRoleID":1,
"EmployeeRole":{
"Title":"Test"
},
"EmployeeGroupID":1,
"EmployeeGroup":{
"Title":"Test"
}
}
这给了我以下响应
{
"data": {
"id": 7,
"person": {
"initials": "",
"firstName": "",
"middleName": "",
"lastName": "",
"dateOfBirth": "2021-01-05T11:14:38+01:00",
"language": ""
},
"contact": {
"tel": "",
"mail": "",
"url": ""
},
"address": {
"country": "",
"zip": "",
"number": "0",
"addition": "",
"street": "",
"state": "",
"city": ""
},
"privateContact": {
"tel": "",
"mail": "",
"url": ""
},
"bankAccount": {
"bank": "",
"bic": "",
"iban": "",
"accountHolder": "",
"establishment": ""
},
"groupID": 0,
"group": {
"id": 0,
"title": ""
},
"roleID": 0,
"role": {
"id": 0,
"title": ""
}
},
"message": "record created successfully",
"status": "success"
}
即使 ID 为 1 的角色记录和 ID 为 1 的组记录不存在,它也会给我以下响应。如果角色或组不存在,它应该给我一个错误,而不是创建记录。
您是如何建立关系的并不明显,因为您似乎正在为 EmployeeGroup 和 EmployeeRole 使用匿名嵌入式结构,并且您没有包含这些代码。我将假设您已正确设置并且 gorm 乐于处理涉及匿名嵌入式结构的关系。
我还假设你的意思是 BelongsTo 关系,否则你会将外键放在哪里 link Employee table 与 Role 或 Group table秒?很明显,外键不在最后两个中。
那么你得到了一个 Employee 结构,当给出一个无效的 GroupID 或 RoleID 时,你有两个选择:要么在 ID 不存在时拒绝操作,要么创建一个新的 Role/Group给出的ID。第一种是更理智和通常的处理方式,但是 gorm 可以做任何一种。
首先,如果您的数据库中有外键检查,您可以执行以下操作:
// First make sure that EmployeeRoleID and EmployeeGroupID are set
err := db.Omit("EmployeeRole", "EmployeeGroup").Create(employee).Error
如果 EmployeeGroup.ID 或 EmployeeRole.ID 不存在,将发生外键冲突,您将得到一个错误。您可以检查错误并推断它与外键有关,然后 return 一个适当的 API 级错误。
根据您使用的数据库,您可能会发现此错误检查有点麻烦。老实说,在保存实体之前触发一堆额外的关系验证查询是非常常见的,所以在这种情况下不必对必须这样做感到惊讶。
另一方面,如果你想每次都保存一个新的Role和Group,你可以去掉Omit调用,确保每个ID都为0,然后调用Create。 Gorm 会保存 Role 和 Group,给他们新的 ID,然后将 link 保存到员工记录中的这些新 ID。
编辑
我试过 运行 你的代码,发现了一些问题:
- 首先,您的输入 JSON 具有所有 TitleCased 键名,但模型结构期望其中很多键名是小写的。您需要决定其中之一并坚持下去,否则编组和解组将不起作用。
- 同样适用于
EmployeeRoleID
/EmployeeGroupID
与groupID
/roleID
。在 JSON 中有一个版本的名字,在 Go 结构中有一个版本(如果你删除了json:"blah"
标签,它们也可以都是相同的 TitleCased 版本)。 - Gorm 不喜欢相关实体的嵌入式匿名结构,它不会为它们创建外键并在主 table 中创建无关字段,不要这样做。
- 您已将 Role 和 Group 的名称用作复合主键的一部分,但这会破坏键控,因为您需要在主结构中使用两个字段才能使其正常工作,例如
EmployeeGroupID
和EmployeeGroupName
,这违背了拥有独立实体的目的。如果您想强制名称的唯一性,请改为添加唯一索引。 - 原来
Omit("Relation.*")
只适用于多对多关系。对于 belongs-to 要做的事情是填写主结构中的RelationID
字段,并通常省略与Omit("Relation")
. 的关系
这是您的模型的简化版本:
type Employee struct {
PrivateGormModel
Person `gorm:"embedded" json:"Person"`
// ...
RoleID uint
Role EmployeeRole `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type EmployeeRole struct {
PrivateGormModel
Title string `gorm:"uniqueIndex"`
}
这是一个展示其工作原理的测试用例,假设已配置 DB *gorm.DB
:
package main
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
var inJSON = `{
"Person": {
"FirstName": "Test"
},
"RoleID": 1
}`
func TestGORM(t *testing.T) {
require := require.New(t)
require.NoError(DB.Migrator().DropTable(&Employee{}, &EmployeeRole{}))
require.NoError(DB.Migrator().AutoMigrate(&Employee{}, &EmployeeRole{}))
emp := Employee{}
json.Unmarshal([]byte(inJSON), &emp)
// create the role to simulate that it exists
role := EmployeeRole{PrivateGormModel{ID: 1}, "Test"}
require.NoError(DB.Create(&role).Error)
// avoid re-saving emp.Role
require.NoError(DB.Omit("Role").Create(&emp).Error)
// if instead the RoleID doesn't exist
emp.RoleID = 5
require.Error(DB.Create(&emp).Error)
}