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;
- 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);
}
})();
- 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;
- 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);
}
}
- 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);
}
}
- 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:
- Create a user
- 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)
Links to this issue
Commits related to this issue
- feat(core): add `EntityRepositoryType` symbol This allows to pass the type of your custom repository in the entity definition, which will be then used in `em.getRepository(Entity)`. ```typescript @E... — committed to mikro-orm/mikro-orm by B4nan 4 years ago
- feat(core): add `EntityRepositoryType` symbol (#698) This allows to pass the type of your custom repository in the entity definition, which will be then used in `em.getRepository(Entity)`. ```typ... — committed to mikro-orm/mikro-orm by B4nan 4 years ago
- feat(core): add `EntityRepositoryType` symbol (#698) This allows to pass the type of your custom repository in the entity definition, which will be then used in `em.getRepository(Entity)`. ```typ... — committed to mikro-orm/mikro-orm by B4nan 4 years ago
- feat(core): add `EntityRepositoryType` symbol (#698) This allows to pass the type of your custom repository in the entity definition, which will be then used in `em.getRepository(Entity)`. ```typ... — committed to mikro-orm/mikro-orm by B4nan 4 years ago
Nope, it is not a big overhead. Feel free to measure yourself, with
AsyncLocalStorageit 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.
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: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.
The exception thrown is DatabaseError from the node.js pg module (used by MikroORM):
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:The ORM is now available for new tasks. 😃
I am using version MikroORM version 3.6.15.
The trouble with
apollo-server-expressseems is because it include and enable by defaultbody-parser.if disabling
body-parserinapollo-server-expressand handle it manually beforeRequestContextall seems work well: