Rest 当某些字段为只读,而其他字段为空时,如何使用Golang结构在API中执行CRUD?

Rest 当某些字段为只读,而其他字段为空时,如何使用Golang结构在API中执行CRUD?,rest,struct,go,Rest,Struct,Go,我正在编写一个用于执行基本CRUD操作的API(基本上是structMySQL表)。下面是一个映射到数据库中表的示例结构。我对字段使用指针,以便将nil视为空/不存在: type Foo struct { Id *int32 Name *string Description *string CreateDate *string } Id字段是一个应由数据库分配的自动递增字段。Name字段是可写且必需的。Description字段可写且可

我正在编写一个用于执行基本CRUD操作的API(基本上是structMySQL表)。下面是一个映射到数据库中表的示例结构。我对字段使用指针,以便将
nil
视为空/不存在:

type Foo struct {
  Id           *int32
  Name         *string
  Description  *string
  CreateDate   *string
}
Id
字段是一个应由数据库分配的自动递增字段。
Name
字段是可写且必需的。
Description
字段可写且可为空。
CreateDate
字段在插入时由MySQL分配,不应写入

当用户发布要创建的新Foo时,请求主体在JSON中如下所示:

POST /Foo
{"Name": "test name", "Description": "test description"}
使用

foo := Foo{}
json.NewDecoder(requestBody).Decode(&foo)
我使用库来简化插入/更新/删除,但如果我希望使用字段反射来概括查询创建,那么即使我正在编写原始sql,我的问题仍然存在

gorpDbMap.Insert(&foo)
如果用户提供只读字段,则会出现我的第一个问题。如果发布了此请求正文,则结构将愉快地接受
Id
,当我执行插入时,它将覆盖自动增量值。我知道这在某种程度上是我使用ORM而不是手动编写SQL
insert
的错误,但我希望在水合结构时,我可以以某种方式强制执行,只有那些可写字段应该被解码,其他字段应该被忽略(或导致错误):

除了手动检查水合结构并取消设置我不希望用户提供的任何只读字段之外,我找不到其他简单的方法

我遇到的第二个问题是确定用户何时取消设置值(为要更新的字段传递
NULL
)与何时未提供值。这是RESTful术语中的部分更新/补丁

例如,假设存在一个id=1的Foo。用户现在希望将
名称
测试名称
更新为
新名称
,将
描述
测试描述
更新为

PATCH /Foo/1
{"Name": "new name", "Description": NULL}
由于我的结构对其字段使用指针,因此我可以确定在创建时,如果
foo.Description==nil
,是否应将
Description
设置为null。但是在这个部分更新中,我如何区分未提供
Description
的情况(因此应保持原样)和调用方希望将
Description
的值设置为
NULL
的情况

我知道可以通过围绕我定义的每个结构编写大量自定义代码来解决这个问题,但我希望找到一个不需要太多样板文件的通用解决方案。我也看到过一些解决方案,它们对补丁请求采用不同的正文格式,但我必须满足现有的合同,因此我不能对部分更新采用不同的格式

我正在考虑几个选择,但都不能让我满意

  • 使用
    接口
    类型化映射并编写代码来检查每个字段并根据需要断言类型。通过这种方式,我可以确定是否未提供字段和
    NULL
    vs。好像有很多工作要做

  • 为每个场景定义多个结构。这感觉有点干净,但也有点不必要的冗长。它只解决了我遇到的两个问题中的一个(强制只读),但无法确定可为null的字段在部分更新时实际为null还是不提供

  • e、 g

    这篇文章解决了部分问题,让我走了这么远,但没有解决我现在遇到的两个问题:


    我看到的大多数建议都是围绕更改模式的设计和避免空值而提出的,但我没有能力修改它,因为其他使用者已经在使用它了。

    这里的一个选项是使用一个自定义类型,在特殊情况下使用JSON封送。例如,如果您想要一个在JSON中只读的整数,您可以这样做:

    type JsonReadOnlyInt int32
    
    func (i JsonReadOnlyInt) MarshalJSON() ([]byte, error) {
        return json.Marshal(int32(i))
    }
    
    func (i *JsonReadOnlyInt) UnmarshalJSON([]byte) error {
        return nil // ignore attempts to set
    }
    
    func (i *JsonReadOnlyInt) Scan(value interface{}) error {
        // And maybe also cases for string/[]byte, depending on the driver
        v, ok := value.(int64)
        if !ok {
            return errors.New("Could not scan")
        }
        *i = JsonReadOnlyInt(v)
        return nil
    }
    
    func (i JsonReadOnlyInt) Value() (driver.Value, error) {
        return int64(i), nil
    }
    
    如果在其中一个结构中使用此类型,则可以将整数编组为JSON,但将在相反方向忽略:

    要使这种类型与GORP一起工作,还需要做更多的工作。看起来这个包使用了标准的库数据库转换接口,所以您需要实现和。大概是这样的:

    type JsonReadOnlyInt int32
    
    func (i JsonReadOnlyInt) MarshalJSON() ([]byte, error) {
        return json.Marshal(int32(i))
    }
    
    func (i *JsonReadOnlyInt) UnmarshalJSON([]byte) error {
        return nil // ignore attempts to set
    }
    
    func (i *JsonReadOnlyInt) Scan(value interface{}) error {
        // And maybe also cases for string/[]byte, depending on the driver
        v, ok := value.(int64)
        if !ok {
            return errors.New("Could not scan")
        }
        *i = JsonReadOnlyInt(v)
        return nil
    }
    
    func (i JsonReadOnlyInt) Value() (driver.Value, error) {
        return int64(i), nil
    }
    
    现在,您应该能够将这种类型的值往返到数据库

    就补丁问题而言,您可以尝试两种选择:

  • 只需解码成一个保存记录旧值的结构。JSON对象中缺少的任何字段都不会被更新,并且可以使用上述策略保护只读字段

  • 使用自定义结构类型来表示字段,而不是像上面这样的简单整数。使其零值对应于unset,并使其
    UnmarshalJSON
    方法设置一个标志,表示它已被设置


  • 哪个更合适可能取决于代码的其余部分。

    sql库内置了处理空值的类型,请尝试直接使用这些类型。sql.NullString:但是设置和获取将变得更加痛苦。谢谢,这非常有帮助。我正试图实现你对我问题中补丁部分的建议,但运气不好。当原始JSON中为结构的成员字段提供的值为空时,似乎不会对该字段调用UnmarshalJSON函数。因此,通过这种方式,我找不到一种方法来确定一个值是以显式NULL传递还是根本不传递:我是误解了还是我上面评论中的示例代码使选项#2不可行?选项#1似乎仍然可以工作,但感觉在代码中管理可能有点困难,因为在编写时,我会查找nil
    foo.Description
    指针