From 20c07613c29b56d368b5b6bf69837d72a2ec8c0c Mon Sep 17 00:00:00 2001 From: Chen Yangjian <252317+cyjake@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:34:18 +0800 Subject: [PATCH] feat: bone.jsonMerge() & bone.jsonPreserve() (#425) --- docs/_layouts/zh.html | 1 + docs/assets/css/style.scss | 4 +- docs/zh/json.md | 84 +++++++++++++++++++++++++++++ src/bone.js | 9 +++- test/integration/suite/json.test.js | 15 ++++++ 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 docs/zh/json.md diff --git a/docs/_layouts/zh.html b/docs/_layouts/zh.html index d798b63d..10abbe99 100644 --- a/docs/_layouts/zh.html +++ b/docs/_layouts/zh.html @@ -24,6 +24,7 @@
  • 数据校验
  • 关联关系
  • 查询接口
  • +
  • JSON 字段
  • 钩子
  • 日志
  • TypeScript 支持
  • diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index ecac469b..3192daca 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -147,7 +147,7 @@ body { color: var(--color); border: 1px solid rgb(118, 118, 118); height: 1.6em; - border-radius: 0; + border-radius: 2px; &:focus-visible { border: none; @@ -338,7 +338,7 @@ body { border-style: solid; border-color: transparent; border-left-color: var(--theme-color); - border-radius: 2px + border-radius: 2px; } .dropdown-list { diff --git a/docs/zh/json.md b/docs/zh/json.md new file mode 100644 index 00000000..df810508 --- /dev/null +++ b/docs/zh/json.md @@ -0,0 +1,84 @@ +--- +layout: zh +title: JSON 字段 +--- + +## 目录 +{:.no_toc} + +1. 目录 +{:toc} + +## 字段声明 + +```typescript +import { Bone, DataTypes } from 'leoric'; + +class Post extends Bone { + @Column(DataTypes.JSONB) + extra: Record; +} +``` + +## 查询 + +可以使用 JSON 函数来自定义过滤条件: + +```typescript +const post = await Post.find('JSON_EXTRACT(extra, "$.foo") = ?', 1); +``` + +MySQL 中的 `column->path`简写方式(比如 `extra->"$.foo"`)暂时不支持。 + +## 更新 + +下面这种更新方式容易遇到并发问题,导致数据彼此覆盖: + +```typescript +const post = await Post.first; +// 假设在这个时间间隔内,同时有其他进程更新 post.extra,更新的数据就会被覆盖 +await post.update('extra', { ...post.extra, foo: 1 }); +``` + +MySQL 里面有两个函数可以用来解决这一情况: + +- [JSON_MERGE_PATCH()](https://dev.mysql.com/doc/refman/8.4/en/json-modification-functions.html#function_json-merge-patch) // 覆盖更新 +- [JSON_MERGE_PRESERVE()](https://dev.mysql.com/doc/refman/8.4/en/json-modification-functions.html#function_json-merge-preserve) // 遇到重名属性时会保留两者的值 + +### JSON_MERGE_PATCH() + +Leoric 里面提供了相应的封装: + +```typescript +const post = await Post.first; +await post.jsonMerge('extra', { foo: 1 }); +``` + +第二行语句实际执行的 SQL 类似这样: + +```sql +UPDATE posts SET extra = JSON_MERGE_PATCH('extra', '{"foo":1}') +``` + +需要注意的是 JSON_MERGE_PATCH() 函数只会对 object 做属性合并,如果是数组、字符串、布尔类型,会直接覆盖。 + +> 由于 JSON_MERGE_PATCH() 更接近 JavaScript 中的 merge 行为(`Object.assign()`、lodash/merge),因此默认的 bone.jsonMerge() 方法并没有和 MySQL 中已经不被鼓励使用 JSON_MERGE() 函数对应,后者效果等同于 JSON_MERGE_PRESERVE()。 + +### JSON_MERGE_PRESERVE() + +JSON_MERGE_PRESERVE() 的逻辑则有所不同,如果是数组、字符串等类型,会返回合并结果: + +```sql +JSON_MERGE_PRESERVE('[1, 2]', '[true, false]') // -> [1, 2, true, false] +JSON_MERGE_PRESERVE('1', 'true'); // -> [1, true] +JSON_MERGE_PRESERVE('{ "a": 1 }', '{ "a": 2 }'); // -> { "a": [1, 2] } +``` + +Leoric 里面也有提供相应的封装: + +```typescript +const post = await Post.first; +await post.jsonMergePreserve('extra', { foo: 1 }); +``` + +由于 JSON_MERGE_PRESERVE() 会改变值的类型,如果原始属性值并不是数组,更新的时候就需要谨慎。 diff --git a/src/bone.js b/src/bone.js index 967b9a09..886199ea 100644 --- a/src/bone.js +++ b/src/bone.js @@ -683,9 +683,14 @@ class Bone { */ async jsonMerge(name, jsonValue, options = {}) { const raw = new Raw(`JSON_MERGE_PATCH(${name}, '${JSON.stringify(jsonValue)}')`); - const rows = await this.update({ [name]: raw }, options); - return rows; + const affectedRows = await this.update({ [name]: raw }, options); + return affectedRows; + } + async jsonMergePreserve(name, jsonValue, options = {}) { + const raw = new Raw(`JSON_MERGE_PRESERVE(${name}, '${JSON.stringify(jsonValue)}')`); + const affectedRows = await this.update({ [name]: raw }, options); + return affectedRows; } /** diff --git a/test/integration/suite/json.test.js b/test/integration/suite/json.test.js index 35e321ab..bc9127ce 100644 --- a/test/integration/suite/json.test.js +++ b/test/integration/suite/json.test.js @@ -56,5 +56,20 @@ describe('=> Basic', () => { await gen.reload(); assert.equal(gen.extra.url, 'https://www.taobao.com/?id=1'); }); + + it('bone.jsonMergePreserve(name, values, options) should work', async () => { + const gen = await Gen.create({ name: '章3️⃣疯' }); + assert.equal(gen.name, '章3️⃣疯'); + await gen.update({ extra: { a: 1 } }); + assert.equal(gen.extra.a, 1); + await gen.jsonMergePreserve('extra', { b: 2, a: 3 }); + await gen.reload(); + assert.deepEqual(gen.extra.a, [1, 3]); + + await gen.jsonMerge('extra', { url: 'https://wanxiang.art/?foo=' }); + await gen.jsonMergePreserve('extra', { url: 'https://www.wanxiang.art/?foo=' }); + await gen.reload(); + assert.deepEqual(gen.extra.url, ['https://wanxiang.art/?foo=', 'https://www.wanxiang.art/?foo=']); + }); }); });