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

fix(sql): sort fetch-joined properties on their orderBy #1336

Merged
merged 2 commits into from
Jan 23, 2021

Conversation

TiesB
Copy link
Contributor

@TiesB TiesB commented Jan 23, 2021

Merges a joined relation's orderBy with the parent's, keeping tableAliases in mind. Fixes #1331

Copy link
Member

@B4nan B4nan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! the fix itself looks fine, but i don't really like the way it is coded inside existing methods with different purposes. should be refactored based on my comments, but if you would struggle with that, I will be happy to merge as is and refactor it myself

@@ -39,7 +39,8 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
const populate = this.autoJoinOneToOneOwner(meta, options.populate as PopulateOptions<T>[], options.fields);
const joinedProps = this.joinedProps(meta, populate);
const qb = this.createQueryBuilder<T>(entityName, ctx, !!ctx, false);
const fields = this.buildFields(meta, populate, joinedProps, qb, options.fields as Field<T>[]);
const fieldsAndJoinedPropsOrderBy = this.buildFields(meta, populate, joinedProps, qb, options.fields as Field<T>[]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like huge property names, especially if they contain conjunctions. Why dont we destruct instead?

Suggested change
const fieldsAndJoinedPropsOrderBy = this.buildFields(meta, populate, joinedProps, qb, options.fields as Field<T>[]);
const { fields, orderBy } = this.buildFields(meta, populate, joinedProps, qb, options.fields as Field<T>[]);

But I would much rather see a dedicated method - this one is called buildFields, it should not have anything to do with ordering.

@@ -393,8 +394,8 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
orderBy = this.getPivotOrderBy(prop, orderBy);
const qb = this.createQueryBuilder<T>(prop.type, ctx, !!ctx).unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
const populate = this.autoJoinOneToOneOwner(targetMeta, [{ field: prop.pivotTable }]);
const fields = this.buildFields(targetMeta, (options?.populate ?? []) as PopulateOptions<T>[], [], qb, options?.fields as Field<T>[]);
qb.select(fields).populate(populate).where(where).orderBy(orderBy!);
const fieldsAndJoinedPropsOrderBy = this.buildFields(targetMeta, (options?.populate ?? []) as PopulateOptions<T>[], [], qb, options?.fields as Field<T>[]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

@@ -457,9 +458,10 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
return res;
}

protected getFieldsForJoinedLoad<T extends AnyEntity<T>>(qb: QueryBuilder<T>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[] = [], parentTableAlias?: string, parentJoinPath?: string): Field<T>[] {
protected getFieldsAndOrderByForJoinedLoad<T extends AnyEntity<T>>(qb: QueryBuilder<T>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[] = [], parentTableAlias?: string, parentJoinPath?: string): { fields: Field<T>[]; orderBy: QueryOrderMap } {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, having conjunctions inside method name feels even worse that in a property name...

}
}
fields.push(...propFieldsAndOrderBy.fields);
orderBy = { ...orderBy, ...propOrderBy, ...propFieldsAndOrderBy.orderBy };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should not need to reassign the variable here. this is equal to the following, right?

Suggested change
orderBy = { ...orderBy, ...propOrderBy, ...propFieldsAndOrderBy.orderBy };
Object.assign(orderBy, { ...propOrderBy, ...propFieldsAndOrderBy.orderBy };

tests/issues/GH1331.test.ts Outdated Show resolved Hide resolved
await orm.close(true);
});

test(`GH issue 1278`, async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong issue number

tests/issues/GH1331.test.ts Outdated Show resolved Hide resolved
Comment on lines 126 to 128
const mock = jest.fn();
const logger = new Logger(mock, ['query']);
Object.assign(orm.config, { logger });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is not used anyhow. i am curious, as large portion of repros contain it - where/why did you copy it from?

Suggested change
const mock = jest.fn();
const logger = new Logger(mock, ['query']);
Object.assign(orm.config, { logger });

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah I copied GH1278.test.ts and the code was in there. I have no idea who originally wrote it.

I probably forgot to remove it myself. Thanks @TiesB for fixing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am quite sure it was me :] I am using this exact snippet on many places in tests, not just in repros (oh yeah, I should have a helper method long time ago, lol). But as long as there are no asserts on the mocked logger, there is no need for mocking it.

await orm.em.persistAndFlush(project);
orm.em.clear();

const loadedProject = await orm.em.findOne(Project, { id: 1 });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use findOneOrFail so the ! is not needed

Suggested change
const loadedProject = await orm.em.findOne(Project, { id: 1 });
const loadedProject = await orm.em.findOneOrFail(Project, project.id);


