feat: support load belongs to association with collection that without primary key (#2529)

* test: find without pk

* feat: load root models in eager loading tree
This commit is contained in:
ChengLei Shao 2023-09-25 15:34:36 +08:00 committed by GitHub
parent 376a91b8ec
commit edbd15ab5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 222 additions and 59 deletions

View File

@ -199,9 +199,10 @@ describe('Eager loading tree', () => {
rootAttributes: findOptions.attributes, rootAttributes: findOptions.attributes,
includeOption: findOptions.include, includeOption: findOptions.include,
db: db, db: db,
rootQueryOptions: findOptions,
}); });
await eagerLoadingTree.load(users.map((item) => item.id)); await eagerLoadingTree.load();
const root = eagerLoadingTree.root; const root = eagerLoadingTree.root;
const u1 = root.instances.find((item) => item.get('name') === 'u1'); const u1 = root.instances.find((item) => item.get('name') === 'u1');
@ -250,9 +251,10 @@ describe('Eager loading tree', () => {
rootAttributes: findOptions.attributes, rootAttributes: findOptions.attributes,
includeOption: findOptions.include, includeOption: findOptions.include,
db: db, db: db,
rootQueryOptions: findOptions,
}); });
await eagerLoadingTree.load([1, 2]); await eagerLoadingTree.load();
const root = eagerLoadingTree.root; const root = eagerLoadingTree.root;
const u1 = root.instances.find((item) => item.get('name') === 'u1'); const u1 = root.instances.find((item) => item.get('name') === 'u1');
@ -301,9 +303,10 @@ describe('Eager loading tree', () => {
rootAttributes: findOptions.attributes, rootAttributes: findOptions.attributes,
includeOption: findOptions.include, includeOption: findOptions.include,
db: db, db: db,
rootQueryOptions: findOptions,
}); });
await eagerLoadingTree.load(users.map((item) => item.id)); await eagerLoadingTree.load();
const root = eagerLoadingTree.root; const root = eagerLoadingTree.root;
const u1 = root.instances.find((item) => item.get('name') === 'u1'); const u1 = root.instances.find((item) => item.get('name') === 'u1');
@ -357,9 +360,10 @@ describe('Eager loading tree', () => {
rootAttributes: findOptions.attributes, rootAttributes: findOptions.attributes,
includeOption: findOptions.include, includeOption: findOptions.include,
db: db, db: db,
rootQueryOptions: findOptions,
}); });
await eagerLoadingTree.load([1, 2]); await eagerLoadingTree.load();
const root = eagerLoadingTree.root; const root = eagerLoadingTree.root;
const p1 = root.instances.find((item) => item.get('title') === 'p1'); const p1 = root.instances.find((item) => item.get('title') === 'p1');
@ -420,8 +424,10 @@ describe('Eager loading tree', () => {
rootAttributes: findOptions.attributes, rootAttributes: findOptions.attributes,
includeOption: findOptions.include, includeOption: findOptions.include,
db: db, db: db,
rootQueryOptions: findOptions,
}); });
await eagerLoadingTree.load([1, 2]);
await eagerLoadingTree.load();
const root = eagerLoadingTree.root; const root = eagerLoadingTree.root;
const p1 = root.instances.find((item) => item.get('title') === 'p1'); const p1 = root.instances.find((item) => item.get('title') === 'p1');
@ -523,6 +529,7 @@ describe('Eager loading tree', () => {
rootAttributes: findOptions.attributes, rootAttributes: findOptions.attributes,
includeOption: findOptions.include, includeOption: findOptions.include,
db: db, db: db,
rootQueryOptions: findOptions,
}); });
expect(eagerLoadingTree.root.children).toHaveLength(1); expect(eagerLoadingTree.root.children).toHaveLength(1);
@ -530,7 +537,7 @@ describe('Eager loading tree', () => {
expect(eagerLoadingTree.root.children[0].children[0].model).toBe(Tag.model); expect(eagerLoadingTree.root.children[0].children[0].model).toBe(Tag.model);
expect(eagerLoadingTree.root.children[0].children[0].children[0].model).toBe(TagCategory.model); expect(eagerLoadingTree.root.children[0].children[0].children[0].model).toBe(TagCategory.model);
await eagerLoadingTree.load((await User.model.findAll()).map((item) => item[User.model.primaryKeyAttribute])); await eagerLoadingTree.load();
expect(eagerLoadingTree.root.instances).toHaveLength(2); expect(eagerLoadingTree.root.instances).toHaveLength(2);
const u1 = eagerLoadingTree.root.instances.find((item) => item.get('name') === 'u1'); const u1 = eagerLoadingTree.root.instances.find((item) => item.get('name') === 'u1');

View File

@ -286,15 +286,13 @@ describe('has many repository', () => {
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id); const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
await PostTagRepository.set([t1.id, t2.id, t3.id]); await PostTagRepository.set([t1.id, t2.id, t3.id]);
const posts = await UserPostRepository.find({ const post = await UserPostRepository.findOne({
filter: { filter: {
'tags.name': 't1', 'tags.name': 't1',
}, },
appends: ['tags'], appends: ['tags'],
}); });
const post = posts[0];
expect(post.tags.length).toEqual(3); expect(post.tags.length).toEqual(3);
const findAndCount = await UserPostRepository.findAndCount({ const findAndCount = await UserPostRepository.findAndCount({

View File

@ -0,0 +1,71 @@
import Database from '../../database';
import { mockDatabase } from '../index';
describe('find collection that without primary key', () => {
let db: Database;
beforeAll(async () => {
db = mockDatabase({
tablePrefix: '',
});
await db.clean({ drop: true });
});
afterEach(async () => {
await db.close();
});
it('should find collection with belongsTo', async () => {
const B = db.collection({
name: 'b',
fields: [
{
type: 'string',
name: 'name',
},
],
});
const A = db.collection({
name: 'a',
autoGenId: false,
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'belongsTo',
name: 'b',
target: 'b',
foreignKey: 'b_id',
},
],
});
await db.sync();
const b1 = await B.repository.create({
values: {
name: 'b1',
},
});
await A.repository.create({
values: {
name: 'a1',
b_id: b1.get('id'),
},
});
const aWithB = await A.repository.find({
appends: ['b'],
filter: {
'b.name': 'b1',
},
});
expect(aWithB[0].get('b').get('name')).toBe('b1');
});
});

View File

@ -14,6 +14,65 @@ describe('find with associations', () => {
await db.close(); await db.close();
}); });
it('should filter has many with limit', async () => {
const User = db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{
type: 'hasMany',
name: 'posts',
},
],
});
const Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'belongsTo', name: 'user' },
],
});
await db.sync();
await User.repository.create({
values: [
{
name: 'u1',
posts: [
{
title: 'u1p1',
},
{
title: 'u1p2',
},
],
},
{
name: 'u2',
posts: [
{
title: 'u2p1',
},
{
title: 'u2p2',
},
],
},
],
});
const users = await User.repository.find({
filter: {
'posts.title.$includes': 'p',
},
limit: 2,
});
expect(users.length).toEqual(2);
});
it('should filter by association array field', async () => { it('should filter by association array field', async () => {
const User = db.collection({ const User = db.collection({
name: 'users', name: 'users',

View File

@ -47,6 +47,7 @@ const EagerLoadingNodeProto = {
export class EagerLoadingTree { export class EagerLoadingTree {
public root: EagerLoadingNode; public root: EagerLoadingNode;
db: Database; db: Database;
private rootQueryOptions: any = {};
constructor(root: EagerLoadingNode) { constructor(root: EagerLoadingNode) {
this.root = root; this.root = root;
@ -56,10 +57,11 @@ export class EagerLoadingTree {
model: ModelStatic<any>; model: ModelStatic<any>;
rootAttributes: Array<string>; rootAttributes: Array<string>;
rootOrder?: any; rootOrder?: any;
rootQueryOptions?: any;
includeOption: Includeable | Includeable[]; includeOption: Includeable | Includeable[];
db: Database; db: Database;
}): EagerLoadingTree { }): EagerLoadingTree {
const { model, rootAttributes, includeOption, db } = options; const { model, rootAttributes, includeOption, db, rootQueryOptions } = options;
const buildNode = (node) => { const buildNode = (node) => {
Object.setPrototypeOf(node, EagerLoadingNodeProto); Object.setPrototypeOf(node, EagerLoadingNodeProto);
@ -137,10 +139,11 @@ export class EagerLoadingTree {
const tree = new EagerLoadingTree(root); const tree = new EagerLoadingTree(root);
tree.db = db; tree.db = db;
tree.rootQueryOptions = rootQueryOptions;
return tree; return tree;
} }
async load(pks: Array<string | number>, transaction?: Transaction) { async load(transaction?: Transaction) {
const result = {}; const result = {};
const orderOption = (association) => { const orderOption = (association) => {
@ -154,26 +157,80 @@ export class EagerLoadingTree {
return order; return order;
}; };
const loadRecursive = async (node, ids) => { const loadRecursive = async (node, ids = []) => {
const modelPrimaryKey = node.model.primaryKeyField || node.model.primaryKeyAttribute;
let instances = []; let instances = [];
// load instances from database
if (!node.parent) { if (!node.parent) {
const findOptions = { // load root instances
where: { [modelPrimaryKey]: ids }, const rootInclude = this.rootQueryOptions?.include || node.includeOption;
attributes: node.attributes,
};
if (node.order) { const includeForFilter = rootInclude.filter((include) => {
findOptions['order'] = node.order; return (
Object.keys(include.where || {}).length > 0 ||
JSON.stringify(this.rootQueryOptions?.filter)?.includes(include.association)
);
});
const belongsToAssociationsOnly = includeForFilter.every((include) => {
const association = node.model.associations[include.association];
if (!association) {
return false;
}
return association.associationType == 'BelongsTo';
});
if (belongsToAssociationsOnly) {
instances = await node.model.findAll({
...this.rootQueryOptions,
attributes: node.attributes,
distinct: true,
include: includeForFilter,
transaction,
});
} else {
const primaryKeyField = node.model.primaryKeyField || node.model.primaryKeyAttribute;
if (!primaryKeyField) {
throw new Error(`Model ${node.model.name} does not have primary key`);
}
// find all ids
const ids = (
await node.model.findAll({
...this.rootQueryOptions,
includeIgnoreAttributes: false,
attributes: [primaryKeyField],
group: `${node.model.name}.${primaryKeyField}`,
transaction,
include: includeForFilter,
} as any)
).map((row) => {
return { row, pk: row.get(primaryKeyField) };
});
const findOptions = {
where: { [primaryKeyField]: ids.map((i) => i.pk) },
attributes: node.attributes,
};
if (node.order) {
findOptions['order'] = node.order;
}
instances = await node.model.findAll({
...findOptions,
transaction,
});
} }
instances = await node.model.findAll({ // clear filter association value
...findOptions, const associations = node.model.associations;
transaction, for (const association of Object.keys(associations)) {
}); for (const instance of instances) {
delete instance[association];
delete instance.dataValues[association];
}
}
} else if (ids.length > 0) { } else if (ids.length > 0) {
const association = node.association; const association = node.association;
const associationType = association.associationType; const associationType = association.associationType;
@ -241,8 +298,8 @@ export class EagerLoadingTree {
node.instances = instances; node.instances = instances;
for (const child of node.children) { for (const child of node.children) {
const modelPrimaryKey = node.model.primaryKeyField || node.model.primaryKeyAttribute;
const nodeIds = instances.map((instance) => instance.get(modelPrimaryKey)); const nodeIds = instances.map((instance) => instance.get(modelPrimaryKey));
await loadRecursive(child, nodeIds); await loadRecursive(child, nodeIds);
} }
@ -343,7 +400,7 @@ export class EagerLoadingTree {
} }
}; };
await loadRecursive(this.root, pks); await loadRecursive(this.root);
const appendChildCollectionName = appendChildCollectionNameAfterRepositoryFind(this.db); const appendChildCollectionName = appendChildCollectionNameAfterRepositoryFind(this.db);
@ -377,8 +434,7 @@ export class EagerLoadingTree {
} }
for (const instance of node.instances) { for (const instance of node.instances) {
const attributes = lodash.pick(instance.dataValues, includeAttributes); instance.dataValues = lodash.pick(instance.dataValues, includeAttributes);
instance.dataValues = attributes;
} }
}; };

View File

@ -391,44 +391,16 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
let rows; let rows;
if (opts.include && opts.include.length > 0) { if (opts.include && opts.include.length > 0) {
// @ts-ignore
const primaryKeyField = model.primaryKeyField || model.primaryKeyAttribute;
// find all ids
const ids = (
await model.findAll({
...opts,
includeIgnoreAttributes: false,
attributes: [primaryKeyField],
group: `${model.name}.${primaryKeyField}`,
transaction,
include: opts.include.filter((include) => {
return (
Object.keys(include.where || {}).length > 0 || JSON.stringify(opts?.filter)?.includes(include.association)
);
}),
} as any)
).map((row) => {
return { row, pk: row.get(primaryKeyField) };
});
if (ids.length == 0) {
return [];
}
// find all rows
const eagerLoadingTree = EagerLoadingTree.buildFromSequelizeOptions({ const eagerLoadingTree = EagerLoadingTree.buildFromSequelizeOptions({
model, model,
rootAttributes: opts.attributes, rootAttributes: opts.attributes,
includeOption: opts.include, includeOption: opts.include,
rootOrder: opts.order, rootOrder: opts.order,
rootQueryOptions: opts,
db: this.database, db: this.database,
}); });
await eagerLoadingTree.load( await eagerLoadingTree.load(transaction);
ids.map((i) => i.pk),
transaction,
);
rows = eagerLoadingTree.root.instances; rows = eagerLoadingTree.root.instances;
} else { } else {