mikro-orm: ValidationError: You cannot call em.flush() from inside lifecycle hook handlers

Describe the bug My problem is related to #627 . As pretty much same thing is happening to me but not a custom repository but a service class. Is it cause I am not using a DI? I don’t even know. I first used apollo-server-fastify and thought it had to something to do with fastify so I switched to apollo-server-express but it didn’t solve the issue. I tried adding RequestContext middleware but it still didn’t work.

Here is the github repo. It contains a single user entity with basic crud operations. But here are the main files;

  1. Server init
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { MikroORM, RequestContext } from "mikro-orm";
import config from "./mikro-orm.config";
import { buildSchema } from "type-graphql";

export const DI = {} as {
  orm: MikroORM;
};
// Init Server
(async () => {
  const app = express();
  const port = process.env.PORT ? Number(process.env.PORT) : 3010;

  try {
    DI.orm = await MikroORM.init(config);

    const apolloServer = new ApolloServer({
      schema: await buildSchema({
        resolvers: [__dirname + "/modules/**/*.resolver.{ts,js}"],
        emitSchemaFile: false,
      }),
      context: ({ req, res }) => ({ req, res }),
    });

    app.use((_req, _res, next) => {
      RequestContext.create(DI.orm.em, next);
    });

    apolloServer.applyMiddleware({ app });

    app.listen(port, "0.0.0.0", () => console.log(`Server running on port: ${port}`));
  } catch (err) {
    console.log(err);
    process.exit(1);
  }
})();

  1. Orm config
import { Options } from "mikro-orm";
import { BaseEntity } from "./globals/entity";
import { User } from "./modules/user/user.entity";

const config: Options = {
  type: "postgresql",
  clientUrl: "postgres://postgres:test@localhost:5432/mikro",
  entities: [BaseEntity, User],
  migrations: {
    path: "./src/migrations",
  },
};

export default config;
  1. User Entity
import { Entity, Property } from "mikro-orm";
import { BaseEntity } from "../../globals/entity";
import { ObjectType, Field, Int } from "type-graphql";
import bcrypt from "bcryptjs";

@ObjectType()
@Entity()
export class User extends BaseEntity {
  @Field()
  @Property({ unique: true })
  username: string;

  @Field()
  @Property({ unique: true })
  email: string;

  @Property()
  password: string;

  @Field(() => Int)
  @Property()
  age: number;

  @Field()
  @Property()
  sex: string;

  async hashPassword(): Promise<void> {
    const salt = await bcrypt.genSalt();
    this.password = await bcrypt.hash(this.password, salt);
  }

  async comparePassword(password: string): Promise<boolean> {
    return bcrypt.compare(password, this.password);
  }
}
  1. User Resolver
import { Resolver, Mutation, Query, Args, Arg } from "type-graphql";
import { User } from "./user.entity";
import UserService from "./user.service";
import {
  CreateUserDTO,
  UserFilterDTO,
  UserPaginationDTO,
  IdDTO,
  UpdateUserDTO,
  UpdateUserPasswordDTO,
} from "./user.dto";

@Resolver(User)
export class UserResolver {
  public readonly userService = new UserService();

  @Mutation(() => User)
  async createUser(@Args() dto: CreateUserDTO): Promise<User> {
    return this.userService.createUser(dto);
  }

  @Query(() => [User])
  async getUsers(
    @Arg("filter", { nullable: true }) filter: UserFilterDTO,
    @Args() pagination: UserPaginationDTO,
  ): Promise<User[]> {
    return this.userService.getUsers(filter, pagination);
  }

  @Query(() => User)
  async getUser(@Args() { id }: IdDTO): Promise<User> {
    return this.userService.getUser(id);
  }

  @Mutation(() => User)
  async updateUser(@Args() { id }: IdDTO, @Args() dto: UpdateUserDTO): Promise<User> {
    return this.userService.updateUser(id, dto);
  }

  @Mutation(() => User)
  async updateUserPassword(@Args() { id }: IdDTO, @Args() dto: UpdateUserPasswordDTO): Promise<User> {
    return this.userService.updateUserPassword(id, dto);
  }

  @Mutation(() => Boolean)
  async deleteUser(@Args() { id }: IdDTO): Promise<boolean> {
    return this.userService.deleteUser(id);
  }
}
  1. User Service
export default class UserService {
  protected readonly userRepo = DI.orm.em.getRepository(User);

  async createUser(dto: CreateUserDTO): Promise<User> {
    const { username, email, password, confirm, age, sex } = dto;

    if (password !== confirm) {
      throw new Error("match fail");
    }

    const user = this.userRepo.create({
      username,
      email,
      password,
      age,
      sex,
    });
    try {
      await user.hashPassword();
      await this.userRepo.persist(user, true);
    } catch (error) {
      if (error.code === "23505") {
        const rxp = /\(([^)]+)\)/;
        const key = rxp.exec(error.detail);
        if (key) {
          throw new Error(`${key[1]} already exists`);
        }
      } else {
        throw new Error(error);
      }
    }

    return user;
  }

async getUsers(filter: UserFilterDTO, pagination: UserPaginationDTO): Promise<User[]> {
    const query = this.userRepo.createQueryBuilder("user");

    if (filter) {
      const { username, email } = filter;
      if (username != null) {
        query.where({ username: { $like: username } });
      }
      if (email != null) {
        // This is how I wrote in TYPEORM
        // query.andWhere("user.email ILIKE :email", {email});
        // what is the equivalent of this in mikro-orm
      }
    }

    const { limit, offset, sort, by } = pagination;
    query
      .limit(limit)
      .offset(offset)
      .orderBy({ [by]: sort });

    console.log(query.getQuery(), query.getParams());

    return query.getResult();
  }
}

