Skip to content

Commit

Permalink
feat(sql): add autoJoinOneToOneOwner option
Browse files Browse the repository at this point in the history
By default, owning side of 1:1 relation will be auto-joined when you select the inverse side so we can have the reference to it.
Now you can disable this behaviour via `autoJoinOneToOneOwner` configuration toggle.

Closes #248
  • Loading branch information
B4nan committed Dec 1, 2019
1 parent b9c00bc commit f2db3e0
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 4 deletions.
12 changes: 8 additions & 4 deletions lib/drivers/AbstractSqlDriver.ts
Expand Up @@ -24,7 +24,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra

async find<T extends AnyEntity<T>>(entityName: string, where: FilterQuery<T>, populate: string[] = [], orderBy: QueryOrderMap = {}, fields?: string[], limit?: number, offset?: number, ctx?: Transaction): Promise<T[]> {
const meta = this.metadata.get(entityName);
populate = this.populateMissingReferences(meta, populate);
populate = this.autoJoinOneToOneOwner(meta, populate);

if (fields && !fields.includes(meta.primaryKey)) {
fields.unshift(meta.primaryKey);
Expand All @@ -42,7 +42,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra

async findOne<T extends AnyEntity<T>>(entityName: string, where: FilterQuery<T>, populate: string[] = [], orderBy: QueryOrderMap = {}, fields?: string[], lockMode?: LockMode, ctx?: Transaction): Promise<T | null> {
const meta = this.metadata.get(entityName);
populate = this.populateMissingReferences(meta, populate);
populate = this.autoJoinOneToOneOwner(meta, populate);
const pk = meta.primaryKey;

if (Utils.isPrimaryKey(where)) {
Expand Down Expand Up @@ -124,7 +124,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra

orderBy = this.getPivotOrderBy(prop, orderBy);
const qb = this.createQueryBuilder(prop.type, ctx, !!ctx);
const populate = this.populateMissingReferences(meta, [prop.pivotTable]);
const populate = this.autoJoinOneToOneOwner(meta, [prop.pivotTable]);
qb.select('*').populate(populate).where(where as Dictionary).orderBy(orderBy);
const items = owners.length ? await qb.execute('all') : [];
const fk1 = prop.joinColumn;
Expand All @@ -144,7 +144,11 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
/**
* 1:1 owner side needs to be marked for population so QB auto-joins the owner id
*/
protected populateMissingReferences(meta: EntityMetadata, populate: string[]): string[] {
protected autoJoinOneToOneOwner(meta: EntityMetadata, populate: string[]): string[] {
if (!this.config.get('autoJoinOneToOneOwner')) {
return populate;
}

const toPopulate = Object.values(meta.properties)
.filter(prop => prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner && !populate.includes(prop.name))
.map(prop => prop.name);
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/Configuration.ts
Expand Up @@ -33,6 +33,7 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
baseDir: process.cwd(),
entityRepository: EntityRepository,
hydrator: ObjectHydrator,
autoJoinOneToOneOwner: true,
tsNode: false,
debug: false,
verbose: false,
Expand Down Expand Up @@ -230,6 +231,7 @@ export interface MikroORMOptions<D extends IDatabaseDriver = IDatabaseDriver> ex
driver?: { new (config: Configuration): D };
driverOptions: Dictionary;
namingStrategy?: { new (): NamingStrategy };
autoJoinOneToOneOwner: boolean;
hydrator: { new (factory: EntityFactory, driver: IDatabaseDriver): Hydrator };
entityRepository: { new (em: EntityManager, entityName: EntityName<AnyEntity>): EntityRepository<AnyEntity> };
replicas?: Partial<ConnectionOptions>[];
Expand Down
64 changes: 64 additions & 0 deletions tests/EntityManager.postgre.test.ts
Expand Up @@ -1011,6 +1011,70 @@ describe('EntityManagerPostgre', () => {
expect(test.id).toBeDefined();
});

test('find by joined property', async () => {
const author = new Author2('Jon Snow', 'snow@wall.st');
const book1 = new Book2('My Life on The Wall, part 1', author);
const book2 = new Book2('My Life on The Wall, part 2', author);
const book3 = new Book2('My Life on The Wall, part 3', author);
const t1 = Test2.create('t1');
t1.book = book1;
const t2 = Test2.create('t2');
t2.book = book2;
const t3 = Test2.create('t3');
t3.book = book3;
author.books.add(book1, book2, book3);
await orm.em.persistAndFlush([author, t1, t2, t3]);
author.favouriteBook = book3;
await orm.em.flush();
orm.em.clear();

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } });
expect(res1).toHaveLength(3);
expect(res1[0].test).toBeUndefined();
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0".* ' +
'from "book2" as "e0" ' +
'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' +
'where "e1"."name" = $1');

orm.em.clear();
mock.mock.calls.length = 0;
const res2 = await orm.em.find(Book2, { author: { favouriteBook: { author: { name: 'Jon Snow' } } } });
expect(res2).toHaveLength(3);
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0".* ' +
'from "book2" as "e0" ' +
'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' +
'left join "book2" as "e2" on "e1"."favourite_book_uuid_pk" = "e2"."uuid_pk" ' +
'left join "author2" as "e3" on "e2"."author_id" = "e3"."id" ' +
'where "e3"."name" = $1');

orm.em.clear();
mock.mock.calls.length = 0;
const res3 = await orm.em.find(Book2, { author: { favouriteBook: book3 } });
expect(res3).toHaveLength(3);
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0".* ' +
'from "book2" as "e0" ' +
'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' +
'where "e1"."favourite_book_uuid_pk" = $1');

orm.em.clear();
mock.mock.calls.length = 0;
const res4 = await orm.em.find(Book2, { author: { favouriteBook: { $or: [{ author: { name: 'Jon Snow' } }] } } });
expect(res4).toHaveLength(3);
expect(mock.mock.calls.length).toBe(1);
expect(mock.mock.calls[0][0]).toMatch('select "e0".* ' +
'from "book2" as "e0" ' +
'left join "author2" as "e1" on "e0"."author_id" = "e1"."id" ' +
'left join "book2" as "e2" on "e1"."favourite_book_uuid_pk" = "e2"."uuid_pk" ' +
'left join "author2" as "e3" on "e2"."author_id" = "e3"."id" ' +
'where "e3"."name" = $1');
});

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

});
1 change: 1 addition & 0 deletions tests/bootstrap.ts
Expand Up @@ -74,6 +74,7 @@ export async function initORMPostgreSql() {
type: 'postgresql',
debug: ['query'],
highlight: false,
autoJoinOneToOneOwner: false,
logger: i => i,
metadataProvider: ReflectMetadataProvider,
cache: { enabled: false },
Expand Down

0 comments on commit f2db3e0

Please sign in to comment.