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=']);
+ });
});
});