refactor: change sort strategy from offset to targetId (#37)
* refactor: change sort strategy from offset to targetId * fix: remove unnecessary query to optimize performance
This commit is contained in:
parent
841249f58c
commit
d1372e273a
@ -85,6 +85,11 @@ resourcer.define({
|
|||||||
name: 'posts.comments',
|
name: 'posts.comments',
|
||||||
actions: actions.associate,
|
actions: actions.associate,
|
||||||
});
|
});
|
||||||
|
resourcer.define({
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'users.posts',
|
||||||
|
actions: actions.associate,
|
||||||
|
});
|
||||||
resourcer.define({
|
resourcer.define({
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'posts.user',
|
name: 'posts.user',
|
||||||
|
@ -26,7 +26,7 @@ describe('get', () => {
|
|||||||
|
|
||||||
afterAll(() => db.close());
|
afterAll(() => db.close());
|
||||||
|
|
||||||
describe.only('sort value initialization', () => {
|
describe('sort value initialization', () => {
|
||||||
it('initialization by bulkCreate', async () => {
|
it('initialization by bulkCreate', async () => {
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
const posts = await Post.findAll({
|
const posts = await Post.findAll({
|
||||||
@ -85,188 +85,165 @@ describe('get', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('sort in whole table', () => {
|
describe('sort in whole table', () => {
|
||||||
it('init sort value', async () => {
|
it('move id=1 to position at id=2', async () => {
|
||||||
const Post = db.getModel('posts');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('move id=1 by offset=1', async () => {
|
|
||||||
const Post = db.getModel('posts');
|
|
||||||
await agent
|
await agent
|
||||||
.post('/posts:sort/1')
|
.post('/posts:sort/1')
|
||||||
.send({
|
.send({
|
||||||
offset: 1,
|
field: 'sort',
|
||||||
|
targetId: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
const post1 = await Post.findByPk(1);
|
const Post = db.getModel('posts');
|
||||||
expect(post1.get('sort')).toBe(1);
|
const posts = await Post.findAll({
|
||||||
|
attributes: ['id', 'sort'],
|
||||||
const post2 = await Post.findByPk(2);
|
order: [['id', 'ASC']]
|
||||||
expect(post2.get('sort')).toBe(0);
|
});
|
||||||
|
expect(posts.map(item => item.get())).toEqual([
|
||||||
|
{ id: 1, sort: 2 },
|
||||||
|
{ id: 2, sort: 1 },
|
||||||
|
{ id: 3, sort: 3 },
|
||||||
|
{ id: 4, sort: 4 },
|
||||||
|
{ id: 5, sort: 5 },
|
||||||
|
{ id: 6, sort: 6 },
|
||||||
|
{ id: 7, sort: 7 },
|
||||||
|
{ id: 8, sort: 8 },
|
||||||
|
{ id: 9, sort: 9 },
|
||||||
|
{ id: 10, sort: 10 }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('move id=1 by offset=9', async () => {
|
it('move id=2 to position at id=1', async () => {
|
||||||
|
await agent
|
||||||
|
.post('/posts:sort/2')
|
||||||
|
.send({
|
||||||
|
field: 'sort',
|
||||||
|
targetId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
|
const posts = await Post.findAll({
|
||||||
|
attributes: ['id', 'sort'],
|
||||||
|
order: [['id', 'ASC']]
|
||||||
|
});
|
||||||
|
expect(posts.map(item => item.get())).toEqual([
|
||||||
|
{ id: 1, sort: 2 },
|
||||||
|
{ id: 2, sort: 1 },
|
||||||
|
{ id: 3, sort: 3 },
|
||||||
|
{ id: 4, sort: 4 },
|
||||||
|
{ id: 5, sort: 5 },
|
||||||
|
{ id: 6, sort: 6 },
|
||||||
|
{ id: 7, sort: 7 },
|
||||||
|
{ id: 8, sort: 8 },
|
||||||
|
{ id: 9, sort: 9 },
|
||||||
|
{ id: 10, sort: 10 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('move id=1 to position at id=10', async () => {
|
||||||
await agent
|
await agent
|
||||||
.post('/posts:sort/1')
|
.post('/posts:sort/1')
|
||||||
.send({
|
.send({
|
||||||
offset: 9,
|
field: 'sort',
|
||||||
|
targetId: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
const post1 = await Post.findByPk(1);
|
|
||||||
expect(post1.get('sort')).toBe(9);
|
|
||||||
|
|
||||||
const post2 = await Post.findByPk(2);
|
|
||||||
expect(post2.get('sort')).toBe(0);
|
|
||||||
|
|
||||||
const post10 = await Post.findByPk(10);
|
|
||||||
expect(post10.get('sort')).toBe(8);
|
|
||||||
|
|
||||||
const post11 = await Post.findByPk(11);
|
|
||||||
expect(post11.get('sort')).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('move id=1 by offset=-1', async () => {
|
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
await agent
|
const posts = await Post.findAll({
|
||||||
.post('/posts:sort/1')
|
attributes: ['id', 'sort'],
|
||||||
.send({
|
order: [['id', 'ASC']]
|
||||||
offset: -1,
|
});
|
||||||
});
|
expect(posts.map(item => item.get())).toEqual([
|
||||||
|
{ id: 1, sort: 10 },
|
||||||
const post1 = await Post.findByPk(1);
|
{ id: 2, sort: 1 },
|
||||||
expect(post1.get('sort')).toBe(0);
|
{ id: 3, sort: 2 },
|
||||||
|
{ id: 4, sort: 3 },
|
||||||
const post2 = await Post.findByPk(2);
|
{ id: 5, sort: 4 },
|
||||||
expect(post2.get('sort')).toBe(1);
|
{ id: 6, sort: 5 },
|
||||||
});
|
{ id: 7, sort: 6 },
|
||||||
|
{ id: 8, sort: 7 },
|
||||||
it('move id=2 by offset=8', async () => {
|
{ id: 9, sort: 8 },
|
||||||
const Post = db.getModel('posts');
|
{ id: 10, sort: 9 }
|
||||||
await agent
|
]);
|
||||||
.post('/posts:sort/2')
|
|
||||||
.send({
|
|
||||||
offset: 8,
|
|
||||||
});
|
|
||||||
|
|
||||||
const post2 = await Post.findByPk(2);
|
|
||||||
expect(post2.get('sort')).toBe(9);
|
|
||||||
|
|
||||||
const post10 = await Post.findByPk(10);
|
|
||||||
expect(post10.get('sort')).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('move id=2 by offset=-1', async () => {
|
|
||||||
const Post = db.getModel('posts');
|
|
||||||
await agent
|
|
||||||
.post('/posts:sort/2')
|
|
||||||
.send({
|
|
||||||
offset: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const post2 = await Post.findByPk(2);
|
|
||||||
expect(post2.get('sort')).toBe(0);
|
|
||||||
|
|
||||||
const post1 = await Post.findByPk(1);
|
|
||||||
expect(post1.get('sort')).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('move id=2 by offset=Infinity', async () => {
|
|
||||||
const Post = db.getModel('posts');
|
|
||||||
await agent
|
|
||||||
.post('/posts:sort/2')
|
|
||||||
.send({
|
|
||||||
offset: 'Infinity',
|
|
||||||
});
|
|
||||||
|
|
||||||
const post1 = await Post.findByPk(1);
|
|
||||||
expect(post1.get('sort')).toBe(0);
|
|
||||||
|
|
||||||
const post2 = await Post.findByPk(2);
|
|
||||||
expect(post2.get('sort')).toBe(10);
|
|
||||||
|
|
||||||
const post10 = await Post.findByPk(10);
|
|
||||||
expect(post10.get('sort')).toBe(9);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('move id=2 by offset=-Infinity', async () => {
|
|
||||||
const Post = db.getModel('posts');
|
|
||||||
await agent
|
|
||||||
.post('/posts:sort/2')
|
|
||||||
.send({
|
|
||||||
offset: '-Infinity',
|
|
||||||
});
|
|
||||||
|
|
||||||
const post1 = await Post.findByPk(1);
|
|
||||||
expect(post1.get('sort')).toBe(0);
|
|
||||||
|
|
||||||
const post2 = await Post.findByPk(2);
|
|
||||||
expect(post2.get('sort')).toBe(-1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sort in filtered scope', () => {
|
describe('sort in filtered scope', () => {
|
||||||
it('move id=1 by offset=3 in scope filter[status]=publish', async () => {
|
it('move id=2 to position at id=8 (same scope value)', async () => {
|
||||||
try {
|
|
||||||
await agent
|
|
||||||
.post('/posts:sort/1?filter[status]=publish')
|
|
||||||
.send({
|
|
||||||
offset: 3,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
expect(error).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在 scope 中的排序无所谓值是否与其他不在 scope 中的重复。
|
|
||||||
it('move id=2 by offset=3 in scope filter[status]=publish', async () => {
|
|
||||||
const Post = db.getModel('posts');
|
|
||||||
await agent
|
await agent
|
||||||
.post('/posts:sort/2?filter[status]=publish')
|
.post('/posts:sort/2')
|
||||||
.send({
|
.send({
|
||||||
offset: 3,
|
field: 'sort_in_status',
|
||||||
|
targetId: 8,
|
||||||
});
|
});
|
||||||
|
|
||||||
const post2 = await Post.findByPk(2);
|
const Post = db.getModel('posts');
|
||||||
expect(post2.get('sort')).toBe(7);
|
const posts = await Post.findAll({
|
||||||
|
where: {
|
||||||
|
status: 'publish'
|
||||||
|
},
|
||||||
|
attributes: ['id', 'sort_in_status'],
|
||||||
|
order: [['id', 'ASC']]
|
||||||
|
});
|
||||||
|
expect(posts.map(item => item.get())).toEqual([
|
||||||
|
{ id: 2, sort_in_status: 4 },
|
||||||
|
{ id: 4, sort_in_status: 1 },
|
||||||
|
{ id: 6, sort_in_status: 2 },
|
||||||
|
{ id: 8, sort_in_status: 3 },
|
||||||
|
{ id: 10, sort_in_status: 5 }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('move id=2 by offset=Infinity in scope filter[status]=publish', async () => {
|
it('move id=1 to position at id=8 (different scope value)', async () => {
|
||||||
await agent
|
await agent
|
||||||
.post('/posts:sort/2?filter[status]=publish')
|
.post('/posts:sort/1')
|
||||||
.send({
|
.send({
|
||||||
offset: 'Infinity',
|
field: 'sort_in_status',
|
||||||
|
targetId: 8,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
const post2 = await Post.findByPk(2);
|
const posts = await Post.findAll({
|
||||||
expect(post2.get('sort')).toBe(10);
|
where: {
|
||||||
|
status: 'publish'
|
||||||
|
},
|
||||||
|
attributes: ['id', 'sort_in_status'],
|
||||||
|
order: [['id', 'ASC']]
|
||||||
|
});
|
||||||
|
expect(posts.map(item => item.get())).toEqual([
|
||||||
|
{ id: 1, sort_in_status: 4 },
|
||||||
|
{ id: 2, sort_in_status: 1 },
|
||||||
|
{ id: 4, sort_in_status: 2 },
|
||||||
|
{ id: 6, sort_in_status: 3 },
|
||||||
|
{ id: 8, sort_in_status: 5 },
|
||||||
|
{ id: 10, sort_in_status: 6 }
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('associations', () => {
|
describe('associations', () => {
|
||||||
describe('hasMany', () => {
|
describe('hasMany', () => {
|
||||||
it('sort only 1 item in group will never change', async () => {
|
it('move id=1 to position at id=3 (different scope value)', async () => {
|
||||||
await agent
|
await agent
|
||||||
.post('/posts/2/comments:sort/1')
|
.post('/users/1/posts:sort/1')
|
||||||
.send({
|
.send({
|
||||||
offset: 1
|
field: 'sort_in_user',
|
||||||
|
targetId: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Comment = db.getModel('comments');
|
|
||||||
const comment1 = await Comment.findByPk(1);
|
|
||||||
expect(comment1.get('sort')).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('/posts/5/comments:sort/7', async () => {
|
const Post = db.getModel('posts');
|
||||||
await agent
|
const posts = await Post.findAll({
|
||||||
.post('/posts/5/comments:sort/7')
|
where: {
|
||||||
.send({
|
user_id: 3
|
||||||
offset: 1
|
},
|
||||||
});
|
attributes: ['id', 'sort_in_user'],
|
||||||
|
order: [['id', 'ASC']]
|
||||||
const Comment = db.getModel('comments');
|
});
|
||||||
const comment7 = await Comment.findByPk(7);
|
|
||||||
expect(comment7.get('sort')).toBe(1);
|
expect(posts.map(item => item.get())).toEqual([
|
||||||
|
{ id: 1, sort_in_user: 1 },
|
||||||
|
{ id: 3, sort_in_user: 2 },
|
||||||
|
{ id: 10, sort_in_user: 3 },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Utils, Op } from 'sequelize';
|
||||||
|
import _ from 'lodash';
|
||||||
import { Context, Next } from '.';
|
import { Context, Next } from '.';
|
||||||
import {
|
import {
|
||||||
Model,
|
Model,
|
||||||
@ -5,10 +7,9 @@ import {
|
|||||||
HASMANY,
|
HASMANY,
|
||||||
BELONGSTO,
|
BELONGSTO,
|
||||||
BELONGSTOMANY,
|
BELONGSTOMANY,
|
||||||
|
whereCompare
|
||||||
} from '@nocobase/database';
|
} from '@nocobase/database';
|
||||||
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '@nocobase/resourcer';
|
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '@nocobase/resourcer';
|
||||||
import { Utils, Op } from 'sequelize';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { filterByFields } from '../utils';
|
import { filterByFields } from '../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,7 +374,6 @@ export async function sort(ctx: Context, next: Next) {
|
|||||||
associatedName,
|
associatedName,
|
||||||
associatedKey,
|
associatedKey,
|
||||||
associated,
|
associated,
|
||||||
filter = {},
|
|
||||||
values
|
values
|
||||||
} = ctx.action.params;
|
} = ctx.action.params;
|
||||||
|
|
||||||
@ -390,114 +390,94 @@ export async function sort(ctx: Context, next: Next) {
|
|||||||
const Model = ctx.db.getModel(resourceName);
|
const Model = ctx.db.getModel(resourceName);
|
||||||
const table = ctx.db.getTable(resourceName);
|
const table = ctx.db.getTable(resourceName);
|
||||||
|
|
||||||
if (!values.offset) {
|
const { field, targetId } = values;
|
||||||
|
if (!values.field || typeof targetId === 'undefined') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const [primaryField] = Model.primaryKeyAttributes;
|
const sortField = table.getField(field);
|
||||||
const sortField = values.field || table.getOptions().sortField || 'sort';
|
if (!sortField) {
|
||||||
|
return next();
|
||||||
// offset 的有效值为:整型 | 'Infinity' | '-Infinity'
|
}
|
||||||
const offset = Number(values.offset);
|
const { primaryKeyAttribute } = Model;
|
||||||
const sign = offset < 0 ? {
|
const { name: sortAttr, scope = [] } = sortField.options;
|
||||||
op: Op.lte,
|
|
||||||
order: 'DESC',
|
|
||||||
direction: 1,
|
|
||||||
extremum: 'min'
|
|
||||||
} : {
|
|
||||||
op: Op.gte,
|
|
||||||
order: 'ASC',
|
|
||||||
direction: -1,
|
|
||||||
extremum: 'max'
|
|
||||||
};
|
|
||||||
|
|
||||||
const transaction = await ctx.db.sequelize.transaction();
|
const transaction = await ctx.db.sequelize.transaction();
|
||||||
|
|
||||||
const { where = {} } = Model.parseApiJson({ filter });
|
const where = {};
|
||||||
if (associated && resourceField instanceof HASMANY) {
|
if (associated && resourceField instanceof HASMANY) {
|
||||||
where[resourceField.options.foreignKey] = associatedKey;
|
where[resourceField.options.foreignKey] = associatedKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到操作对象
|
// 找到操作对象
|
||||||
const operand = await Model.findOne({
|
const source = await Model.findOne({
|
||||||
// 这里增加 where 条件是要求如果有 filter 条件,就应该在同条件的组中排序,不是同条件组的报错处理。
|
|
||||||
where: {
|
where: {
|
||||||
...where,
|
...where,
|
||||||
[primaryField]: resourceKey
|
[primaryKeyAttribute]: resourceKey
|
||||||
},
|
},
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
if (!source) {
|
||||||
if (!operand) {
|
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
// TODO: 错误需要后面统一处理
|
throw new Error(`resource(${resourceKey}) does not exist`);
|
||||||
throw new Error(`resource(${resourceKey}) with filter does not exist`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let target;
|
const target = await Model.findByPk(targetId, { transaction });
|
||||||
|
|
||||||
// 如果是有限的变动值
|
if (!target) {
|
||||||
if (Number.isFinite(offset)) {
|
await transaction.rollback();
|
||||||
const absChange = Math.abs(offset);
|
throw new Error(`resource(${targetId}) does not exist`);
|
||||||
const group = await Model.findAll({
|
}
|
||||||
where: {
|
|
||||||
...where,
|
const sourceScopeWhere = source.getScopeWhere(scope);
|
||||||
[primaryField]: {
|
const targetScopeWhere = target.getScopeWhere(scope);
|
||||||
[Op.ne]: resourceKey
|
const sameScope = whereCompare(sourceScopeWhere, targetScopeWhere);
|
||||||
},
|
|
||||||
[sortField]: {
|
let increment;
|
||||||
[sign.op]: operand[sortField]
|
const updateWhere = { ...targetScopeWhere };
|
||||||
}
|
if (sameScope) {
|
||||||
},
|
const direction = source[sortAttr] < target[sortAttr] ? {
|
||||||
limit: absChange,
|
sourceOp: Op.gt,
|
||||||
// offset: 0,
|
targetOp: Op.lte,
|
||||||
attributes: [primaryField, sortField],
|
increment: -1
|
||||||
order: [
|
} : {
|
||||||
[sortField, sign.order]
|
sourceOp: Op.lt,
|
||||||
],
|
targetOp: Op.gte,
|
||||||
transaction
|
increment: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
increment = direction.increment;
|
||||||
|
|
||||||
|
Object.assign(updateWhere, {
|
||||||
|
[sortAttr]: {
|
||||||
|
[direction.sourceOp]: source[sortAttr],
|
||||||
|
[direction.targetOp]: target[sortAttr]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
increment = 1;
|
||||||
|
Object.assign(updateWhere, {
|
||||||
|
[sortAttr]: {
|
||||||
|
[Op.gte]: target[sortAttr]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!group.length) {
|
|
||||||
// 如果变动范围内的元素数比范围小
|
|
||||||
// 说明全部数据不足一页
|
|
||||||
// target = group[0][priorityKey] - sign.direction;
|
|
||||||
// 没有元素无需变动
|
|
||||||
await transaction.commit();
|
|
||||||
ctx.body = operand;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果变动范围内都有元素(可能出现 limit 范围内元素不足的情况)
|
|
||||||
if (group.length === absChange) {
|
|
||||||
target = group[group.length - 1][sortField];
|
|
||||||
|
|
||||||
await Model.increment(sortField, {
|
|
||||||
by: sign.direction,
|
|
||||||
where: {
|
|
||||||
[primaryField]: {
|
|
||||||
[Op.in]: group.map(item => item[primaryField])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 如果要求置顶或沉底(未在上一过程中计算出目标值)
|
|
||||||
if (typeof target === 'undefined') {
|
await Model.increment(sortAttr, {
|
||||||
target = await Model[sign.extremum](sortField, {
|
by: increment,
|
||||||
where,
|
where: updateWhere,
|
||||||
transaction
|
transaction
|
||||||
}) - sign.direction;
|
});
|
||||||
}
|
|
||||||
await operand.update({
|
await source.update({
|
||||||
[sortField]: target
|
[sortAttr]: target[sortAttr],
|
||||||
|
...targetScopeWhere
|
||||||
}, {
|
}, {
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
|
||||||
ctx.body = operand;
|
ctx.body = source;
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
@ -259,7 +259,7 @@ export abstract class Model extends SequelizeModel {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScopeWhere(scope: string[]) {
|
getScopeWhere(scope: string[] = []) {
|
||||||
const Model = this.constructor as ModelCtor<Model>;
|
const Model = this.constructor as ModelCtor<Model>;
|
||||||
const table = this.database.getTable(this.constructor.name);
|
const table = this.database.getTable(this.constructor.name);
|
||||||
const associations = table.getAssociations();
|
const associations = table.getAssociations();
|
||||||
|
Loading…
Reference in New Issue
Block a user