使用 gorm 创建记录时如何验证所属关系

Mik*_*ink 5 json go go-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_ ]*$`))),
    )
}
Run Code Online (Sandbox Code Playgroud)

我的员工组和员工角色都只是一个带有 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))
    }
}
Run Code Online (Sandbox Code Playgroud)

我将传入的 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"
    }
}
Run Code Online (Sandbox Code Playgroud)

这给了我以下回应

{
    "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"
}
Run Code Online (Sandbox Code Playgroud)

即使 ID 为 1 的角色记录和 ID 为 1 的组记录不存在,也会给出以下响应。如果角色或组不存在,它应该给我一个错误并且不会创建记录。

Eze*_*uns 4

您如何建立关系并不明显,因为您似乎对 EmployeeGroup 和 EmployeeRole 使用匿名嵌入结构,并且您没有包含这些代码。我假设您已正确设置,并且 gorm 很乐意处理涉及匿名嵌入结构的关系。

我还假设您的意思是 BelongsTo 关系,否则您将在哪里放置外键来链接 Employee 表与 Role 或 Group 表?很明显,外键不在最后两个中。

这样你就得到了一个 Employee 结构体,当给定无效的 GroupID 或 RoleID 时,你有两个选择:要么在 ID 不存在时拒绝操作,要么使用给定的 ID 创建一个新的 Role/Group。第一个是更理智和通常的处理事情的方式,但 gorm 可以做任何一个。

首先,如果数据库中有外键检查,您可以执行以下操作:

// First make sure that EmployeeRoleID and EmployeeGroupID are set 
err := db.Omit("EmployeeRole", "EmployeeGroup").Create(employee).Error
Run Code Online (Sandbox Code Playgroud)

如果 EmployeeGroup.ID 或 EmployeeRole.ID 不存在,则会发生外键冲突,并且您将收到错误消息。您可以检查错误并推断它与外键有关,然后返回适当的 API 级错误。

您可能会发现此错误检查有点麻烦,具体取决于您使用的数据库。老实说,在保存实体之前触发一堆额外的关系验证查询是非常常见的,所以在这种情况下必须这样做不要感到惊讶。

另一方面,如果您想每次保存新的角色和组,则可以删除 Omit 调用,确保每个 ID 为零,然后调用 Create。Gorm 将保存角色和组,并为其提供新 ID,然后将指向这些新 ID 的链接保存在员工记录中。


编辑

我尝试运行你的代码,发现了一些问题:

  • 首先,您的输入 JSON 具有所有 TitleCased 键名称,但模型结构期望其中很多都是小写的。您需要决定其中之一并坚持下去,否则编组和解组将不起作用。
  • 这同样适用于EmployeeRoleID/EmployeeGroupIDgroupID/ roleID。在 JSON 中拥有一种名称版本,在 Go 结构中拥有一种版本(如果您删除了标签,它们也可以都是相同的 TitleCased 版本json:"blah")。
  • Gorm 不喜欢为相关实体嵌入匿名结构,它不会为它们创建外键并在主表中创建无关字段,不要这样做。
  • 您已使用 Role 和 Group 的名称作为复合主键的一部分,但这会破坏键控,因为您需要主结构中的两个字段才能使其工作,例如EmployeeGroupIDEmployeeGroupName,这违背了拥有单独主键的目的实体。如果您想强制名称的唯一性,请添加唯一索引。
  • 事实证明,这只Omit("Relation.*")适用于多对多关系。对于“属于”,要做的事情是填写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"`
}
Run Code Online (Sandbox Code Playgroud)

这是一个测试用例,展示了它是如何工作的,假设有一个配置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)
}
Run Code Online (Sandbox Code Playgroud)