Skip to content

Commit

Permalink
feat(mongodb): add support for indexes in mongo driver (#393)
Browse files Browse the repository at this point in the history
MongoDB driver now supports indexes and unique constraints. You can use `@Index()` and `@Unique()`.
To automatically create new indexes when initializing the ORM, you need to enable `ensureIndexes` option.

```typescript
const orm = await MikroORM.init({
  entitiesDirs: ['entities'], // relative to `baseDir`
  dbName: 'my-db-name',
  type: 'mongo',
  ensureIndexes: true, // defaults to false
});
```

Alternatively you can call `ensureIndexes()` method on the `MongoDriver`:

await orm.em.getDriver().ensureIndexes();

Closes #159
  • Loading branch information
B4nan committed Mar 8, 2020
1 parent 3be7eb1 commit 7155549
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 7 deletions.
22 changes: 22 additions & 0 deletions docs/docs/usage-with-mongo.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ const orm = await MikroORM.init({
await orm.em.getDriver().createCollections();
```

## Indexes

Starting with v3.4, MongoDB driver supports indexes and unique constraints. You can
use `@Index()` and `@Unique()` as described in [Defining Entities section](defining-entities.md#indexes).
To automatically create new indexes when initializing the ORM, you need to enable
`ensureIndexes` option.

```typescript
const orm = await MikroORM.init({
entitiesDirs: ['entities'], // relative to `baseDir`
dbName: 'my-db-name',
type: 'mongo',
ensureIndexes: true, // defaults to false
});
```

Alternatively you can call `ensureIndexes()` method on the `MongoDriver`:

```typescript
await orm.em.getDriver().ensureIndexes();
```

## Native collection methods

Sometimes you need to perform some bulk operation, or you just want to populate your
Expand Down
4 changes: 4 additions & 0 deletions lib/MikroORM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class MikroORM<D extends IDatabaseDriver = IDatabaseDriver> {
orm.driver.setMetadata(orm.metadata);
await orm.connect();

if (orm.config.get('ensureIndexes')) {
await orm.driver.ensureIndexes();
}

return orm;
}

Expand Down
8 changes: 8 additions & 0 deletions lib/connections/MongoConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export class MongoConnection extends Connection {
return this.db.createCollection(this.getCollectionName(name));
}

dropCollection(name: EntityName<AnyEntity>): Promise<boolean> {
return this.db.dropCollection(this.getCollectionName(name));
}

getDefaultClientUrl(): string {
return 'mongodb://127.0.0.1:27017';
}
Expand All @@ -56,6 +60,10 @@ export class MongoConnection extends Connection {
return match ? `${match[1]}://${options.auth ? options.auth.user + ':*****@' : ''}${match[2]}` : clientUrl;
}

getDb(): Db {
return this.db;
}

async execute(query: string): Promise<any> {
throw new Error(`${this.constructor.name} does not support generic execute method`);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
return this.dependencies;
}

async ensureIndexes(): Promise<void> {
throw new Error(`${this.constructor.name} does not use ensureIndexes`);
}

protected getPivotOrderBy(prop: EntityProperty, orderBy?: QueryOrderMap): QueryOrderMap {
if (orderBy) {
return orderBy;
Expand Down
2 changes: 2 additions & 0 deletions lib/drivers/IDatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface IDatabaseDriver<C extends Connection = Connection> {

setMetadata(metadata: MetadataStorage): void;

ensureIndexes(): Promise<void>;

/**
* Returns name of the underlying database dependencies (e.g. `mongodb` or `mysql2`)
* for SQL drivers it also returns `knex` in the array as connectors are not used directly there
Expand Down
42 changes: 38 additions & 4 deletions lib/drivers/MongoDriver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ClientSession, ObjectId } from 'mongodb';
import { DatabaseDriver } from './DatabaseDriver';
import { MongoConnection } from '../connections/MongoConnection';
import { EntityData, AnyEntity, FilterQuery } from '../typings';
import { EntityData, AnyEntity, FilterQuery, EntityMetadata } from '../typings';
import { Configuration, Utils } from '../utils';
import { MongoPlatform } from '../platforms/MongoPlatform';
import { FindOneOptions, FindOptions } from './IDatabaseDriver';
Expand Down Expand Up @@ -70,9 +70,43 @@ export class MongoDriver extends DatabaseDriver<MongoConnection> {
}

async createCollections(): Promise<void> {
await Promise.all(Object.values(this.metadata.getAll()).map(meta => {
return this.getConnection('write').createCollection(meta.collection);
}));
const promises = Object.values(this.metadata.getAll())
.map(meta => this.getConnection('write').createCollection(meta.collection));

await Promise.all(promises);
}

async dropCollections(): Promise<void> {
const db = this.getConnection('write').getDb();
const collections = await db.listCollections().toArray();
const existing = collections.map(c => c.name);
const promises = Object.values(this.metadata.getAll())
.filter(meta => existing.includes(meta.collection))
.map(meta => this.getConnection('write').dropCollection(meta.collection));

await Promise.all(promises);
}

async ensureIndexes(): Promise<void> {
await this.createCollections();
const promises: Promise<string>[] = [];

const createIndexes = (meta: EntityMetadata, type: 'indexes' | 'uniques') => {
meta[type].forEach(index => {
const properties = Utils.asArray(index.properties).map(prop => meta.properties[prop].fieldName);
promises.push(this.getConnection('write').getCollection(meta.name).createIndex(properties, {
name: index.name,
unique: type === 'uniques',
}));
});
};

for (const meta of Object.values(this.metadata.getAll())) {
createIndexes(meta, 'indexes');
createIndexes(meta, 'uniques');
}

await Promise.all(promises);
}

private renameFields<T>(entityName: string, data: T): T {
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
autoJoinOneToOneOwner: true,
propagateToOneOwner: true,
forceUtcTimezone: false,
ensureIndexes: false,
tsNode: false,
debug: false,
verbose: false,
Expand Down Expand Up @@ -285,6 +286,7 @@ export interface MikroORMOptions<D extends IDatabaseDriver = IDatabaseDriver> ex
autoJoinOneToOneOwner: boolean;
propagateToOneOwner: boolean;
forceUtcTimezone: boolean;
ensureIndexes: boolean;
hydrator: { new (factory: EntityFactory, em: EntityManager): Hydrator };
entityRepository: { new (em: EntityManager, entityName: EntityName<AnyEntity>): EntityRepository<AnyEntity> };
replicas?: Partial<ConnectionOptions>[];
Expand Down
18 changes: 18 additions & 0 deletions tests/EntityManager.mongo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,24 @@ describe('EntityManagerMongo', () => {
expect(driver.getConnection().getCollection(BookTag.name).collectionName).toBe('book-tag');
});

test('ensure indexes', async () => {
await orm.em.getDriver().ensureIndexes();
const conn = orm.em.getDriver().getConnection('write');
const authorInfo = await conn.getCollection('author').indexInformation();
const bookInfo = await conn.getCollection('books-table').indexInformation();
expect(authorInfo).toEqual({
_id_: [['_id', 1]],
born_1: [['born', 1]],
email_1: [['email', 1]],
custom_idx_1: [['name', 1], ['email', 1]],
});
expect(bookInfo).toEqual({
_id_: [['_id', 1]],
publisher_idx: [['publisher', 1]],
title_1_author_1: [['title', 1], ['author', 1]],
});
});

test('should use user and password as connection options', async () => {
const config = new Configuration({ user: 'usr', password: 'pw' } as any, false);
const connection = new MongoConnection(config);
Expand Down
1 change: 1 addition & 0 deletions tests/EntityManager.mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe('EntityManagerMySql', () => {
expect(driver.getPlatform().denormalizePrimaryKey(1)).toBe(1);
expect(driver.getPlatform().denormalizePrimaryKey('1')).toBe('1');
await expect(driver.find(BookTag2.name, { books: { $in: [1] } })).resolves.not.toBeNull();
await expect(driver.ensureIndexes()).rejects.toThrowError('MySqlDriver does not use ensureIndexes');
});

test('driver appends errored query', async () => {
Expand Down
4 changes: 3 additions & 1 deletion tests/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ export async function initORMMongo() {
highlight: false,
logger: i => i,
type: 'mongo',
ensureIndexes: true,
implicitTransactions: true,
cache: { pretty: true },
});

// create collections first so we can use transactions
await orm.em.getDriver().createCollections();
await orm.em.getDriver().dropCollections();
await orm.em.getDriver().ensureIndexes();

return orm;
}
Expand Down
5 changes: 4 additions & 1 deletion tests/entities/Author.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
AfterCreate, AfterDelete, AfterUpdate, BeforeCreate, BeforeDelete, BeforeUpdate, DateType,
Cascade, Collection, Entity, EntityAssigner, ManyToMany, ManyToOne, OneToMany, Property, wrap,
Cascade, Collection, Entity, EntityAssigner, ManyToMany, ManyToOne, OneToMany, Property, wrap, Index, Unique,
} from '../../lib';

import { Book } from './Book';
import { AuthorRepository } from '../repositories/AuthorRepository';
import { BaseEntity } from './BaseEntity';

@Entity({ customRepository: () => AuthorRepository })
@Index({ name: 'custom_idx_1', properties: ['name', 'email'] })
export class Author extends BaseEntity {

static beforeDestroyCalled = 0;
Expand All @@ -16,6 +17,7 @@ export class Author extends BaseEntity {
@Property()
name: string;

@Unique()
@Property()
email: string;

Expand All @@ -32,6 +34,7 @@ export class Author extends BaseEntity {
identities?: string[];

@Property({ type: DateType })
@Index()
born?: Date;

@OneToMany(() => Book, book => book.author, { referenceColumnName: '_id', cascade: [Cascade.PERSIST], orphanRemoval: true })
Expand Down
4 changes: 3 additions & 1 deletion tests/entities/Book.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ObjectId } from 'mongodb';
import { Cascade, Collection, Entity, IdentifiedReference, ManyToMany, ManyToOne, PrimaryKey, Property, wrap } from '../../lib';
import { Cascade, Collection, Entity, IdentifiedReference, Index, ManyToMany, ManyToOne, PrimaryKey, Property, Unique, wrap } from '../../lib';
import { Publisher } from './Publisher';
import { Author } from './Author';
import { BookTag } from './book-tag';
import { BaseEntity3 } from './BaseEntity3';

@Entity({ tableName: 'books-table' })
@Unique({ properties: ['title', 'author'] })
export class Book extends BaseEntity3 {

@PrimaryKey()
Expand All @@ -18,6 +19,7 @@ export class Book extends BaseEntity3 {
author: Author;

@ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE] })
@Index({ name: 'publisher_idx' })
publisher!: IdentifiedReference<Publisher, '_id' | 'id'>;

@ManyToMany()
Expand Down

0 comments on commit 7155549

Please sign in to comment.