Skip to content

Commit

Permalink
refactor: support formulas with joined strategy
Browse files Browse the repository at this point in the history
Related: #440
  • Loading branch information
B4nan committed Jun 16, 2020
1 parent e543be2 commit d6c2736
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 69 deletions.
5 changes: 2 additions & 3 deletions packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,10 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

entity = Utils.isEntity<T>(data) ? data : this.getEntityFactory().create<T>(entityName, data as EntityData<T>);

// add to IM immediately - needed for self-references that can be part of `data` (and do not trigger cascade merge)
// add to IM immediately - needed for self-references that can be part of `data` (do not trigger cascade merge)
this.getUnitOfWork().merge(entity, [entity]);
EntityAssigner.assign(entity, data as EntityData<T>, { onlyProperties: true, merge: true });
this.getUnitOfWork().merge(entity); // add to IM again so we have correct payload saved to change set computation
this.getUnitOfWork().merge(entity); // add to IM again so we have correct payload saved for change set computation

return entity;
}
Expand Down Expand Up @@ -590,7 +590,6 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
}

const preparedPopulate = this.preparePopulate(entityName, options.populate);

await this.entityLoader.populate(entityName, [entity], preparedPopulate, where, options.orderBy || {}, options.refresh);

return entity;
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,11 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
throw new Error(`Pessimistic locks are not supported by ${this.constructor.name} driver`);
}

protected shouldHaveColumn<T>(prop: EntityProperty<T>, populate: PopulateOptions<T>[]): boolean {
protected shouldHaveColumn<T>(prop: EntityProperty<T>, populate: PopulateOptions<T>[], includeFormulas = true): boolean {
if (prop.formula) {
return includeFormulas;
}

if (prop.persist === false) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/entity/EntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class EntityLoader {
fk = meta.properties[prop.mappedBy].name;
}

if (prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner && !this.em.config.get('autoJoinOneToOneOwner')) {
if (prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner && populate.strategy !== LoadStrategy.JOINED && !this.em.config.get('autoJoinOneToOneOwner')) {
children.length = 0;
children.push(...entities);
fk = meta.properties[prop.mappedBy].name;
Expand Down
25 changes: 18 additions & 7 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,17 +329,13 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
return Object.values(res).map((rows: Dictionary[]) => this.mergeSingleJoinedResult(rows, joinedProps)) as T[];
}

getRefForField(field: string, schema: string, alias: string) {
return this.connection.getKnex().ref(field).withSchema(schema).as(alias);
}

protected getSelectForJoinedLoad<T>(qb: QueryBuilder, meta: EntityMetadata, joinedProps: EntityProperty<T>[], populate: PopulateOptions<T>[]): Field[] {
const selects: Field[] = [];

// alias all fields in the primary table
Object.values(meta.properties)
.filter(prop => this.shouldHaveColumn(prop, populate))
.forEach(prop => selects.push(...prop.fieldNames));
.forEach(prop => selects.push(...this.mapPropToFieldNames(qb, prop)));

joinedProps.forEach(relation => {
const meta2 = this.metadata.get(relation.type);
Expand All @@ -352,13 +348,28 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
});

for (const prop2 of properties) {
selects.push(...prop2.fieldNames.map(fieldName => this.getRefForField(fieldName, tableAlias, `${tableAlias}_${fieldName}`)));
selects.push(...this.mapPropToFieldNames(qb, prop2, tableAlias));
}
});

return selects;
}

protected mapPropToFieldNames<T>(qb: QueryBuilder<T>, prop: EntityProperty<T>, tableAlias?: string): Field[] {
if (prop.formula) {
const alias = qb.ref(tableAlias ?? qb.alias).toString();
const aliased = qb.ref(tableAlias ? `${tableAlias}_${prop.fieldNames[0]}` : prop.fieldNames[0]).toString();

return [`${prop.formula!(alias)} as ${aliased}`];
}

if (tableAlias) {
return prop.fieldNames.map(fieldName => qb.ref(fieldName).withSchema(tableAlias).as(`${tableAlias}_${fieldName}`));
}

return prop.fieldNames;
}

protected createQueryBuilder<T extends AnyEntity<T>>(entityName: string, ctx?: Transaction<KnexTransaction>, write?: boolean): QueryBuilder<T> {
return new QueryBuilder(entityName, this.metadata, this, ctx, undefined, write ? 'write' : 'read');
}
Expand Down Expand Up @@ -439,7 +450,6 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
}

