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

我正在编写一个用于执行基本CRUD操作的API(基本上是struct< =>
mysql表).这是一个映射到数据库中的表的示例结构.我使用字段的指针,以便我可以将nil视为NULL /不存在:

type Foo struct {
  Id           *int32
  Name         *string
  Description  *string
  CreateDate   *string
}

Id字段是一个自动增量字段,应由数据库分配. “名称”字段是可写且必需的. Description字段是可写和可空的. CreateDate字段由MySQL在插入时分配,不应写入.

当用户POST一个新的Foo来创建时,请求体在JSON中看起来像这样:

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

这很容易解码,并使用一个水合Foo结构

foo := Foo{}
json.NewDecoder(requestBody).Decode(&foo)

我正在使用https://github.com/coopernurse/gorp库来简化插入/更新/删除,但即使我正在编写原始sql,如果我希望使用字段上的反射来概括查询创建,我的问题仍然存在.

gorpDbMap.Insert(&foo)

如果用户提供只读字段,则会出现我的第一个问题.如果此请求主体是POSTed,则struct很乐意接受Id,当我执行insert时,它会覆盖autoincrement值.我知道这对于使用ORM而不是手动编写SQL插件有点我的错,但我希望我能以某种方式强化结构,只有那些可写字段应该被解码而其他任何被忽略(或导致错误) ):

POST /Foo
{"Id": 1, "Name": "test name"}

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

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

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

PATCH /Foo/1
{"Name": "new name", "Description": NULL}

由于我的struct使用指针作为其字段,因此我可以确定在创建时是否应将Description设置为null,如果foo.Description == nil.但是在这个部分更新中,我如何区分未提供描述的情况(因此应该保持原样)和上面调用者希望将Description的值设置为NULL的情况?

我知道有很多方法可以通过在我定义的每个结构周围编写大量自定义代码来解决这个问题,但我希望找到一个不需要太多样板的通用解决方案.我也看到了针对PATCH请求采用不同体型的解决方案,但我必须满足现有的合同,因此我不能采用不同的格式进行部分更新.

我正在考虑几个选项但不满足我.

>使用接口类型映射并编写代码来检查每个字段并根据需要断言类型.这样我就可以确定一个字段和NULL是否完全没有提供.好像很多工作.
>为每个方案定义多个结构.这感觉有点干净,但也有点不必要的冗长.它只解决了我遇到的两个问题中的一个(强制执行只读),但没有确定何时在部分更新时实际上将NULLable字段清空或者没有提供.

例如

type FooWrite struct {
  Name        *string
  Description *string
}

type FooRead struct {
  FooWrite
  Id         int32
  CreateDate string
}

这篇文章解决了部分问题并让我了解了这一点,但没有解决我现在遇到的两个问题:https://willnorris.com/2014/05/go-rest-apis-and-pointers

我见过的大多数建议都围绕着改变模式的设计并避免使用NULL,但我没有能力修改它,因为它已被其他消费者使用.

最佳答案 这里的一个选择是使用特殊情况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
}

如果在其中一个结构中使用此类型,则整数将能够编组为JSON,但会反向忽略:http://play.golang.org/p/lW7xuXR6y0

这需要更多的工作才能使这种类型与GORP一起工作.看起来该包使用标准库数据库转换接口,因此您需要实现Scanner from database/sqlValuer from database/sql/driver.如下所示:

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方法设置一个标志,表示它已设置.

哪一个更合适可能取决于您的其余代码.

点赞