Skip to main content
Version: Next

Populating relations

MikroORM is capable of loading large nested structures while maintaining good performance, querying each database table only once. Imagine you have this nested structure:

  • Book has one Publisher (M:1), one Author (M:1) and many BookTags (M:N)
  • Publisher has many Tests (M:N)

When you use nested populate while querying all BookTags, this is what happens in the background:

const tags = await em.find(BookTag, {}, {
populate: ['books.publisher.tests', 'books.author'],
});
console.log(tags[0].books[0].publisher.tests[0].name); // prints name of nested test
console.log(tags[0].books[0].author.name); // prints name of nested author
  1. Load all BookTags
  2. Load all Books associated with previously loaded BookTags
  3. Load all Publishers associated with previously loaded Books
  4. Load all Tests associated with previously loaded Publishers
  5. Load all Authors associated with previously loaded Books

For SQL drivers with pivot tables this means:

SELECT `e0`.* FROM `book_tag` AS `e0`;

SELECT `e0`.*, `e1`.`book_id`, `e1`.`book_tag_id`
FROM `book` AS `e0` LEFT JOIN `book_to_book_tag` AS `e1` ON `e0`.`id` = `e1`.`book_id`
WHERE `e1`.`book_tag_id` IN (?, ?, ?, ?, ?)
ORDER BY `e1`.`id` ASC;

SELECT `e0`.* FROM `publisher` AS `e0` WHERE `e0`.`id` IN (?, ?, ?);

SELECT `e0`.*, `e1`.`test_id`, `e1`.`publisher_id`
FROM `test` AS `e0` LEFT JOIN `publisher_to_test` AS `e1` ON `e0`.`id` = `e1`.`test_id`
WHERE `e1`.`publisher_id` IN (?, ?, ?)
ORDER BY `e1`.`id` ASC;

SELECT `e0`.* FROM `author` AS `e0` WHERE `e0`.`id` IN (?);

For mongo driver it's even simpler as no pivot tables are involved:

db.getCollection("book-tag").find({}).toArray();
db.getCollection("book").find({"tags":{"$in":[...]}}).toArray();
db.getCollection("publisher").find({"_id":{"$in":[...]}}).toArray();
db.getCollection("test").find({"_id":{"$in":[...]}}).toArray();
db.getCollection("author").find({"_id":{"$in":[...]}}).toArray();

Populating all relations

You can also populate all relationships by passing populate: ['*']. The result will be also strictly typed (the Loaded type respects the star hint).

const tags = await em.find(BookTag, {}, {
populate: ['*'],
});

This will always use select-in strategy to deal with possible cycles.

Inferring populate hint from filter

If you want to automatically select all the relations that are part of your filter query, use populate: ['$infer']:

// this will populate all the books and their authors, all via a single query
const tags = await em.find(BookTag, {
books: { author: { name: '...' } },
}, {
populate: ['$infer'],
});

This will always use joined strategy as we already have the relations joined because they are in the filter. This feature is not available in MongoDB driver as there is no join support.

Filter on populated entities

The request to populate can be ambiguous. For example, let's say as a hypothetical that there's a Book called 'One' with tags 'Fiction' and 'Hard Cover'.

Then you run the following:

const books = await em.find(Book, { tags: { name: 'Fiction' } }, {
populate: ['tags'],
});

You're requesting books that have the tag of 'Fiction' then asking to populate the tags on each book. Did you mean that you want to populate all tags on each book that matches the filter? If so, you'd expect that book 'One' would have both 'Fiction' and 'Hard Cover' populated. Or did you mean that we should only populate the tags that match the outer filter? If so you'd expect that book 'One' would only have 'Fiction' in the populated collection because the outer filter specified that.

Both behaviors are useful in different cases, so MikroORM provides an option that allows you to control this called populateWhere. There are two options, INFER and ALL. The default is ALL which will ensure that all possible members of the collection are fetched in the populate (e.g. the first interpretation above).

You can specify this globally:

const orm = await MikroORM.init({
// We want our populate fetches to respect the outer filter passed in a where condition.
populateWhere: PopulateHint.INFER,
});

Or you can override this on a query by query basis:

const books = await em.find(Book, { tags: { name: 'Fiction' } }, {
populate: ['tags'],
populateWhere: PopulateHint.INFER,
});

Using PopulateHint.INFER in this case instructs MikroORM to interpret the find as per the second interpretation above.

A value provided on a specific query overrides whatever default is specified globally.

Loading strategies

The way that MikroORM fetches the data based on populate hint is also configurable. By default, MikroORM uses a "select in" strategy which runs one separate query for each level of a populate. If you're using an SQL database you can also ask MikroORM to use a join for all tables involved in the populate and run it as a single query. This is again configurable globally or per query.

For more information see the Loading Strategies section.

Populating already loaded entities

To populate existing entities, you can use em.populate().

const authors = await em.createQueryBuilder(Author).select('*').getResult();
await em.populate(authors, ['books.tags']);

// now your Author entities will have `books` collections populated,
// as well as they will have their `tags` collections populated.
console.log(authors[0].books[0].tags[0]); // initialized BookTag