Skip to content

Commit

Permalink
refactor: implement joined strategy for find() method
Browse files Browse the repository at this point in the history
Related: #440
  • Loading branch information
B4nan committed Jun 16, 2020
1 parent 56eb023 commit 906c15d
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 10 deletions.
1 change: 0 additions & 1 deletion packages/core/src/entity/EntityRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export class EntityRepository<T> {
getReference<PK extends keyof T>(id: Primary<T>, wrapped: true): IdentifiedReference<T, PK>;
getReference<PK extends keyof T = keyof T>(id: Primary<T>): T;
getReference<PK extends keyof T = keyof T>(id: Primary<T>, wrapped: false): T;
getReference<PK extends keyof T = keyof T>(id: Primary<T>, wrapped: true): Reference<T>;
getReference<PK extends keyof T = keyof T>(id: Primary<T>, wrapped = false): T | Reference<T> {
return this.em.getReference<T>(this.entityName, id, wrapped);
}
Expand Down
24 changes: 18 additions & 6 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
const result = await this.rethrow(qb.execute('all'));

// if (joinedLoads.length > 0) {
// // TODO
// return this.processJoinedLoads(result, joinedLoads) as unknown as T;
// }
if (joinedLoads.length > 0) {
return this.mergeJoinedResult(result, meta, joinedLoads);
}

return result;
}
Expand Down Expand Up @@ -93,7 +92,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
const result = await this.rethrow(qb.execute(method));

if (Array.isArray(result)) {
return this.processJoinedLoads(result, joinedLoads) as unknown as T;
return this.mergeSingleJoinedResult(result, joinedLoads) as unknown as T;
}

return result;
Expand Down Expand Up @@ -291,7 +290,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
.map(({ field }) => field);
}

protected processJoinedLoads<T extends AnyEntity<T>>(rawResults: Dictionary[], joinedLoads: string[]): T | null {
protected mergeSingleJoinedResult<T extends AnyEntity<T>>(rawResults: Dictionary[], joinedLoads: string[]): T | null {
if (rawResults.length === 0) {
return null;
}
Expand All @@ -308,6 +307,19 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
}, {}) as unknown as T;
}

protected mergeJoinedResult<T extends AnyEntity<T>>(rawResults: Dictionary[], meta: EntityMetadata<T>, joinedLoads: string[]): T[] {
// group by the root entity primary key first
const res = rawResults.reduce((result, item) => {
const pk = Utils.getCompositeKeyHash<T>(item as T, meta);
result[pk] = result[pk] || [];
result[pk].push(item);

return result;
}, {}) as Dictionary<any[]>;

return Object.values(res).map((rows: Dictionary[]) => this.mergeSingleJoinedResult(rows, joinedLoads)) as T[];
}

getRefForField(field: string, schema: string, alias: string) {
return this.connection.getKnex().ref(field).withSchema(schema).as(alias);
}
Expand Down
80 changes: 77 additions & 3 deletions tests/joined-loads.postgre.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MikroORM, Logger, LoadStrategy } from '@mikro-orm/core';
import { LoadStrategy, Logger, MikroORM } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { initORMPostgreSql, wipeDatabasePostgreSql } from './bootstrap';
import { Author2, Book2 } from './entities-sql';
Expand All @@ -12,7 +12,7 @@ describe('Joined loading', () => {

afterAll(async () => orm.close(true));

test('populate OneToMany with joined strategy', async () => {
test('populate OneToMany with joined strategy [findOne()]', async () => {
const author2 = new Author2('Albert Camus', 'albert.camus@email.com');
const stranger = new Book2('The Stranger', author2);
const fall = new Book2('The Fall', author2);
Expand All @@ -26,7 +26,30 @@ describe('Joined loading', () => {
expect(a2.books2[1].title).toEqual('The Fall');
});

test('should only fire one query', async () => {
test('populate OneToMany with joined strategy [find()]', async () => {
const a1 = new Author2('Albert Camus 1', 'albert.camus1@email.com');
a1.books2.add(new Book2('The Stranger 1', a1), new Book2('The Fall 1', a1));
const a2 = new Author2('Albert Camus 2', 'albert.camus2@email.com');
a2.books2.add(new Book2('The Stranger 2', a2), new Book2('The Fall 2', a2));
const a3 = new Author2('Albert Camus 3', 'albert.camus3@email.com');
a3.books2.add(new Book2('The Stranger 3', a3), new Book2('The Fall 3', a3));
await orm.em.persistAndFlush([a1, a2, a3]);
orm.em.clear();

const ret = await orm.em.find(Author2, {}, { populate: ['books2', 'following'], orderBy: { email: 'asc' } });
expect(ret).toHaveLength(3);
expect(ret[0].books2).toHaveLength(2);
expect(ret[0].books2[0].title).toEqual('The Stranger 1');
expect(ret[0].books2[1].title).toEqual('The Fall 1');
expect(ret[1].books2).toHaveLength(2);
expect(ret[1].books2[0].title).toEqual('The Stranger 2');
expect(ret[1].books2[1].title).toEqual('The Fall 2');
expect(ret[2].books2).toHaveLength(2);
expect(ret[2].books2[0].title).toEqual('The Stranger 3');
expect(ret[2].books2[1].title).toEqual('The Fall 3');
});

test('should only fire one query [findOne()]', async () => {
const author2 = new Author2('Albert Camus', 'albert.camus@email.com');
const stranger = new Book2('The Stranger', author2);
const fall = new Book2('The Fall', author2);
Expand Down Expand Up @@ -77,6 +100,57 @@ describe('Joined loading', () => {
'where "e0"."id" = $1');
});

test('should only fire one query [find()]', async () => {
const author2 = new Author2('Albert Camus', 'albert.camus@email.com');
const stranger = new Book2('The Stranger', author2);
const fall = new Book2('The Fall', author2);
author2.books2.add(stranger, fall);
await orm.em.persistAndFlush(author2);
orm.em.clear();

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });

await orm.em.find(Author2, { id: author2.id }, { populate: { books2: { perex: true } } });
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."created_at", "e0"."updated_at", "e0"."name", "e0"."email", "e0"."age", "e0"."terms_accepted", "e0"."optional", "e0"."identities", "e0"."born", "e0"."born_time", "e0"."favourite_book_uuid_pk", "e0"."favourite_author_id", ' +
'"b1"."uuid_pk" as "b1_uuid_pk", "b1"."created_at" as "b1_created_at", "b1"."title" as "b1_title", "b1"."perex" as "b1_perex", "b1"."price" as "b1_price", "b1"."double" as "b1_double", "b1"."meta" as "b1_meta", "b1"."author_id" as "b1_author_id", "b1"."publisher_id" as "b1_publisher_id" ' +
'from "author2" as "e0" ' +
'left join "book2" as "b1" on "e0"."id" = "b1"."author_id" ' +
'where "e0"."id" = $1');

orm.em.clear();
mock.mock.calls.length = 0;
await orm.em.find(Author2, { id: author2.id }, { populate: { books2: true } });
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."created_at", "e0"."updated_at", "e0"."name", "e0"."email", "e0"."age", "e0"."terms_accepted", "e0"."optional", "e0"."identities", "e0"."born", "e0"."born_time", "e0"."favourite_book_uuid_pk", "e0"."favourite_author_id", ' +
'"b1"."uuid_pk" as "b1_uuid_pk", "b1"."created_at" as "b1_created_at", "b1"."title" as "b1_title", "b1"."price" as "b1_price", "b1"."double" as "b1_double", "b1"."meta" as "b1_meta", "b1"."author_id" as "b1_author_id", "b1"."publisher_id" as "b1_publisher_id" ' +
'from "author2" as "e0" ' +
'left join "book2" as "b1" on "e0"."id" = "b1"."author_id" ' +
'where "e0"."id" = $1');

orm.em.clear();
mock.mock.calls.length = 0;
await orm.em.find(Author2, { id: author2.id }, { populate: { books: LoadStrategy.JOINED } });
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."created_at", "e0"."updated_at", "e0"."name", "e0"."email", "e0"."age", "e0"."terms_accepted", "e0"."optional", "e0"."identities", "e0"."born", "e0"."born_time", "e0"."favourite_book_uuid_pk", "e0"."favourite_author_id", ' +
'"b1"."uuid_pk" as "b1_uuid_pk", "b1"."created_at" as "b1_created_at", "b1"."title" as "b1_title", "b1"."price" as "b1_price", "b1"."double" as "b1_double", "b1"."meta" as "b1_meta", "b1"."author_id" as "b1_author_id", "b1"."publisher_id" as "b1_publisher_id" ' +
'from "author2" as "e0" ' +
'left join "book2" as "b1" on "e0"."id" = "b1"."author_id" ' +
'where "e0"."id" = $1');

orm.em.clear();
mock.mock.calls.length = 0;
await orm.em.find(Author2, { id: author2.id }, { populate: { books: [LoadStrategy.JOINED, { perex: true }] } });
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0"."id", "e0"."created_at", "e0"."updated_at", "e0"."name", "e0"."email", "e0"."age", "e0"."terms_accepted", "e0"."optional", "e0"."identities", "e0"."born", "e0"."born_time", "e0"."favourite_book_uuid_pk", "e0"."favourite_author_id", ' +
'"b1"."uuid_pk" as "b1_uuid_pk", "b1"."created_at" as "b1_created_at", "b1"."title" as "b1_title", "b1"."perex" as "b1_perex", "b1"."price" as "b1_price", "b1"."double" as "b1_double", "b1"."meta" as "b1_meta", "b1"."author_id" as "b1_author_id", "b1"."publisher_id" as "b1_publisher_id" ' +
'from "author2" as "e0" ' +
'left join "book2" as "b1" on "e0"."id" = "b1"."author_id" ' +
'where "e0"."id" = $1');
});

test('can populate all related entities', async () => {
const author2 = new Author2('Albert Camus', 'albert.camus@email.com');
const stranger = new Book2('The Stranger', author2);
Expand Down

0 comments on commit 906c15d

Please sign in to comment.