10

I'm trying to figure out the best way to handle a one-to-many relationship using type-graphql and typeorm with a postgresql db (using apollo server with express). I have a user table which has a one-to-many relation with a courses table. The way I am currently handling this is to use the @RelationId field to create a column of userCourseIds and then using @FieldResolver with dataloader to batch fetch the courses that belong to that user(s). My issue is that with the @RelationId field, a separate query is made to get the relationids whether or not I actually query for userCourses. Is there a way to handle this where it won't make that extra query or is there a better way to go about handling one-to-many relationships?

User side of relation:

@OneToMany(() => Course, (course) => course.creator, { cascade: true })
userCourses: Course[];
@RelationId((user: User) => user.userCourses)
userCourseIds: string;

Course side of relation:

@Field()
@Column()
creatorId: string;

@ManyToOne(() => User, (user) => user.userCourses)
creator: User;

userCourses FieldResolver:

@FieldResolver(() => [Course], { nullable: true })
async userCourses(@Root() user: User, @Ctx() { courseLoader }: MyContext) {
  const userCourses = await courseLoader.loadMany(user.userCourseIds);
  return userCourses;
}
Joel Jacobsen
  • 313
  • 1
  • 5
  • 15

1 Answers1

9

userCourses field can be written like below using @TypeormLoader decorator provided by type-graphql-datalaoader.

@ObjectType()
@Entity()
class User {
  ...

  @Field((type) => [Course])
  @OneToMany(() => Course, (course) => course.creator, { cascade: true })
  @TypeormLoader((course: Course) => course.creatorId, { selfKey: true })
  userCourses: Course[];
}

@ObjectType()
@Entity()
class Course {
  ...

  @ManyToOne(() => User, (user) => user.userCourses)
  creator: User;

  @RelationId((course: Course) => course.creator)
  creatorId: string;
}

Additional queries will not be issued anymore since userCourseIds is omitted. Although creatorId with @RelationId exists, it will not issue extra queries since the entity has the value by its own.

Update: June 2021

@TypeormLoader no longer needs arguments. Therefore @RelationId can be completely omitted, if preferred.

Kazuaki Tanida
  • 146
  • 1
  • 5
  • @RelationId makes additional queries for me. – NeverBe Nov 10 '20 at 12:04
  • Is the relation property owned by the entity? `@ManyToOne` and `@OneToOne` with `@JoinColumn` own the id. `@RelationId` for them will not trigger extra queries. – Kazuaki Tanida Nov 10 '20 at 15:19
  • how GraphQL dataloader work with @RelationId ? I think it is ignored. N+1 issue guaranteed – NeverBe Nov 10 '20 at 15:23
  • In the above case, `creatorId` will not trigger extra queries since it is for `@ManyToOne` proterty. `@TypeormLoader` of `userCourses` selects all `course`s which have the `user`'s id inside its batchLoadFn under the hood. – Kazuaki Tanida Nov 10 '20 at 15:35
  • `course.creatorId` is needed for `@TypeormLoader` so that it can know the condition of the select query. – Kazuaki Tanida Nov 10 '20 at 15:46