Fix/model update associations (#29)
* refactor: change updateAssociations from set null to small grouped handling * feat: add transaction for updateAssociations * test: add more basic cases * fix: pick options for different model methods * fix: adjust options picking strategy
This commit is contained in:
parent
dd1d4fc7bf
commit
1980464f63
@ -24,7 +24,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
logging: false,
|
logging: process.env.DB_LOG_SQL === 'on'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDatabase() {
|
export function getDatabase() {
|
||||||
|
@ -14,7 +14,7 @@ afterEach(async () => {
|
|||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('model', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
db.table({
|
db.table({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
@ -79,11 +79,11 @@ describe('actions', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'hasmany',
|
type: 'hasMany',
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'hasmany',
|
type: 'hasMany',
|
||||||
name: 'current_user_comments',
|
name: 'current_user_comments',
|
||||||
target: 'comments',
|
target: 'comments',
|
||||||
},
|
},
|
||||||
@ -201,32 +201,267 @@ describe('actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('through attributes', async () => {
|
describe('.updateAssociations', () => {
|
||||||
const [Post, Tag] = db.getModels(['posts', 'tags']);
|
describe('belongsTo', () => {
|
||||||
const post = await Post.create();
|
it('update with primary key', async () => {
|
||||||
const tag = await Tag.create();
|
const [User, Post] = db.getModels(['users', 'posts']);
|
||||||
await post.updateAssociations({
|
const user = await User.create();
|
||||||
tags: [{
|
const post = await Post.create();
|
||||||
name: 'xxx',
|
await post.updateAssociations({
|
||||||
posts_tags: {
|
user: user.id
|
||||||
name: 'name134',
|
});
|
||||||
}
|
|
||||||
}, {
|
const authorizedPost = await Post.findByPk(post.id);
|
||||||
id: tag.id,
|
expect(authorizedPost.user_id).toBe(user.id);
|
||||||
posts_tags: {
|
});
|
||||||
name: 'name234',
|
|
||||||
}
|
it('update with new object', async () => {
|
||||||
}],
|
const Post = db.getModel('posts');
|
||||||
|
const post = await Post.create();
|
||||||
|
await post.updateAssociations({
|
||||||
|
user: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorizedPost = await Post.findByPk(post.id);
|
||||||
|
expect(authorizedPost.user_id).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update with new model', async () => {
|
||||||
|
const [User, Post] = db.getModels(['users', 'posts']);
|
||||||
|
const user = await User.create();
|
||||||
|
const post = await Post.create();
|
||||||
|
await post.updateAssociations({
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorizedPost = await Post.findByPk(post.id);
|
||||||
|
expect(authorizedPost.user_id).toBe(user.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const PostTag = db.getModel('posts_tags');
|
|
||||||
const [t1, t2] = await PostTag.findAll({
|
describe('hasMany', () => {
|
||||||
where: {
|
it('update with primary key', async () => {
|
||||||
post_id: post.id,
|
const [Post, Comment] = db.getModels(['posts', 'comments']);
|
||||||
},
|
const post = await Post.create();
|
||||||
order: ['tag_id'],
|
const comments = await Comment.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments: comments.map(item => item.id)
|
||||||
|
});
|
||||||
|
const postComments = await Comment.findAll({
|
||||||
|
where: { post_id: post.id },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
expect(postComments.map(item => item.id)).toEqual([1,2,3,4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update with new object', async () => {
|
||||||
|
const [Post, Comment] = db.getModels(['posts', 'comments']);
|
||||||
|
const post = await Post.create();
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments: [{},{},{},{}]
|
||||||
|
});
|
||||||
|
const postCommentIds = await Comment.findAll({
|
||||||
|
where: { post_id: post.id },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
expect(postCommentIds.map(item => item.id)).toEqual([1,2,3,4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update with new model', async () => {
|
||||||
|
const [Post, Comment] = db.getModels(['posts', 'comments']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const comments = await Comment.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments
|
||||||
|
});
|
||||||
|
const postCommentIds = await Comment.findAll({
|
||||||
|
where: { post_id: post.id },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
expect(postCommentIds.map(item => item.id)).toEqual([1,2,3,4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update with exist rows/primaryKeys', async () => {
|
||||||
|
const [Post, Comment] = db.getModels(['posts', 'comments']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const comments = await Comment.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments
|
||||||
|
});
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments
|
||||||
|
});
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments: comments.map(item => item.id)
|
||||||
|
});
|
||||||
|
const postCommentIds = await Comment.findAll({
|
||||||
|
where: { post_id: post.id },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
expect(postCommentIds.map(item => item.id)).toEqual([1,2,3,4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update with exist objects', async () => {
|
||||||
|
const [Post, Comment] = db.getModels(['posts', 'comments']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const comments = await Comment.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments
|
||||||
|
});
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments: comments.map(item => ({
|
||||||
|
...item.get(),
|
||||||
|
content: `content${item.id}`
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
const postComments = await Comment.findAll({
|
||||||
|
where: { post_id: post.id },
|
||||||
|
attributes: ['id', 'content']
|
||||||
|
});
|
||||||
|
expect(postComments.map(({ id, content }) => ({ id, content }))).toEqual([
|
||||||
|
{ id: 1, content: 'content1' },
|
||||||
|
{ id: 2, content: 'content2' },
|
||||||
|
{ id: 3, content: 'content3' },
|
||||||
|
{ id: 4, content: 'content4' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update another with exist objects', async () => {
|
||||||
|
const [Post, Comment] = db.getModels(['posts', 'comments']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const post2 = await Post.create();
|
||||||
|
const comments = await Comment.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
comments
|
||||||
|
});
|
||||||
|
const postComments = await Comment.findAll({
|
||||||
|
where: { post_id: post.id }
|
||||||
|
});
|
||||||
|
expect(postComments.map(({ id, post_id }) => ({ id, post_id }))).toEqual([
|
||||||
|
{ id: 1, post_id: post.id },
|
||||||
|
{ id: 2, post_id: post.id },
|
||||||
|
{ id: 3, post_id: post.id },
|
||||||
|
{ id: 4, post_id: post.id }
|
||||||
|
]);
|
||||||
|
|
||||||
|
await post2.updateAssociations({
|
||||||
|
comments: postComments.map(item => ({
|
||||||
|
...item.get(),
|
||||||
|
content: `content${item.id}`
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
const updatedComments = await Comment.findAll();
|
||||||
|
console.log(updatedComments);
|
||||||
|
const post1CommentsCount = await Comment.count({
|
||||||
|
where: { post_id: post.id }
|
||||||
|
});
|
||||||
|
expect(post1CommentsCount).toBe(0);
|
||||||
|
|
||||||
|
const post2Comments = await Comment.findAll({
|
||||||
|
where: { post_id: post2.id },
|
||||||
|
attributes: ['id', 'content']
|
||||||
|
});
|
||||||
|
expect(post2Comments.map(({ id, content }) => ({ id, content }))).toEqual([
|
||||||
|
{ id: 1, content: 'content1' },
|
||||||
|
{ id: 2, content: 'content2' },
|
||||||
|
{ id: 3, content: 'content3' },
|
||||||
|
{ id: 4, content: 'content4' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('belongsToMany', () => {
|
||||||
|
it('update with primary key', async () => {
|
||||||
|
const [Post, Tag, PostTag] = db.getModels(['posts', 'tags', 'posts_tags']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const tags = await Tag.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
tags: tags.map(item => item.id)
|
||||||
|
});
|
||||||
|
const tagged = await PostTag.findAll({
|
||||||
|
where: { post_id: post.id },
|
||||||
|
attributes: ['tag_id']
|
||||||
|
});
|
||||||
|
expect(tagged.map(item => item.tag_id)).toEqual([1,2,3,4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update with exist rows/primaryKeys', async () => {
|
||||||
|
const [Post, Tag, PostTag] = db.getModels(['posts', 'tags', 'posts_tags']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const tags = await Tag.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
tags: tags.map(item => item.id)
|
||||||
|
});
|
||||||
|
await post.updateAssociations({
|
||||||
|
tags: tags.map(item => item.id)
|
||||||
|
});
|
||||||
|
await post.updateAssociations({
|
||||||
|
tags
|
||||||
|
});
|
||||||
|
const tagged = await PostTag.findAll({
|
||||||
|
where: { post_id: post.id },
|
||||||
|
attributes: ['tag_id', 'post_id']
|
||||||
|
});
|
||||||
|
expect(tagged.map(({ post_id, tag_id }) => ({ post_id, tag_id }))).toEqual([
|
||||||
|
{ tag_id: 1, post_id: 1 },
|
||||||
|
{ tag_id: 2, post_id: 1 },
|
||||||
|
{ tag_id: 3, post_id: 1 },
|
||||||
|
{ tag_id: 4, post_id: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update other with exist rows/primaryKeys', async () => {
|
||||||
|
const [Post, Tag, PostTag] = db.getModels(['posts', 'tags', 'posts_tags']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const post2 = await Post.create();
|
||||||
|
const tags = await Tag.bulkCreate([{}, {}, {}, {}]);
|
||||||
|
await post.updateAssociations({
|
||||||
|
tags: tags.map(item => item.id)
|
||||||
|
});
|
||||||
|
await post2.updateAssociations({
|
||||||
|
tags
|
||||||
|
});
|
||||||
|
const tagged = await PostTag.findAll();
|
||||||
|
expect(tagged.map(({ post_id, tag_id }) => ({ post_id, tag_id }))).toEqual([
|
||||||
|
{ tag_id: 1, post_id: 1 },
|
||||||
|
{ tag_id: 2, post_id: 1 },
|
||||||
|
{ tag_id: 3, post_id: 1 },
|
||||||
|
{ tag_id: 4, post_id: 1 },
|
||||||
|
{ tag_id: 1, post_id: 2 },
|
||||||
|
{ tag_id: 2, post_id: 2 },
|
||||||
|
{ tag_id: 3, post_id: 2 },
|
||||||
|
{ tag_id: 4, post_id: 2 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('through attributes', async () => {
|
||||||
|
const [Post, Tag] = db.getModels(['posts', 'tags']);
|
||||||
|
const post = await Post.create();
|
||||||
|
const tag = await Tag.create();
|
||||||
|
await post.updateAssociations({
|
||||||
|
tags: [{
|
||||||
|
name: 'xxx',
|
||||||
|
posts_tags: {
|
||||||
|
name: 'name134',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
id: tag.id,
|
||||||
|
posts_tags: {
|
||||||
|
name: 'name234',
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const PostTag = db.getModel('posts_tags');
|
||||||
|
const [t1, t2] = await PostTag.findAll({
|
||||||
|
where: {
|
||||||
|
post_id: post.id,
|
||||||
|
},
|
||||||
|
order: ['tag_id'],
|
||||||
|
});
|
||||||
|
expect(t1.name).toBe('name234');
|
||||||
|
expect(t2.name).toBe('name134');
|
||||||
});
|
});
|
||||||
expect(t1.name).toBe('name234');
|
|
||||||
expect(t2.name).toBe('name134');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('scope', () => {
|
describe('scope', () => {
|
||||||
@ -262,8 +497,12 @@ describe('actions', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
const comments = await post.getCurrent_user_comments();
|
const comments = await post.getCurrent_user_comments();
|
||||||
// TODO: no expect
|
// TODO: no expect
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -925,25 +1164,26 @@ describe('belongsToMany', () => {
|
|||||||
tag2 = await Tag.create({name: 'tag2'});
|
tag2 = await Tag.create({name: 'tag2'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('@', async () => {
|
it('update with targetKey', async () => {
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
tags: tag1.name,
|
tags: tag1.name,
|
||||||
});
|
});
|
||||||
expect(await post.countTags()).toBe(1);
|
expect(await post.countTags()).toBe(1);
|
||||||
});
|
});
|
||||||
it('@', async () => {
|
// TODO(question)
|
||||||
|
it.skip('update with primaryKey (defined targetKey)', async () => {
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
tags: tag2.id,
|
tags: tag2.id,
|
||||||
});
|
});
|
||||||
expect(await post.countTags()).toBe(1);
|
expect(await post.countTags()).toBe(1);
|
||||||
});
|
});
|
||||||
it('@', async () => {
|
it('update with model', async () => {
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
tags: [tag1, tag2],
|
tags: [tag1, tag2],
|
||||||
});
|
});
|
||||||
expect(await post.countTags()).toBe(2);
|
expect(await post.countTags()).toBe(2);
|
||||||
});
|
});
|
||||||
it('@', async () => {
|
it('update with targetKey', async () => {
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
tags: {
|
tags: {
|
||||||
name: 'tag2',
|
name: 'tag2',
|
||||||
@ -952,7 +1192,7 @@ describe('belongsToMany', () => {
|
|||||||
expect(await post.countTags()).toBe(1);
|
expect(await post.countTags()).toBe(1);
|
||||||
expect((await post.getTags())[0].id).toBe(tag2.id);
|
expect((await post.getTags())[0].id).toBe(tag2.id);
|
||||||
});
|
});
|
||||||
it('@', async () => {
|
it('update with new object', async () => {
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
tags: [{
|
tags: [{
|
||||||
name: 'tag3',
|
name: 'tag3',
|
||||||
|
@ -253,6 +253,230 @@ export abstract class Model extends SequelizeModel {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSingleAssociation(key: string, data: any, options: SaveOptions<any> & { context?: any; } = {}) {
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
transaction = await this.sequelize.transaction(),
|
||||||
|
...opts
|
||||||
|
} = options;
|
||||||
|
Object.assign(opts, { transaction });
|
||||||
|
|
||||||
|
const table = this.database.getTable(this.constructor.name);
|
||||||
|
const association = table.getAssociations().get(key);
|
||||||
|
const accessors = association.getAccessors();
|
||||||
|
|
||||||
|
if (typeof data === 'number' || typeof data === 'string' || data instanceof SequelizeModel) {
|
||||||
|
await this[accessors.set](data, opts);
|
||||||
|
} else if (typeof data === 'object') {
|
||||||
|
const Target = association.getTargetModel();
|
||||||
|
const targetAttribute = association instanceof BelongsTo
|
||||||
|
? association.options.targetKey
|
||||||
|
: association.options.sourceKey;
|
||||||
|
if (data[targetAttribute]) {
|
||||||
|
await this[accessors.set](data[targetAttribute], opts);
|
||||||
|
if (Object.keys(data).length > 1) {
|
||||||
|
const target = await Target.findOne({
|
||||||
|
where: {
|
||||||
|
[targetAttribute]: data[targetAttribute],
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
await target.update(data, opts);
|
||||||
|
// @ts-ignore
|
||||||
|
await target.updateAssociations(data, opts);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const t = await this[accessors.create](data, opts);
|
||||||
|
await t.updateAssociations(data, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!options.transaction) {
|
||||||
|
await transaction.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMultipleAssociation(associationName: string, data: any, options: SaveOptions<any> & { context?: any; } = {}) {
|
||||||
|
const items = Array.isArray(data) ? data : [data];
|
||||||
|
if (!items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
transaction = await this.sequelize.transaction(),
|
||||||
|
...opts
|
||||||
|
} = options;
|
||||||
|
Object.assign(opts, { transaction });
|
||||||
|
|
||||||
|
const table = this.database.getTable(this.constructor.name);
|
||||||
|
const association = table.getAssociations().get(associationName);
|
||||||
|
const accessors = association.getAccessors();
|
||||||
|
const Target = association.getTargetModel();
|
||||||
|
// 当前表关联 target 表的外键(大部分情况与 target 表主键相同,但可以设置为不同的,要考虑)
|
||||||
|
const { targetKey = Target.primaryKeyAttribute } = association.options;
|
||||||
|
// target 表的主键
|
||||||
|
const targetPk = Target.primaryKeyAttribute;
|
||||||
|
const targetKeyIsPk = targetKey === targetPk;
|
||||||
|
// 准备设置的关联主键
|
||||||
|
const toSetPks = new Set();
|
||||||
|
const toSetUks = new Set();
|
||||||
|
// 筛选后准备添加的关联主键
|
||||||
|
const toAddItems = new Set();
|
||||||
|
// 准备添加的关联对象
|
||||||
|
const toUpsertObjects = [];
|
||||||
|
|
||||||
|
// 遍历所有值成员准备数据
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item instanceof SequelizeModel) {
|
||||||
|
if (targetKeyIsPk) {
|
||||||
|
toSetPks.add(item.getDataValue(targetPk));
|
||||||
|
} else {
|
||||||
|
toSetUks.add(item.getDataValue(targetKey));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof item === 'number' || typeof item === 'string') {
|
||||||
|
let targetKeyType = getDataTypeKey(Target.rawAttributes[targetKey].type).toLocaleLowerCase();
|
||||||
|
if (targetKeyType === 'integer') {
|
||||||
|
targetKeyType = 'number';
|
||||||
|
}
|
||||||
|
// 如果传值类型与之前在 Model 上定义的 targetKey 不同,则报错。
|
||||||
|
// 不应兼容定义的 targetKey 不是 primaryKey 却传了 primaryKey 的值的情况。
|
||||||
|
if (typeof item !== targetKeyType) {
|
||||||
|
throw new Error(`target key type [${typeof item}] does not match to [${targetKeyType}]`);
|
||||||
|
}
|
||||||
|
if (targetKeyIsPk) {
|
||||||
|
toSetPks.add(item);
|
||||||
|
} else {
|
||||||
|
toSetUks.add(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
toUpsertObjects.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 仅传关联键处理开始 */
|
||||||
|
// 查找已存在的关联数据
|
||||||
|
const byPkExistItems = toSetPks.size ? await this[accessors.get]({
|
||||||
|
...opts,
|
||||||
|
where: {
|
||||||
|
[targetPk]: {
|
||||||
|
[Op.in]: Array.from(toSetPks)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes: [targetPk]
|
||||||
|
}) : [];
|
||||||
|
const pkExistItems = new Map();
|
||||||
|
byPkExistItems.forEach(item => {
|
||||||
|
pkExistItems.set(item[targetPk], item);
|
||||||
|
});
|
||||||
|
for (const key of toSetPks) {
|
||||||
|
if (!pkExistItems.has(key)) {
|
||||||
|
toAddItems.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const byUkExistItems = await this[accessors.get]({
|
||||||
|
...opts,
|
||||||
|
where: {
|
||||||
|
[targetKey]: {
|
||||||
|
[Op.in]: Array.from(toSetUks)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes: [targetPk, targetKey],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
const ukExistItems = new Map();
|
||||||
|
byUkExistItems.forEach(item => {
|
||||||
|
ukExistItems.set(item[targetKey], item);
|
||||||
|
});
|
||||||
|
for (const key of toSetUks) {
|
||||||
|
if (ukExistItems.has(key)) {
|
||||||
|
toSetUks.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const byUkItems = toSetUks.size ? await Target.findAll({
|
||||||
|
...opts,
|
||||||
|
// @ts-ignore
|
||||||
|
where: {
|
||||||
|
[targetKey]: {
|
||||||
|
[Op.in]: Array.from(toSetUks)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes: [targetPk, targetKey]
|
||||||
|
}) : [];
|
||||||
|
byUkItems.forEach(item => {
|
||||||
|
toAddItems.add(item);
|
||||||
|
});
|
||||||
|
/* 仅传关联键处理结束 */
|
||||||
|
|
||||||
|
/* 值为对象处理开始 */
|
||||||
|
for (const item of toUpsertObjects) {
|
||||||
|
let target;
|
||||||
|
if (typeof item[targetKey] === 'undefined') {
|
||||||
|
// TODO(optimize): 不确定 bulkCreate 的结果是否能保证顺序,能保证的话这里可以优化为批量处理
|
||||||
|
target = await Target.create(item, opts);
|
||||||
|
} else {
|
||||||
|
let created: boolean;
|
||||||
|
[target, created] = await Target.findOrCreate({
|
||||||
|
where: { [targetKey]: item[targetKey] },
|
||||||
|
defaults: item,
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!created) {
|
||||||
|
await target.update(item, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (association instanceof BelongsToMany) {
|
||||||
|
// TODO(optimize): 这里暂时未能批量执行
|
||||||
|
await this[accessors.add](target, opts);
|
||||||
|
const ThroughModel = association.getThroughModel();
|
||||||
|
const throughName = association.getThroughName();
|
||||||
|
const throughValues = item[throughName];
|
||||||
|
if (typeof throughValues === 'object') {
|
||||||
|
const { foreignKey, sourceKey, otherKey } = association.options;
|
||||||
|
const through = await ThroughModel.findOne({
|
||||||
|
where: {
|
||||||
|
[foreignKey]: this.get(sourceKey),
|
||||||
|
[otherKey]: target.get(targetKey),
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
await through.update(throughValues, opts);
|
||||||
|
await through.updateAssociations(throughValues, opts);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toAddItems.add(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
await target.updateAssociations(item, opts);
|
||||||
|
}
|
||||||
|
/* 值为对象处理结束 */
|
||||||
|
|
||||||
|
// 添加所有计算后的关联
|
||||||
|
await this[accessors.add](Array.from(toAddItems), opts);
|
||||||
|
|
||||||
|
if (!options.transaction) {
|
||||||
|
await transaction.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAssociation(key: string, data: any, options: SaveOptions<any> & { context?: any; }) {
|
||||||
|
const table = this.database.getTable(this.constructor.name);
|
||||||
|
const association = table.getAssociations().get(key);
|
||||||
|
switch (true) {
|
||||||
|
case association instanceof BelongsTo:
|
||||||
|
case association instanceof HasOne:
|
||||||
|
return this.updateSingleAssociation(key, data, options);
|
||||||
|
case association instanceof HasMany:
|
||||||
|
case association instanceof BelongsToMany:
|
||||||
|
return this.updateMultipleAssociation(key, data, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关联数据的更新
|
* 关联数据的更新
|
||||||
*
|
*
|
||||||
@ -260,166 +484,21 @@ export abstract class Model extends SequelizeModel {
|
|||||||
*
|
*
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
async updateAssociations(data: any, options?: SaveOptions & { context?: any }) {
|
async updateAssociations(data: any, options: SaveOptions & { context?: any } = {}) {
|
||||||
const model = this;
|
const { transaction = await this.sequelize.transaction() } = options;
|
||||||
const name = this.constructor.name;
|
const table = this.database.getTable(this.constructor.name);
|
||||||
const table = this.database.getTable(name);
|
for (const key of table.getAssociations().keys()) {
|
||||||
for (const [key, association] of table.getAssociations()) {
|
|
||||||
if (!data[key]) {
|
if (!data[key]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let item = data[key];
|
await this.updateAssociation(key, data[key], {
|
||||||
const accessors = association.getAccessors();
|
...options,
|
||||||
if (association instanceof BelongsTo || association instanceof HasOne) {
|
transaction
|
||||||
if (typeof item === 'number' || typeof item === 'string') {
|
});
|
||||||
await model[accessors.set](item, options);
|
}
|
||||||
continue;
|
|
||||||
}
|
if (!options.transaction) {
|
||||||
if (item instanceof SequelizeModel) {
|
await transaction.commit();
|
||||||
await model[accessors.set](item, options);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (typeof item !== 'object') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const Target = association.getTargetModel();
|
|
||||||
const targetAttribute = association instanceof BelongsTo
|
|
||||||
? association.options.targetKey
|
|
||||||
: association.options.sourceKey;
|
|
||||||
if (item[targetAttribute]) {
|
|
||||||
await model[accessors.set](item[targetAttribute], options);
|
|
||||||
if (Object.keys(item).length > 1) {
|
|
||||||
const target = await Target.findOne({
|
|
||||||
where: {
|
|
||||||
[targetAttribute]: item[targetAttribute],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await target.update(item, options);
|
|
||||||
// @ts-ignore
|
|
||||||
await target.updateAssociations(item, options);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const t = await model[accessors.create](item, options);
|
|
||||||
await t.updateAssociations(item, options);
|
|
||||||
}
|
|
||||||
if (association instanceof HasMany || association instanceof BelongsToMany) {
|
|
||||||
if (!Array.isArray(item)) {
|
|
||||||
item = [item];
|
|
||||||
}
|
|
||||||
if (item.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await model[accessors.set](null, options);
|
|
||||||
const Target = association.getTargetModel();
|
|
||||||
await Promise.all(item.map(async value => {
|
|
||||||
let target: SequelizeModel;
|
|
||||||
let targetKey: string;
|
|
||||||
// 支持 number 和 string 类型的字段作为关联字段
|
|
||||||
if (typeof value === 'number' || typeof value === 'string') {
|
|
||||||
targetKey = (association instanceof BelongsToMany ? association.options.targetKey : Target.primaryKeyAttribute) as string;
|
|
||||||
let targetKeyType = getDataTypeKey(Target.rawAttributes[targetKey].type).toLocaleLowerCase();
|
|
||||||
if (targetKeyType === 'integer') {
|
|
||||||
targetKeyType = 'number';
|
|
||||||
}
|
|
||||||
let primaryKeyType = getDataTypeKey(Target.rawAttributes[Target.primaryKeyAttribute].type).toLocaleLowerCase();
|
|
||||||
if (primaryKeyType === 'integer') {
|
|
||||||
primaryKeyType = 'number';
|
|
||||||
}
|
|
||||||
if (typeof value === targetKeyType) {
|
|
||||||
target = await Target.findOne({
|
|
||||||
where: {
|
|
||||||
[targetKey] : value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Target.primaryKeyAttribute !== targetKey && !target && typeof value === primaryKeyType) {
|
|
||||||
target = await Target.findOne({
|
|
||||||
where: {
|
|
||||||
[Target.primaryKeyAttribute] : value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!target) {
|
|
||||||
console.log(targetKey);
|
|
||||||
throw new Error(`target [${value}] does not exist`);
|
|
||||||
}
|
|
||||||
return await model[accessors.add](target, options);
|
|
||||||
}
|
|
||||||
if (value instanceof SequelizeModel) {
|
|
||||||
if (association instanceof HasMany) {
|
|
||||||
return await model[accessors.add](value.getDataValue(Target.primaryKeyAttribute), options);
|
|
||||||
}
|
|
||||||
return await model[accessors.add](value, options);
|
|
||||||
}
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
targetKey = association.options.targetKey as string;
|
|
||||||
// 如果有主键,直接查询主键
|
|
||||||
if (value[Target.primaryKeyAttribute]) {
|
|
||||||
target = await Target.findOne({
|
|
||||||
where: {
|
|
||||||
[Target.primaryKeyAttribute]: value[Target.primaryKeyAttribute],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 如果主键和关系字段配置的不一样
|
|
||||||
else if (Target.primaryKeyAttribute !== targetKey && value[targetKey]) {
|
|
||||||
target = await Target.findOne({
|
|
||||||
where: {
|
|
||||||
[targetKey]: value[targetKey],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (target) {
|
|
||||||
await model[accessors.add](target, options);
|
|
||||||
if (Object.keys(value).length > 1) {
|
|
||||||
await target.update(value, options);
|
|
||||||
// @ts-ignore
|
|
||||||
await target.updateAssociations(value, options);
|
|
||||||
}
|
|
||||||
if (association instanceof BelongsToMany) {
|
|
||||||
const ThroughModel = association.getThroughModel();
|
|
||||||
const throughName = association.getThroughName();
|
|
||||||
if (typeof value[throughName] === 'object') {
|
|
||||||
const { foreignKey, sourceKey, otherKey, targetKey } = association.options;
|
|
||||||
const through = await ThroughModel.findOne({
|
|
||||||
where: {
|
|
||||||
[foreignKey]: this.get(sourceKey),
|
|
||||||
[otherKey]: target.get(targetKey),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const throughValues = value[throughName];
|
|
||||||
await through.update(throughValues);
|
|
||||||
await through.updateAssociations(throughValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const t = await model[accessors.create](value, options);
|
|
||||||
// console.log(t);
|
|
||||||
await model[accessors.add](t, options);
|
|
||||||
await t.updateAssociations(value, options);
|
|
||||||
if (association instanceof BelongsToMany) {
|
|
||||||
const ThroughModel = association.getThroughModel();
|
|
||||||
const throughName = association.getThroughName();
|
|
||||||
if (typeof value[throughName] === 'object') {
|
|
||||||
const { foreignKey, sourceKey, otherKey, targetKey } = association.options;
|
|
||||||
const through = await ThroughModel.findOne({
|
|
||||||
where: {
|
|
||||||
[foreignKey]: this.get(sourceKey),
|
|
||||||
[otherKey]: t.get(targetKey),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const throughValues = value[throughName];
|
|
||||||
await through.update(throughValues);
|
|
||||||
await through.updateAssociations(throughValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user