Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: bone.jsonMerge() & bone.jsonMergePreserve() #425

Merged
merged 1 commit into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_layouts/zh.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<li><a href="{{ '/zh/validations' | relative_url }}">数据校验</a></li>
<li><a href="{{ '/zh/associations' | relative_url }}">关联关系</a></li>
<li><a href="{{ '/zh/querying' | relative_url }}">查询接口</a></li>
<li><a href="{{ '/zh/json' | relative_url }}">JSON 字段</a></li>
<li><a href="{{ '/zh/hooks' | relative_url }}">钩子</a></li>
<li><a href="{{ '/zh/logging' | relative_url }}">日志</a></li>
<li><a href="{{ '/zh/types' | relative_url }}">TypeScript 支持</a></li>
Expand Down
4 changes: 2 additions & 2 deletions docs/assets/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
84 changes: 84 additions & 0 deletions docs/zh/json.md
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
}
```

## 查询

可以使用 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() 会改变值的类型,如果原始属性值并不是数组,更新的时候就需要谨慎。
9 changes: 7 additions & 2 deletions src/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
15 changes: 15 additions & 0 deletions test/integration/suite/json.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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=']);
});
});
});
Loading