Skip to content

Commit

Permalink
feat(core): allow disabling identity map and change set tracking (#1307)
Browse files Browse the repository at this point in the history
Sometimes we might want to disable identity map and change set tracking for some query.
This is possible via `disableIdentityMap` option. Behind the scenes, it will create new
context, load the entities inside that, and clear it afterwards, so the main identity map
will stay clean.

```ts
const users = await orm.em.find(User, { email: '[email protected]' }, {
  disableIdentityMap: true,
  populate: { cars: { brand: true } },
});
users[0].name = 'changed';
await orm.em.flush(); // calling flush have no effect, as the entity is not managed
```

> Keep in mind that this can also have
> [negative effect on the performance](https://stackoverflow.com/questions/9259480/entity-framework-mergeoption-notracking-bad-performance).

Closes #1267
  • Loading branch information
B4nan authored Jan 15, 2021
1 parent fc35c22 commit 03da184
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 3 deletions.
22 changes: 22 additions & 0 deletions docs/docs/entity-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,28 @@ where lower(email) = '[email protected]'
order by (point(loc_latitude, loclongitude) <@> point(0, 0)) asc
```
## Disabling identity map and change set tracking
Sometimes we might want to disable identity map and change set tracking for some query.
This is possible via `disableIdentityMap` option. Behind the scenes, it will create new
context, load the entities inside that, and clear it afterwards, so the main identity map
will stay clean.
> As opposed to _managed_ entities, such entities are called _detached_.
> To be able to work with them, you first need to merge them via `em.registerManaged()`.
```ts
const users = await orm.em.find(User, { email: '[email protected]' }, {
disableIdentityMap: true,
populate: { cars: { brand: true } },
});
users[0].name = 'changed';
await orm.em.flush(); // calling flush have no effect, as the entity is not managed
```
> Keep in mind that this can also have
> [negative effect on the performance](https://stackoverflow.com/questions/9259480/entity-framework-mergeoption-notracking-bad-performance).
## Type of Fetched Entities
Both `em.find` and `em.findOne()` methods have generic return types.
Expand Down
20 changes: 19 additions & 1 deletion packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async find<T extends AnyEntity<T>, P extends Populate<T> = any>(entityName: EntityName<T>, where: FilterQuery<T>, populate?: P | FindOptions<T, P>, orderBy?: QueryOrderMap, limit?: number, offset?: number): Promise<Loaded<T, P>[]> {
const options = Utils.isObject<FindOptions<T, P>>(populate) ? populate : { populate, orderBy, limit, offset } as FindOptions<T, P>;

if (options.disableIdentityMap) {
const fork = this.fork(false);
const ret = await fork.find<T, P>(entityName, where, { ...options, disableIdentityMap: false });
fork.clear();

return ret;
}

entityName = Utils.className(entityName);
where = await this.processWhere(entityName, where, options, 'read');
this.validator.validateParams(where);
Expand Down Expand Up @@ -272,8 +281,17 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
* Finds first entity matching your `where` query.
*/
async findOne<T extends AnyEntity<T>, P extends Populate<T> = any>(entityName: EntityName<T>, where: FilterQuery<T>, populate?: P | FindOneOptions<T, P>, orderBy?: QueryOrderMap): Promise<Loaded<T, P> | null> {
entityName = Utils.className(entityName);
const options = Utils.isObject<FindOneOptions<T, P>>(populate) ? populate : { populate, orderBy } as FindOneOptions<T, P>;

if (options.disableIdentityMap) {
const fork = this.fork(false);
const ret = await fork.findOne<T, P>(entityName, where, { ...options, disableIdentityMap: false });
fork.clear();

return ret;
}

entityName = Utils.className(entityName);
const meta = this.metadata.get<T>(entityName);
where = await this.processWhere(entityName, where, options, 'read');
this.validator.validateEmptyWhere(where);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/drivers/IDatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export interface FindOptions<T, P extends Populate<T> = Populate<T>> {
offset?: number;
refresh?: boolean;
convertCustomTypes?: boolean;
disableIdentityMap?: boolean;
fields?: (string | FieldsMap)[];
schema?: string;
flags?: QueryFlag[];
Expand Down
2 changes: 0 additions & 2 deletions tests/EntityManager.mongo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1886,8 +1886,6 @@ describe('EntityManagerMongo', () => {
await orm.em.flush();
orm.em.clear();

const tags = await orm.em.find(BookTag, {}, ['books']);
console.log(tags);
let tag = await orm.em.findOneOrFail(BookTag, tag1.id, ['books']);
const err = 'You cannot modify inverse side of M:N collection BookTag.books when the owning side is not initialized. Consider working with the owning side instead (Book.tags).';
expect(() => tag.books.add(orm.em.getReference(Book, book4.id))).toThrowError(err);
Expand Down
47 changes: 47 additions & 0 deletions tests/EntityManager.sqlite2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,53 @@ describe('EntityManagerSqlite2', () => {
expect(book.tags.count()).toBe(0);
});

test('disabling identity maap', async () => {
const author = orm.em.create(Author4, { name: 'Jon Snow', email: '[email protected]' });
const book1 = orm.em.create(Book4, { title: 'My Life on the Wall, part 1', author });
const book2 = orm.em.create(Book4, { title: 'My Life on the Wall, part 2', author });
const book3 = orm.em.create(Book4, { title: 'My Life on the Wall, part 3', author });
const tag1 = orm.em.create(BookTag4, { name: 'silly' });
const tag2 = orm.em.create(BookTag4, { name: 'funny' });
const tag3 = orm.em.create(BookTag4, { name: 'sick' });
const tag4 = orm.em.create(BookTag4, { name: 'strange' });
const tag5 = orm.em.create(BookTag4, { name: 'sexy' });
book1.tags.add(tag1, tag3);
book2.tags.add(tag1, tag2, tag5);
book3.tags.add(tag2, tag4, tag5);

orm.em.persist(book1);
orm.em.persist(book2);
await orm.em.persist(book3).flush();
orm.em.clear();

const authors = await orm.em.find(Author4, {}, {
populate: { books: { tags: true } },
disableIdentityMap: true,
});

expect(authors).toHaveLength(1);
expect(authors[0].id).toBe(author.id);
expect(authors[0].books).toHaveLength(3);
expect(authors[0].books[0].id).toBe(book1.id);
expect(authors[0].books[0].tags).toHaveLength(2);
expect(authors[0].books[0].tags[0].name).toBe('silly');
expect(orm.em.getUnitOfWork().getIdentityMap().values().length).toBe(0);

const a1 = await orm.em.findOneOrFail(Author4, author.id, {
populate: { books: { tags: true } },
disableIdentityMap: true,
});
expect(a1.id).toBe(author.id);
expect(a1.books).toHaveLength(3);
expect(a1.books[0].id).toBe(book1.id);
expect(a1.books[0].tags).toHaveLength(2);
expect(a1.books[0].tags[0].name).toBe('silly');
expect(orm.em.getUnitOfWork().getIdentityMap().values().length).toBe(0);

expect(a1).not.toBe(authors[0]);
expect(a1.books[0]).not.toBe(authors[0].books[0]);
});

test('populating many to many relation', async () => {
const p1 = orm.em.create(Publisher4, { name: 'foo' });
expect(p1.tests).toBeInstanceOf(Collection);
Expand Down

0 comments on commit 03da184

Please sign in to comment.