Can you review the code too and see if I can improve it or something that I did bad? Also how do you use ILIKE

Stack trace

ValidationError: You cannot call em.flush() from inside lifecycle hook handlers
    at Function.cannotCommit (/home/red/Documents/work/rewise/server/node_modules/mikro-orm/dist/utils/ValidationError.js:129:16)
    at UnitOfWork.commit (/home/red/Documents/work/rewise/server/node_modules/mikro-orm/dist/unit-of-work/UnitOfWork.js:87:43)
    at EntityManager.flush (/home/red/Documents/work/rewise/server/node_modules/mikro-orm/dist/EntityManager.js:330:36)
    at EntityManager.persistAndFlush (/home/red/Documents/work/rewise/server/node_modules/mikro-orm/dist/EntityManager.js:275:20)
    at EntityManager.persist (/home/red/Documents/work/rewise/server/node_modules/mikro-orm/dist/EntityManager.js:265:25)
    at EntityRepository.persist (/home/red/Documents/work/rewise/server/node_modules/mikro-orm/dist/entity/EntityRepository.js:9:24)
    at UserService.<anonymous> (/home/red/Documents/work/rewise/server/src/modules/user/user.service.ts:24:27)
    at Generator.next (<anonymous>)
    at fulfilled (/home/red/Documents/work/rewise/server/src/modules/user/user.service.ts:5:58) {
  entity: undefined
}

To Reproduce Steps to reproduce the behavior:

  1. Create a user
  2. Create/update another user with already used email/username Now the method is unusable

Expected behavior Expected to work normal but after unique constraint error is thrown, following request on that method fails showing this error.

Additional context

Versions

Dependency Version
node 12.18.0
typescript 3.9.7
mikro-orm 3.6.15

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 29 (16 by maintainers)

Commits related to this issue

Most upvoted comments

Creating ‘forks’ or instantiating a new UoW for every request seems like a pretty big overhead imho.

Nope, it is not a big overhead. Feel free to measure yourself, with AsyncLocalStorage it is very cheap. Only some of the classes are instantiated that way (mainly EM and UoW), all of the expensive resources are shared.

Using dedicated contexts for each request is not an option you can take, it is mandatory. Without that you will run into serialization issues.

Changing manually the “working” flag is a dirty fix as I wrote. I intend to leave this only until this issue is fixed. Note that the flag change is made only for this specific exception catch (allowing the UoW state to stay invalid for other exceptions). Since you suggest to drop the entity manager instead, I’ll comply. Noticing it is still a dirty fix to drop the whole entity manager and instantiate a new one.

My point was to say “The unit of work should not break after a constraint violation.” and to give a clue where the issue may happen. Sorry if I haven’t been explicit enough.

There won’t be any fix for this. As I said, you should use clean EM (so creating new fork or calling em.clear()) after an error is thrown. In this simple example, it might look like its a good idea to reuse the UoW, but in complex apps this won’t fly. There might be many changes in the UoW, one of them causes the error, but all the entities can be interconnected, and persisting “another one that won’t produce the error” is not enough, as you still have all the old entities in it. Do this instead:

try {
  em.persist(myEntity);
  await em.flush(); // Fails because of unique constraint violation
} catch(e) {
  // ...
}
// Few lines later
em.clear(); // reset the EM (and its UoW)
// em = em.fork(); // or create new fork
em.persist(anotherEntity); // Those entity's data are correct
em.flush(); // Now working! :D

Or you can use em.clear() which is pretty much the same as creating new fork (it will also reset the UoW).

Hello,

I am experiencing the same issue “ValidationError: You cannot call em.flush() from inside lifecycle hook handlers” when flushing a second time after the first one crashed on unique constraint.

em.persist(myEntity);
try {
  em.flush(); // Fails because of unique constraint violation
}
catch(e) {
  if (e instanceof DatabaseError && e.code === '23505') {
    // Handle duplicate error
  }
}
// Few lines later
em.persist(anotherEntity); // Those entity's data are correct
em.flush(); // Throws : ValidationError: You cannot call em.flush() from inside lifecycle hook handlers

The exception thrown is DatabaseError from the node.js pg module (used by MikroORM):

error: insert into "XXX" ("XXX", "XXX", ...) values ($1, $2, $3, $4, $5) returning "XXX" - duplicate key value violates unique constraint "uniq_YYY"

at Parser.parseErrorMessage (../node_modules/pg-protocol/src/parser.ts:357:11)
at Parser.handlePacket (../node_modules/pg-protocol/src/parser.ts:186:21)
at Parser.parse (../node_modules/pg-protocol/src/parser.ts:101:30)
at Socket.stream.on (../node_modules/pg-protocol/src/index.ts:7:48)

My guess is the ORM rollbacks correctly but since the exception is not its own, it does not come back to an idle/available state.

Doing a dirty em.unitOfWork.working = false; hacked the issue:

em.persist(myEntity);
try {
  em.flush(); // Fails because of unique constraint violation
}
catch(e) {
  if (e instanceof DatabaseError && e.code === '23505') {
    // Handle duplicate error
    em.unitOfWork.working = false;
  }
}
// Few lines later
em.persist(anotherEntity); // Those entity's data are correct
em.flush(); // Now working! :D

The ORM is now available for new tasks. 😃

I am using version MikroORM version 3.6.15.

The trouble with apollo-server-express seems is because it include and enable by default body-parser.

if disabling body-parser in apollo-server-express and handle it manually before RequestContext all seems work well:

application.use("/", bodyParser.json());
application.use((_req, _res, next) => RequestContext.create(DI.orm.em, next));
apolloServer.applyMiddleware({ app: application, path: "/", bodyParserConfig: false });