protected buildFields<T>(meta: EntityMetadata<T>, populate: PopulateOptions<T>[], joinedProps: EntityProperty<T>[], qb: QueryBuilder, fields?: Field[]): Field[] {
const props = Object.values<EntityProperty<T>>(meta.properties).filter(prop => this.shouldHaveColumn(prop, populate));
const lazyProps = Object.values<EntityProperty<T>>(meta.properties).filter(prop => prop.lazy && !populate.some(p => p.field === prop.name || p.all));
const hasExplicitFields = !!fields;

Expand All @@ -448,6 +458,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
} else if (joinedProps.length > 0) {
fields = this.getSelectForJoinedLoad(qb, meta, joinedProps, populate);
} else if (lazyProps.length > 0) {
const props = Object.values<EntityProperty<T>>(meta.properties).filter(prop => this.shouldHaveColumn(prop, populate, false));
fields = Utils.flatten(props.filter(p => !lazyProps.includes(p)).map(p => p.fieldNames));
}

Expand Down
32 changes: 16 additions & 16 deletions tests/EntityManager.mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ describe('EntityManagerMySql', () => {
test('nested transaction rollback with save-points will commit the outer one', async () => {
const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });

// start outer transaction
const transaction = orm.em.transactional(async em => {
Expand Down Expand Up @@ -624,7 +624,7 @@ describe('EntityManagerMySql', () => {

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

await orm.em.transactional(async em => {
await em.lock(author, LockMode.PESSIMISTIC_WRITE);
Expand All @@ -642,7 +642,7 @@ describe('EntityManagerMySql', () => {

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

await orm.em.transactional(async em => {
await em.lock(author, LockMode.PESSIMISTIC_READ);
Expand Down Expand Up @@ -805,7 +805,7 @@ describe('EntityManagerMySql', () => {

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

const b0 = (await orm.em.findOne(FooBaz2, { id: baz.id }))!;
expect(mock.mock.calls[0][0]).toMatch('select `e0`.*, `e1`.`id` as `bar_id` from `foo_baz2` as `e0` left join `foo_bar2` as `e1` on `e0`.`id` = `e1`.`baz_id` where `e0`.`id` = ? limit ?');
Expand Down Expand Up @@ -1289,7 +1289,7 @@ describe('EntityManagerMySql', () => {

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

orm.em.clear();
const books = await orm.em.find(Book2, { tagsUnordered: { name: { $ne: 'funny' } } }, ['tagsUnordered', 'perex'], { title: QueryOrder.DESC });
Expand Down Expand Up @@ -1467,7 +1467,7 @@ describe('EntityManagerMySql', () => {
test('self referencing (1 step)', async () => {
const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });

const author = new Author2('name', 'email');
author.favouriteAuthor = author;
Expand Down Expand Up @@ -1584,7 +1584,7 @@ describe('EntityManagerMySql', () => {

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });
const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, ['perex']);
expect(res1).toHaveLength(3);
expect(res1[0].test).toBeInstanceOf(Test2);
Expand Down Expand Up @@ -1696,8 +1696,8 @@ describe('EntityManagerMySql', () => {
test('query highlighting', async () => {
const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
orm.em.config.set('highlight', true);
Object.assign(orm.config, { logger });
orm.config.set('highlight', true);

const author = new Author2('Jon Snow', 'snow@wall.st');
await orm.em.persistAndFlush(author);
Expand All @@ -1710,13 +1710,13 @@ describe('EntityManagerMySql', () => {
}

expect(mock.mock.calls[2][0]).toMatch('commit');
orm.em.config.set('highlight', false);
orm.config.set('highlight', false);
});

test('read replicas', async () => {
const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });

let author = new Author2('Jon Snow', 'snow@wall.st');
author.born = new Date('1990-03-23');
Expand Down Expand Up @@ -1811,7 +1811,7 @@ describe('EntityManagerMySql', () => {

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

const res1 = await orm.em.find(Book2, { publisher: { $ne: null } }, { schema: 'mikro_orm_test_schema_2', populate: ['perex'] });
const res2 = await orm.em.find(Book2, { publisher: { $ne: null } }, ['perex']);
Expand Down Expand Up @@ -1847,7 +1847,7 @@ describe('EntityManagerMySql', () => {

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });
await orm.em.flush();
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('delete from `foo_baz2` where `id` = ?');
Expand Down Expand Up @@ -1979,7 +1979,7 @@ describe('EntityManagerMySql', () => {

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

// without paginate flag it fails to get 5 records
const res1 = await orm.em.find(Author2, { books: { title: /^Bible/ } }, {
Expand Down Expand Up @@ -2044,7 +2044,7 @@ describe('EntityManagerMySql', () => {

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

const b = await orm.em.findOneOrFail(Book2, { author: { name: 'God' } });
expect(b.price).toBe(1000);
Expand Down Expand Up @@ -2124,7 +2124,7 @@ describe('EntityManagerMySql', () => {

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

const r1 = await orm.em.find(Author2, {}, { populate: { books: true } });
expect(r1[0].books[0].perex).not.toBe('123');
Expand Down
22 changes: 11 additions & 11 deletions tests/EntityManager.postgre.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ describe('EntityManagerPostgre', () => {
test('nested transaction rollback with save-points will commit the outer one', async () => {
const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });

// start outer transaction
const transaction = orm.em.transactional(async em => {
Expand Down Expand Up @@ -485,7 +485,7 @@ describe('EntityManagerPostgre', () => {

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

await orm.em.transactional(async em => {
await em.lock(author, LockMode.PESSIMISTIC_WRITE);
Expand All @@ -503,7 +503,7 @@ describe('EntityManagerPostgre', () => {

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

await orm.em.transactional(async em => {
await em.lock(author, LockMode.PESSIMISTIC_READ);
Expand Down Expand Up @@ -632,7 +632,7 @@ describe('EntityManagerPostgre', () => {

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

// autoJoinOneToOneOwner: false
const b0 = await orm.em.findOneOrFail(FooBaz2, { id: baz.id });
Expand Down Expand Up @@ -938,7 +938,7 @@ describe('EntityManagerPostgre', () => {

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });
const res = await orm.em.find(Author2, { books: { title: { $in: ['b1', 'b2'] } } }, ['books.perex']);
expect(res).toHaveLength(1);
expect(res[0].books.length).toBe(2);
Expand Down Expand Up @@ -1024,11 +1024,11 @@ describe('EntityManagerPostgre', () => {

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
orm.em.config.set('debug', true);
Object.assign(orm.config, { logger });
orm.config.set('debug', true);
await orm.em.nativeInsert(Author2, { name: 'native name 1', email: 'native1@email.com' });
expect(mock.mock.calls[0][0]).toMatch('insert into "author2" ("email", "name") values (\'native1@email.com\', \'native name 1\') returning "id", "created_at", "updated_at"');
orm.em.config.set('debug', ['query']);
orm.config.set('debug', ['query']);
});

test('Utils.prepareEntity changes entity to number id', async () => {
Expand Down Expand Up @@ -1063,7 +1063,7 @@ describe('EntityManagerPostgre', () => {
test('self referencing (1 step)', async () => {
const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });

const author = new Author2('name', 'email');
author.favouriteAuthor = author;
Expand Down Expand Up @@ -1139,7 +1139,7 @@ describe('EntityManagerPostgre', () => {

const mock = jest.fn();
const logger = new Logger(mock, true);
Object.assign(orm.em.config, { logger });
Object.assign(orm.config, { logger });
const res1 = await orm.em.find(Book2, { author: { name: 'Jon Snow' } }, ['perex']);
expect(res1).toHaveLength(3);
expect(res1[0].test).toBeUndefined();
Expand Down Expand Up @@ -1251,7 +1251,7 @@ describe('EntityManagerPostgre', () => {

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

// with paginate flag (and a bit of dark sql magic) we get what we want
const res2 = await orm.em.find(Author2, { books: { title: /^Bible/ } }, {
Expand Down
6 changes: 5 additions & 1 deletion tests/entities-sql/Book2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ export class Book2 {
@ManyToMany(() => BookTag2, undefined, { pivotTable: 'book_to_tag_unordered', orderBy: { name: QueryOrder.ASC } })
tagsUnordered = new Collection<BookTag2>(this);

constructor(title: string, author: Author2) {
constructor(title: string, author: Author2, price?: number) {
this.title = title;
this.author = author;

if (price) {
this.price = price;
}
}

}
Expand Down

0 comments on commit d6c2736

Please sign in to comment.