Skip to content

Commit

Permalink
feat(core): allow configuring aliasing naming strategy (#2419)
Browse files Browse the repository at this point in the history
Implements different table aliasing for select-in loading strategy and for QueryBuilder
and makes it configurable.

BREAKING CHANGE:
Previously with select-in strategy as well as with QueryBuilder, table aliases
were always the letter `e` followed by unique index. In v5, we use the same
method as with joined strategy - the letter is inferred from the entity name.

This can be breaking if you used the aliases somewhere, e.g. in custom SQL
fragments. We can restore to the old behaviour by implementing custom naming
strategy, overriding the `aliasName` method:

```ts
import { AbstractNamingStrategy } from '@mikro-orm/core';

class CustomNamingStrategy extends AbstractNamingStrategy {
  aliasName(entityName: string, index: number) {
    return 'e' + index;
  }
}
```

Note that in v5 it is possible to use `expr()` helper to access the alias name
dynamically, e.g. ``expr(alias => `lower('${alias}.name')`)``, which should be
now preferred way instead of hardcoding the aliases.
  • Loading branch information
B4nan committed Nov 14, 2021
1 parent f09dc66 commit 89d63b3
Show file tree
Hide file tree
Showing 37 changed files with 512 additions and 444 deletions.
15 changes: 15 additions & 0 deletions docs/docs/naming-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,18 @@ Return a join table name. This is used as default value for `pivotTable`.
Return the foreign key column name for the given parameters.

---

#### `NamingStrategy.indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence'): string`

Returns key/constraint name for given type. Some drivers might not support all
the types (e.g. mysql and sqlite enforce the PK name).

---

#### `NamingStrategy.aliasName(entityName: string, index: number): string`

Returns alias name for given entity. The alias needs to be unique across the
query, which is by default ensured via appended index parameter. It is optional
to use it as long as you ensure it will be unique.

---
24 changes: 24 additions & 0 deletions docs/docs/upgrading-v4-to-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,27 @@ Previously those dynamically added getters returned the array copy of collection
items. In v5, we return the collection instance, which is also iterable and has
a `length` getter and indexed access support, so it mimics the array. To get the
array copy as before, call `getItems()` as with a regular collection.

## Different table aliasing for select-in loading strategy and for QueryBuilder

Previously with select-in strategy as well as with QueryBuilder, table aliases
were always the letter `e` followed by unique index. In v5, we use the same
method as with joined strategy - the letter is inferred from the entity name.

This can be breaking if you used the aliases somewhere, e.g. in custom SQL
fragments. We can restore to the old behaviour by implementing custom naming
strategy, overriding the `aliasName` method:

```ts
import { AbstractNamingStrategy } from '@mikro-orm/core';

class CustomNamingStrategy extends AbstractNamingStrategy {
aliasName(entityName: string, index: number) {
return 'e' + index;
}
}
```

Note that in v5 it is possible to use `expr()` helper to access the alias name
dynamically, e.g. ``expr(alias => `lower('${alias}.name')`)``, which should be
now preferred way instead of hardcoding the aliases.
2 changes: 1 addition & 1 deletion packages/core/src/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
protected comparator!: EntityComparator;
protected metadata!: MetadataStorage;

protected constructor(protected readonly config: Configuration,
protected constructor(readonly config: Configuration,
protected readonly dependencies: string[]) { }

abstract find<T>(entityName: string, where: FilterQuery<T>, options?: FindOptions<T>): Promise<EntityData<T>[]>;
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/naming-strategy/AbstractNamingStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export abstract class AbstractNamingStrategy implements NamingStrategy {
return columnName.replace(/[_ ](\w)/g, m => m[1].toUpperCase()).replace(/_+/g, '');
}

aliasName(entityName: string, index: number): string {
// Take only the first letter of the prefix to keep character counts down since some engines have character limits
return entityName.charAt(0).toLowerCase() + index;
}

abstract classToTableName(entityName: string): string;

abstract joinColumnName(propertyName: string): string;
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/naming-strategy/NamingStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,10 @@ export interface NamingStrategy {
*/
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence'): string;

/**
* Returns alias name for given entity. The alias needs to be unique across the query, which is by default
* ensured via appended index parameter. It is optional to use it as long as you ensure it will be unique.
*/
aliasName(entityName: string, index: number): string;

}
2 changes: 1 addition & 1 deletion packages/knex/src/query/ObjectCriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
}

private autoJoin<T>(qb: IQueryBuilder<T>, alias: string): string {
const nestedAlias = qb.getNextAlias();
const nestedAlias = qb.getNextAlias(this.prop?.pivotTable ?? this.entityName);
const customExpression = ObjectCriteriaNode.isCustomExpression(this.key!);
const scalar = Utils.isPrimaryKey(this.payload) || this.payload instanceof RegExp || this.payload instanceof Date || customExpression;
const operator = Utils.isPlainObject(this.payload) && Object.keys(this.payload).every(k => Utils.isOperator(k, false));
Expand Down
28 changes: 18 additions & 10 deletions packages/knex/src/query/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type {
MetadataStorage,
PopulateOptions,
QBFilterQuery,
QueryOrderMap } from '@mikro-orm/core';
QueryOrderMap,
} from '@mikro-orm/core';
import {
LoadStrategy,
LockMode,
Expand Down Expand Up @@ -48,6 +49,8 @@ import type { Field, JoinOptions } from '../typings';
*/
export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {

readonly alias: string;

/** @internal */
type!: QueryType;
/** @internal */
Expand All @@ -57,7 +60,7 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
/** @internal */
_populateMap: Dictionary<string> = {};

private aliasCounter = 1;
private aliasCounter = 0;
private flags: Set<QueryFlag> = new Set([QueryFlag.CONVERT_CUSTOM_TYPES]);
private finalized = false;
private _joins: Dictionary<JoinOptions> = {};
Expand All @@ -79,7 +82,7 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
private subQueries: Dictionary<string> = {};
private readonly platform = this.driver.getPlatform();
private readonly knex = this.driver.getConnection(this.connectionType).getKnex();
private readonly helper = new QueryBuilderHelper(this.entityName, this.alias, this._aliasMap, this.subQueries, this.metadata, this.knex, this.platform);
private readonly helper: QueryBuilderHelper;

/**
* @internal
Expand All @@ -88,10 +91,16 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
private readonly metadata: MetadataStorage,
private readonly driver: AbstractSqlDriver,
private readonly context?: Knex.Transaction,
readonly alias = `e0`,
alias?: string,
private connectionType?: 'read' | 'write',
private readonly em?: SqlEntityManager) {
if (alias) {
this.aliasCounter++;
}

this.alias = alias ?? this.getNextAlias(this.entityName);
this._aliasMap[this.alias] = this.entityName;
this.helper = new QueryBuilderHelper(this.entityName, this.alias, this._aliasMap, this.subQueries, this.metadata, this.knex, this.platform);
}

select(fields: Field<T> | Field<T>[], distinct = false): this {
Expand Down Expand Up @@ -426,9 +435,8 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
return join?.inverseAlias || join?.alias;
}

getNextAlias(prefix = 'e'): string {
// Take only the first letter of the prefix to keep character counts down since some engines have character limits
return `${prefix.charAt(0).toLowerCase()}${this.aliasCounter++}`;
getNextAlias(entityName = 'e'): string {
return this.driver.config.getNamingStrategy().aliasName(entityName, this.aliasCounter++);
}

/**
Expand Down Expand Up @@ -581,7 +589,7 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {

if (type !== 'pivotJoin') {
const oldPivotAlias = this.getAliasForJoinPath(path + '[pivot]');
pivotAlias = oldPivotAlias ?? `e${this.aliasCounter++}`;
pivotAlias = oldPivotAlias ?? this.getNextAlias(prop.pivotTable);
}

const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path);
Expand Down Expand Up @@ -739,7 +747,7 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
this.autoJoinPivotTable(field);
} else if (meta && this.helper.isOneToOneInverse(field)) {
const prop = meta.properties[field];
this._joins[prop.name] = this.helper.joinOneToReference(prop, this.alias, `e${this.aliasCounter++}`, 'leftJoin');
this._joins[prop.name] = this.helper.joinOneToReference(prop, this.alias, this.getNextAlias(prop.pivotTable ?? prop.type), 'leftJoin');
this._populateMap[field] = this._joins[field].alias;
}
});
Expand Down Expand Up @@ -821,7 +829,7 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
const owner = pivotMeta.props.find(prop => prop.reference === ReferenceType.MANY_TO_ONE && prop.owner)!;
const inverse = pivotMeta.props.find(prop => prop.reference === ReferenceType.MANY_TO_ONE && !prop.owner)!;
const prop = this._cond[pivotMeta.name + '.' + owner.name] || this._orderBy[pivotMeta.name + '.' + owner.name] ? inverse : owner;
const pivotAlias = this.getNextAlias();
const pivotAlias = this.getNextAlias(pivotMeta.name!);

this._joins[field] = this.helper.joinPivotTable(field, prop, this.alias, pivotAlias, 'leftJoin');
Utils.renameKey(this._cond, `${field}.${owner.name}`, Utils.getPrimaryKeyHash(owner.fieldNames.map(fieldName => `${pivotAlias}.${fieldName}`)));
Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/query/ScalarCriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class ScalarCriteriaNode extends CriteriaNode {
if (this.shouldJoin()) {
const path = this.getPath();
const parentPath = this.parent!.getPath(); // the parent is always there, otherwise `shouldJoin` would return `false`
const nestedAlias = qb.getAliasForJoinPath(path) || qb.getNextAlias();
const nestedAlias = qb.getAliasForJoinPath(path) || qb.getNextAlias(this.prop?.pivotTable ?? this.entityName);
const field = `${alias}.${this.prop!.name}`;
const type = this.prop!.reference === ReferenceType.MANY_TO_MANY ? 'pivotJoin' : 'leftJoin';
qb.join(field, nestedAlias, undefined, type, path);
Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export interface IQueryBuilder<T> {
groupBy(fields: (string | keyof T) | (string | keyof T)[]): this;
having(cond?: QBFilterQuery | string, params?: any[]): this;
getAliasForJoinPath(path: string): string | undefined;
getNextAlias(prefix?: string): string;
getNextAlias(entityName?: string): string;
}

export interface ICriteriaNode {
Expand Down
2 changes: 1 addition & 1 deletion tests/DatabaseDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Driver extends DatabaseDriver<Connection> {

protected readonly platform = new Platform1();

constructor(protected readonly config: Configuration,
constructor(readonly config: Configuration,
protected readonly dependencies: string[]) {
super(config, dependencies);
}
Expand Down
Loading

0 comments on commit 89d63b3

Please sign in to comment.