const loadedProject = await orm.em.findOne(Project, { id: 1 });
expect(loadedProject?.radios.getItems().map(r => r.order)).toStrictEqual([0, 1, 2]);
expect(loadedProject?.radios.getItems()[0].options.getItems().map(r => r.order).toString()).toBe([1, 2, 3, 4].toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks also very hard to read...

Suggested change
expect(loadedProject?.radios.getItems()[0].options.getItems().map(r => r.order).toString()).toBe([1, 2, 3, 4].toString());
expect(loadedProject.radios[0].options.getIdentifiers('order').toEqual([1, 2, 3, 4]);

@TiesB
Copy link
Contributor Author

TiesB commented Jan 23, 2021

Thanks a lot for the feedback! I refactored it to one single function and applied your other suggestions (I hope correctly ;-) ). I was not sure about how to deal with the orderBy in the loadFromPivotTable method; it would be great if you could look at that.

@TiesB TiesB requested a review from B4nan January 23, 2021 14:40
Copy link
Member

@B4nan B4nan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking much cleaner now!

@@ -40,6 +40,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
const joinedProps = this.joinedProps(meta, populate);
const qb = this.createQueryBuilder<T>(entityName, ctx, !!ctx, false);
const fields = this.buildFields(meta, populate, joinedProps, qb, options.fields as Field<T>[]);
const orderBy = this.fillInOrderBy(entityName, qb, meta, options.orderBy!, joinedProps);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was expecting something like buildOrderBy, and I'd rather not mutate the exiting options.orderBy there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to buildJoinedPropsOrderBy now, since I feel buildOrderBy is not right because it doesn't touch the options.orderBy anymore.

@@ -583,6 +584,19 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
await this.rethrow(qb.execute());
}

protected fillInOrderBy<T extends AnyEntity<T>>(entityName: string, qb: QueryBuilder<T>, meta: EntityMetadata<T>, orderBy: QueryOrderMap, joinedProps: PopulateOptions<T>[]): QueryOrderMap {
joinedProps.forEach(prop => {
const propAlias = qb.getAliasForJoinPath(`${entityName}.${prop.field}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this probably won't work with more than one level. the join path always starts with root entity name, but for if you would load author, its books and their tags, it would be something like Author.books.tags

would be good to add some tests for such use case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good point! I changed it such that it can handle nested relations (I looked at getFieldsForJoinedLoad() for reference) and extended the test with a level deeper relation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is intriguing me is the amount of queries executed; a query is added for retrieving the 'ds', even though they are also retrieved with the 2nd massive query. Do you know why this is and whether it's needed?

select `e0`.* from `a` as `e0` where `e0`.`id` = ? limit ?

select `e0`.`id`, `e0`.`order`, `e0`.`a_id`, `c1`.`id` as `c1__id`, `c1`.`order` as `c1__order`, `c1`.`b_id` as `c1__b_id`, `d2`.`id` as `d2__id`, `d2`.`order` as `d2__order`, `d2`.`c_id` as `d2__c_id` from `b` as `e0` left join `c` as `c1` on `e0`.`id` = `c1`.`b_id` left join `d` as `d2` on `c1`.`id` = `d2`.`c_id` where `e0`.`a_id` in (?) order by `e0`.`order` asc, `e0`.`id` asc, `c1`.`order` asc, `c1`.`id` asc, `d2`.`order` asc, `d2`.`id` asc

select `e0`.* from `d` as `e0` where `e0`.`c_id` in (?, ?, ?, ?, ?, ?, ?, ?) order by `e0`.`order` asc, `e0`.`id` asc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard to say, might be some other issue, either with reference wrapper, or eager: true combined with joined strategy

@B4nan
Copy link
Member

B4nan commented Jan 23, 2021

Regarding the loadFromPivotTable() method, it should be used only for select-in strategy (or when lazy loading m:n collections via init() method).

@TiesB
Copy link
Contributor Author

TiesB commented Jan 23, 2021

I am running into the 500loc limit in codeclimate. I'd like to leave the refactoring to get the loc within limits to you, since you know the code waaayy better than I do :-)

Regarding the loadFromPivotTable() method, it should be used only for select-in strategy (or when lazy loading m:n collections via init() method).

Cool. Then I'll leave that untouched.

@TiesB TiesB requested a review from B4nan January 23, 2021 16:38
@B4nan
Copy link
Member

B4nan commented Jan 23, 2021

Sure, don't worry too much about codeclimate.

@B4nan B4nan merged commit f18cd88 into mikro-orm:master Jan 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

OrderBy does not work for nested collections with LoadStrategy.JOINED
3 participants