-
-
Notifications
You must be signed in to change notification settings - Fork 502
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
Conversation
There was a problem hiding this 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>[]); |
There was a problem hiding this comment.
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?
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>[]); |
There was a problem hiding this comment.
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 } { |
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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?
orderBy = { ...orderBy, ...propOrderBy, ...propFieldsAndOrderBy.orderBy }; | |
Object.assign(orderBy, { ...propOrderBy, ...propFieldsAndOrderBy.orderBy }; |
tests/issues/GH1331.test.ts
Outdated
await orm.close(true); | ||
}); | ||
|
||
test(`GH issue 1278`, async () => { |
There was a problem hiding this comment.
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
const mock = jest.fn(); | ||
const logger = new Logger(mock, ['query']); | ||
Object.assign(orm.config, { logger }); |
There was a problem hiding this comment.
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?
const mock = jest.fn(); | |
const logger = new Logger(mock, ['query']); | |
Object.assign(orm.config, { logger }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
tests/issues/GH1331.test.ts
Outdated
await orm.em.persistAndFlush(project); | ||
orm.em.clear(); | ||
|
||
const loadedProject = await orm.em.findOne(Project, { id: 1 }); |
There was a problem hiding this comment.
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
const loadedProject = await orm.em.findOne(Project, { id: 1 }); | |
const loadedProject = await orm.em.findOneOrFail(Project, project.id); |
tests/issues/GH1331.test.ts
Outdated
|
||
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()); |
There was a problem hiding this comment.
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...
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]); |
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. |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}`); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
Regarding the |
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 :-)
Cool. Then I'll leave that untouched. |
Sure, don't worry too much about codeclimate. |
Merges a joined relation's orderBy with the parent's, keeping tableAliases in mind. Fixes #1331