mikro-orm
Simple typescript mongo ORM for node.js based on data-mapper, unit-of-work and identity-map patterns.
Heavily inspired by doctrine.
Installation & Usage
Fist install the module via yarn
or npm
:
$ yarn add mikro-orm
or
$ npm i -s mikro-orm
Then call MikroORM.init
as part of bootstrapping your app:
const orm = await MikroORM.init({
entitiesDirs: ['entities'],
dbName: 'my-db-name',
clientUrl: '...',
baseDir: __dirname,
});
console.log(orm.em);
And do not forget to clear entity manager before each request if you do not want
to store all loaded entities in memory:
const app = express();
app.use((req, res, next) => {
orm.em.clear();
next();
});
Now you can define your entities (in one of the entitiesDirs
folders):
Defining entity
@Entity({ collection: 'books-table' })
export class Book extends BaseEntity {
@Property()
title: string;
@ManyToOne({ entity: () => Author.name })
author: Author;
@ManyToOne({ entity: () => Publisher.name })
publisher: Publisher;
constructor(title: string, author: Author) {
super();
this.title = title;
this.author = author;
}
}
With your entities set up, you can start using entity manager and repositories as described
in following section. For more examples, take a look at tests/EntityManager.test.ts
.
Persisting and cascading
To save entity state to database, you need to persist it. Persist takes care or deciding
whether to use insert
or update
and computes appropriate change-set. Entity references
that are not persisted yet (does not have identifier) will be cascade persisted automatically.
const author = new Author('Jon Snow', 'snow@wall.st');
author.born = new Date();
const publisher = new Publisher('7K publisher');
const book1 = new Book('My Life on The Wall, part 1', author);
book1.publisher = publisher;
const book2 = new Book('My Life on The Wall, part 2', author);
book2.publisher = publisher;
const book3 = new Book('My Life on The Wall, part 3', author);
book3.publisher = publisher;
await orm.em.persist([book1, book2, book3]);
await orm.em.persist(book1, false);
await orm.em.persist(book2, false);
await orm.em.persist(book3);
Fetching entities with EntityManager
To fetch entities from database you can use find()
and findOne()
of EntityManager
:
API:
EntityManager.getCollection(entityName: string): Collection;
EntityManager.getRepository<T extends BaseEntity>(entityName: string): EntityRepository<T>;
EntityManager.find<T extends BaseEntity>(entityName: string, where?: FilterQuery<T>, populate?: string[], orderBy?: { [k: string]: 1 | -1; }, limit?: number, offset?: number): Promise<T[]>;
EntityManager.findOne<T extends BaseEntity>(entityName: string, where: FilterQuery<T> | string, populate?: string[]): Promise<T>;
EntityManager.merge<T extends BaseEntity>(entityName: string, data: any): T;
EntityManager.getReference<T extends BaseEntity>(entityName: string, id: string): T;
EntityManager.remove(entityName: string, where: BaseEntity | any): Promise<number>;
EntityManager.removeEntity(entity: BaseEntity): Promise<number>;
EntityManager.count(entityName: string, where: any): Promise<number>;
EntityManager.persist(entity: BaseEntity | BaseEntity[], flush?: boolean): Promise<void>;
EntityManager.flush(): Promise<void>;
EntityManager.clear(): void;
EntityManager.canPopulate(entityName: string, property: string): boolean;
Example:
const author = orm.em.findOne(Author.name, '...id...');
const books = orm.em.find(Book.name, {});
for (const author of authors) {
console.log(author.name);
for (const book of author.books) {
console.log(book.title);
console.log(book.author.isInitialized());
console.log(book.author.id);
console.log(book.author.name);
console.log(book.publisher);
console.log(book.publisher.isInitialized());
console.log(book.publisher.id);
console.log(book.publisher.name);
}
}
Using EntityRepository
instead of EntityManager
More convenient way of fetching entities from database is by using EntityRepository
, that
carries the entity name so you do not have to pass it to every find
and findOne
calls:
API:
EntityRepository.persist(entity: BaseEntity, flush?: boolean): Promise<void>;
EntityRepository.findOne(where: FilterQuery<BaseEntity> | string, populate?: string[]): Promise<BaseEntity>;
EntityRepository.find(where: FilterQuery<BaseEntity>, populate?: string[], orderBy?: { [k: string]: 1 | -1; }, limit?: number, offset?: number): Promise<BaseEntity[]>;
EntityRepository.findAll(populate?: string[], orderBy?: { [k: string]: 1 | -1; }, limit?: number, offset?: number): Promise<BaseEntity[]>;
EntityRepository.remove(where: BaseEntity | any): Promise<number>;
EntityRepository.flush(): Promise<void>;
EntityRepository.canPopulate(property: string): boolean;
EntityRepository.count(where?: any): Promise<number>;
Example:
const booksRepository = orm.em.getRepository<Book>(Book.name);
const books = await booksRepository.find({ author: '...' }, ['author'], { title: -1 }, 2, 1);
console.log(books);
Custom repository
To use custom repository, just extend EntityRepository<T>
class:
export class CustomAuthorRepository extends EntityRepository<Author> {
public findAndUpdate(...) {
}
}
And register your repository as @Entity
decorator:
@Entity({ customRepository: CustomAuthorRepository })
export class Publisher extends BaseEntity {
}
Then your custom repository can be accessed via EntityManager.getRepository()
method.
Core features
Identity Map
MikroORM
uses identity map in background so you will always get the same instance of
one entity.
const authorRepository = orm.em.getRepository<Author>(Author.name);
const jon = await authorRepository.findOne({ name: 'Jon Snow' }, ['books']);
const authors = await authorRepository.findAll(['books']);
console.log(jon === authors[0]);
If you want to clear this identity map cache, you can do so via EntityManager.clear()
method:
orm.em.clear();
Entity references
Every single entity relation is mapped to an entity reference. Reference is an entity that has
only its identifier. This reference is stored in identity map so you will get the same object
reference when fetching the same document from database.
You can call await entity.init()
to initialize the entity. This will trigger database call
and populate itself, keeping the same reference in identity map.
const author = await orm.em.getReference('...id...');
console.log(author.id);
console.log(author.isInitialized());
console.log(author.name);
await author.init();
console.log(author.isInitialized());
console.log(author.name);
Using entity constructors
Internally, MikroORM
never calls entity constructor, so you are free to use it as you wish.
The constructor will be called only when you instantiate the class yourself via new
operator,
so it is a handy place to require your data when creating new entity.
For example following Book
entity definition will always require to set title
and author
,
but publisher
will be optional:
@Entity()
export class Book extends BaseEntity {
@Property()
title: string;
@ManyToOne({ entity: () => Author.name })
author: Author;
@ManyToOne({ entity: () => Publisher.name })
publisher: Publisher;
@ManyToMany({ entity: () => BookTag.name, inversedBy: 'books' })
tags: Collection<BookTag>;
constructor(title: string, author: Author) {
super();
this.title = title;
this.author = author;
}
}
ObjectID
and string
duality
Every entity has both ObjectID
and string
id available, also all methods of EntityManager
and EntityRepository
supports querying by both of them.
const author = await orm.em.getReference('...id...');
console.log(author.id);
console.log(author._id);
const article = '...article id...';
const book = '...book id...';
const repo = orm.em.getRepository<Author>(Author.name);
const foo1 = await repo.find({ id: { $in: [article] }, favouriteBook: book });
const bar1 = await repo.find({ id: { $in: [new ObjectID(article)] }, favouriteBook: new ObjectID(book) });
const foo2 = await repo.find({ _id: { $in: [article] }, favouriteBook: book });
const bar2 = await repo.find({ _id: { $in: [new ObjectID(article)] }, favouriteBook: new ObjectID(book) });
Collections
OneToMany
and ManyToMany
collections are stored in a Collection
wrapper. It implements
iterator so you can use for of
loop to iterate through it.
const author = orm.em.findOne(Author.name, '...', ['books']);
await author.books.init();
for (const book of author.books) {
console.log(book.title);
console.log(book.author.isInitialized());
console.log(book.author.id);
console.log(book.author.name);
console.log(book.publisher);
console.log(book.publisher.isInitialized());
console.log(book.publisher.id);
console.log(book.publisher.name);
}
author.books.add(book);
console.log(author.books.contains(book));
author.books.remove(book);
console.log(author.books.contains(book));
author.books.add(book);
console.log(author.books.count());
author.books.removeAll();
console.log(author.books.contains(book));
console.log(author.books.count());
console.log(author.books.getItems());
console.log(author.books.getIdentifiers());
console.log(author.books.getIdentifiers('id'));
OneToMany
collections
OneToMany
collections are inverse side of ManyToOne
references, to which they need to point via fk
attribute:
@Entity()
export class Book extends BaseEntity {
@ManyToOne({ entity: () => Author.name })
author: Author;
}
@Entity()
export class BookTag extends BaseEntity {
@OneToMany({ entity: () => Book.name, fk: 'author' })
books: Collection<Book>;
}
ManyToMany
collections
As opposed to SQL databases, with MongoDB we do not need to have join tables for ManyToMany
relations.
All references are stored as an array of ObjectID
s on owning entity.
Unidirectional
Unidirectional ManyToMany
relations are defined only on one side, and marked explicitly as owner
:
@ManyToMany({ entity: () => Book.name, owner: true })
books: Collection<Book>;
Bidirectional
Bidirectional ManyToMany
relations are defined on both sides, while one is owning side (where references are store),
marked by inversedBy
attribute pointing to the inverse side:
@ManyToMany({ entity: () => BookTag.name, inversedBy: 'books' })
tags: Collection<BookTag>;
And on the inversed side we define it with mappedBy
attribute poining back to the owner:
@ManyToMany({ entity: () => Book.name, mappedBy: 'tags' })
books: Collection<Book>;
Updating entity values with BaseEntity.assign()
When you want to update entity based on user input, you will usually have just plain
string ids of entity relations as user input. Normally you would need to use
EntityManager.getReference()
to create references from each id first, and then
use those references to update entity relations:
const jon = new Author('Jon Snow', 'snow@wall.st');
const book = new Book('Book', jon);
book.author = orm.em.getReference<Author>(Author.name, '...id...');
Same result can be easily achieved with BaseEntity.assign()
:
book.assign({
title: 'Better Book 1',
author: '...id...',
});
console.log(book.title);
console.log(book.author);
console.log(book.author.id);
TODO
- cascade persist in collections
- aggregate support?
- improve populating of collections in EM#find() method
- add nativeUpdate and nativeDelete (without hooks support), allow only entities in EM#remove
- cascade remove references on other entities when deleting entity (e.g. from M:N collection)
TODO docs
- lifecycle hooks
- property type validation