ReactiveDB/core

update/upsert 可能会将不在 `raw` 参数里的字段从 undefined 设为 null

chuan6 opened this issue · 2 comments

实际发生的场景:

  • 受到影响的那个字段是 OBJECT 类型

  • raw 参数只包含主键和一个与受影响字段无关(不存在关联)的字段

  • 手动 upsert { 主键: 'xxx', 受影响字段: undefined },可以将该字段值重置为 undefined

  • 构建一个失败的单元测试以确认问题

在 test/schemas/Post 里,添加 attachments 字段,设置其类型为 RDBType.OBJECT,

it.only("should keep an `undefined` attribute unchanged if the attribute is absent from the upsert body", function*() {
  const mockPost = postGen(1, null).pop()
  const { _id, content, created } = mockPost

  // 需要先有这个条目才能复现原来 undefined 的属性被改为 null 的行为。
  yield database.upsert('Post', { _id, created })

  const post = { _id, content }
  const execRet = yield database.update('Post', post) // upsert 也能重现,insert 不会

  const [ret] = yield database
    .get<PostSchema>('Post', {
      where: {
        _id: post._id,
      },
    })
    .values()

  expect(ret).to.deep.equal({ _id, content, created })
  checkExecutorResult(execRet, 1)
})

得:

AssertionError: expected { Object (_id, content, ...) } to deeply equal { Object (_id, content, ...) }
+ expected - actual

{
    "_id": "47675ecd"
-  "attachments": null
    "content": "posts content:ab8920dd"
    "created": "1969-12-31T16:00:00.000Z"
}

看了一下,这应该是 lovefield 对 nullable 列的预期行为,而 OBJECT 类型的列被定死为 nullable。

感觉这样的行为对我们的应用而言有些问题;主要在于,它拿掉了原生表达不完整数据的能力。

本来,我们遇到某一单元格的值为 undefined 时,假设这个单元格的值在前端还没有获取到(比如 websocket 推送里没有这个字段),而当值为 null,我们会假设后端数据源对应的值就是 null。

但如果类型为 OBJECT (还有 ARRAY_BUFFER)的列会在更新时把 undefined 变为 null,那上层的代码就不能自然地依赖 undefined 和 null 的不同来分辨(缺值 vs 空值)上述两种情况了。

/cc @Saviio @Brooooooklyn

lovefield 代码上,可以追踪到 rowClass 上的 toDbPayload 方法,将其中对 OBJECT 类型的判断由

if (type == lf.Type.OBJECT) {
  obj[key] = goog.isDefAndNotNull(value) ? value : null;
}

改为

if (type == lf.Type.OBJECT) {
  obj[key] = !goog.isNull(value) ? value : null
}

就可以避免这里我们遇到的问题。

由于这个目标与 lovefield 的意图相悖(见上面的评论),我们可能需要 fork 才能用上上边的做法,@Saviio @Brooooooklyn 你们有什么看法吗?