Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use DataLoader? #1135

Open
zuohuadong opened this issue May 9, 2018 · 38 comments
Open

How to use DataLoader? #1135

zuohuadong opened this issue May 9, 2018 · 38 comments

Comments

@zuohuadong
Copy link

Are there any relevant examples?

https://github.com/facebook/dataloader

@Mirokko
Copy link

Mirokko commented May 9, 2018

You probably wanna start with Interceptors.

@Mirokko
Copy link

Mirokko commented May 12, 2018

@twilroad Nope, but link above is basically everything you need to get started. Interceptor can be used for caching (you can see it in docs), but with slight modifications you can store result from next handler. As a param, you will need a cache key, you can pass it like shown here https://docs.nestjs.com/advanced/mixins.

@caseyduquettesc
Copy link

caseyduquettesc commented Oct 4, 2018

@babbarankit
Copy link

Sample Code

export class ApplicationModule implements NestModule {
  constructor(
    private readonly articleService: ArticleService,
    private readonly graphQLFactory: GraphQLFactory,
    private readonly userService: UserService,
    private readonly cinemaClassicsService: CinemaClassicsService,
    private readonly artformCategoryService: ArtformCategoryService,
    private readonly configService: ConfigService,
    private readonly geoChapterService: GeoChapterService,
    private readonly downloadCategoryService: DownloadCategoryService,
    private readonly artformService: ArtformService,
    private readonly personalProfileService: PersonalProfileService,
    private readonly artistProfileService: ArtistProfileService,
    private readonly artistListService: ArtistListService,
  ) {}
  configure(consumer: MiddlewareConsumer) {
    const awsSettings = this.configService.getAWSConfig();
    const disableCors = this.configService.disableCors();
    let headers: {
      'Access-Control-Allow-Origin'?: string;
    } = {};
    if (disableCors) {
      headers = { 'Access-Control-Allow-Origin': 'http://localhost:3010' };
    }
    consumer
      .apply(
        require('react-s3-uploader/s3router')({
          bucket: awsSettings.defaultBucket,
          region: awsSettings.defaultRegion,
          signatureVersion: 'v4',
          headers,
          ACL: 'public-read',
          uniquePrefix: true,
        }),
      )
      .forRoutes('/s3');
    consumer.apply(AuthMiddleware).forRoutes('/health/anonymous');

    consumer.apply(AuthMiddleware).forRoutes('/health/authenticated');

    consumer.apply(AuthMiddleware).forRoutes('/query');

    const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql', './../node_modules/@ankitbabbar/instiapp-api-core/src/**/*.graphql');
    const resolvers = {
      DateTime: GraphQLDateTime,
      Date: GraphQLDate,
      Time: GraphQLTime,
    };

    const schema = this.graphQLFactory.createSchema({
      typeDefs,
      resolvers,
      resolverValidationOptions: {
        requireResolversForResolveType: false,
      },
    });

    consumer
      .apply(
        graphqlExpress((req, res) => {
          const viewer = res.locals.viewer;
          const dataLoaders: ContribDataLoaders = {
            user: new DataLoader((ids: number[]) => this.userService.getManyByIds(viewer, ids)),
            article: new DataLoader((ids: number[]) => this.articleService.getManyByIds(viewer, ids)),
            cinemaClassics: new DataLoader((ids: number[]) => this.cinemaClassicsService.getManyByIds(viewer, ids)),
            artform: new DataLoader((ids: number[]) => this.artformService.getManyByIds(viewer, ids)),
            geoChapter: new DataLoader((ids: number[]) => this.geoChapterService.getManyByIds(viewer, ids)),
            artformCategory: new DataLoader((ids: number[]) => this.artformCategoryService.getManyByIds(viewer, ids)),
            downloadCategory: new DataLoader((ids: number[]) => this.downloadCategoryService.getManyByIds(viewer, ids)),
            personalProfile: new DataLoader((ids: number[]) => this.personalProfileService.getManyByIds(viewer, ids)),
            artistList: new DataLoader((ids: number[]) => this.artistListService.getManyByIds(viewer, ids)),
            artistProfile: new DataLoader((ids: number[]) => this.artistProfileService.getManyByIds(viewer, ids)),
          };
          const context: IGQLContext = {
            viewer,
            dataLoaders,
          };
          return {
            schema,
            rootValue: req,
            formatError,
            context,
          };
        }),
      )
      .forRoutes('/query');
  }
}

@biels
Copy link

biels commented Nov 6, 2018

How would this work with the new GraphQLModule.forRoot({...options})? Where should we initialize the context? Maybe creating a wrapper module and doing it on configure(consumer) somehow? It should be a different instance of data loader(s) for each different request.

@mohaalak
Copy link

How would this work with the new GraphQLModule.forRoot({...options})? Where should we initialize the context? Maybe creating a wrapper module and doing it on configure(consumer) somehow? It should be a different instance of data loader(s) for each different request.

take a look at this approach
nestjs/graphql#2 (comment)

@kamilmysliwiec kamilmysliwiec changed the title use dataloader How to use DataLoader? Dec 29, 2018
@rychkog
Copy link

rychkog commented Apr 11, 2019

@kamilmysliwiec Dataloader is supposed to batch all the resolved entity ids and query them in one go as a single SQL query.

But for some reason when used in Nest it executes more than one query for a single request.

Here is an example

Suppose we have a following GQL query

query {
  persons {
    name
    address {
      street
    }
  }
}

So addresses for all the persons from this query should be fetched using a single SQL query (using dataloader approach) but in reality, I have something like this:

  1. All the persons fetched
  2. Dataloader collects and loads address ids for persons 1 and 2 (SQL query 1)
  3. Then for person 3 (SQL query 2)
  4. Then for persons 4 and 5 (SQL query 3)

I know that dataloader should be invoked on a nextTick but looks like there is something in a way NestJS implements resolvers.

Any help is appreciated, maybe I just missed something regarding the dataloader..

I think this is related graphql/dataloader#150

@rychkog
Copy link

rychkog commented Apr 29, 2019

For those who faced the same issue, I've mentioned above.
It was caused by the fact that on every GQL property resolution Nest invokes my Auth global guard which also does lots of async stuff, interfering with dataloader's execution.

I've added a flag that checks whether a user is already authenticated in the guard in order to prevent its redundant executions.

@kamilmysliwiec Is this expected behavior that guards (probably interceptors, filters, etc. as well) get executed on every GQL property resolution?

@kamilmysliwiec
Copy link
Member

@kamilmysliwiec Is this expected behavior that guards (probably interceptors, filters, etc. as well) get executed on every GQL property resolution?

@rychkog this is fixed in @nestjs/graphql@next already. I'll publish this release as soon as possible.

@jakubnavratil
Copy link

@kamilmysliwiec maybe this could be somewhat optional? you disabled all guards, filters and interceptors. what if I want to run guard againts some property?

In my case I was running interceptor altering the context and this was breaking change. I think this ability could be actually in ResolveProperty options or something, especially guards should be allowed.

@kamilmysliwiec
Copy link
Member

@trubit it would have a big impact on the performance. I'd suggest moving this logic inside the resolve property handler.

@EdouardBougon
Copy link

I agree with @trubit , it's a breaking change.

For example, I use guard to control access to my properties, with a AuthGuard and RoleGuard.
Some properties are like "query", with filter, pagination etc... and need the same rules as query or mutation.

So now, we will have 2 different implementations to check roles and auth ...

@jakubnavratil
Copy link

I imagine this line
https://github.com/nestjs/graphql/blob/1504e021af3119feb8d8ecaed29b47d695939d9c/lib/services/resolvers-explorer.service.ts#L134
could be somehow referenced in @ResolveProperty's decorator options. At least making it possible to add guards on properties. Of course it takes some performance hits, was it so big?

@vnenkpet
Copy link

vnenkpet commented Jun 10, 2019

@zuohuadong How I do it is I use request-scoped providers that create instances of injectable services that have dataloaders in them.

export interface IDataLoader<K, V> {
  load(id: K): Promise<V>;
}
import DataLoader from 'dataloader';
import { UserEntity } from '../user.entity';
import { IDataLoader } from 'src/api/common/interfaces/data-loader.interface';
import { Connection } from 'typeorm';

export class UsersDataLoader implements IDataLoader<string, UserEntity> {
  constructor(private readonly dataLoader: DataLoader<string, UserEntity>) {}

  public static async create(connection: Connection): Promise<UsersDataLoader> {
    const usersRepo = connection.getRepository<UserEntity>(UserEntity);
    const dataLoader = new DataLoader<string, UserEntity>(async keys => {
      const loadedEntities = await usersRepo.findByIds(keys);
      return keys.map(key => loadedEntities.find(entity => entity.id === key)); // sort by keys
    });

    return new UsersDataLoader(dataLoader);
  }

  public async load(id: string) {
    return this.dataLoader.load(id);
  }
}
import { Provider, Scope } from '@nestjs/common';
import { Types } from 'src/common/types';
import { UsersDataLoader } from './users.data-loader';

export const usersDataLoaderProvider: Provider = {
  inject: [Types.DATABASE_CONNECTION],
  useFactory: UsersDataLoader.create,
  provide: UsersDataLoader,
  scope: Scope.REQUEST,
};

Then you simply inject it as a regular service in the resolver.

@Resolver(of => PostType)
export class PostResolver {
  constructor(
    private readonly usersDataLoader: UsersDataLoader,
  ) {}
  @ResolveProperty('author')
  public async author(@Parent() post: PostEntity) {
    return this.usersDataLoader.load(post.authorId);
  }
}

This way you can also use the dataloaders anywhere in the code, like in service classes, not only in resolver methods.

@secmohammed
Copy link

@vnenkpet this throws an error Nest can't resolve dependencies of the UserDataLoader (?).

@secmohammed
Copy link

it took me like 2 days debugging how to use DataLoader with Nestjs, and finally I could make it.
so here is what I did perhaps this might help someone else in the future.

first create your contract as @vnenkpet did.

export interface IDataLoader<K, V> {
    load(id: K): Promise<V>;
}

then create your user loader for example.

import { Injectable, Inject, forwardRef } from "@nestjs/common";
import { ModuleRef } from "@nestjs/core";
import { UserDTO } from "@commerce/shared";
import DataLoader = require("dataloader"); // commonjs module

import { IDataLoader } from "../contracts/nest-dataloader";
import { UserService } from "../users/user.service";

@Injectable()
// you can replace the UserDTO with your entity or whatever, just pass what you want to accept and return in the end.
export class UserDataLoader implements IDataLoader<string, UserDTO> {
    constructor(private readonly dataLoader: DataLoader<any, any>) {}

    public static async create(
        userService: UserService // injecting the user service as I'm going to use it to fetch the users by ids
    ): Promise<UserDataLoader> {
        // your Dataloader, which again accepts the ids and return whatever you want.
        const dataloader = new DataLoader<string, UserDTO>(async ids => {
            let users = await userService.fetchUsersByIds(ids);
            // after having the response, map the ids you already have with the users you got from the service by ids. So Dataloader can navigate later.
            return ids.map(key => users.find(entity => entity.id === key));
        });
        return new UserDataLoader(dataloader);
    }
    public async load(id: string) {
        return this.dataLoader.load(id);
    }
}

In my scenario, I had to use this userService at another module, so here is the code for that.

at your users module you must put that service at providers property in order to work, and you have to export it from there as you are going to use it in another module ( so you can import it from there )

// at your users module.
@Module({
    providers:  [UserService, UserResolver],
    exports: [UserService]
})

at the other module you are trying to use the loader at ( which in my case was products module and I needed to fetch users associated with these products.)

// at your products module or whatever module.

@Module({
    providers: [
        ProductResolver,
        ProductService,
        {
            inject: [UserService],
            useFactory: UserDataLoader.create,
            provide: UserDataLoader,
            scope: Scope.REQUEST
        }
    ],
    imports: [UsersModule]
})

then finally at ProductResolver, I injected it.


 constructor(
        private readonly usersDataLoader: UserDataLoader
    ) {}

then when resolving the property at the same resolver.

 @ResolveProperty("user", () => UserDTO)
    async user(@Parent() product: ProductDTO): Promise<UserDTO> {
        return this.usersDataLoader.load(product.user.id);
    }

@jeppe-smith
Copy link

jeppe-smith commented Sep 22, 2019

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

@pablor21
Copy link

@secmohammed @jeppe-smith For some reason, your solution doesn't fix the n+1 problem, I've enabled the query logger in mysql and I can see that the fetch queries for the related objects are fired one by one.
If I remove the Scope.REQUEST, then the queries are executed once, but the loader cache keeps the results until the app closes, this will be problematic if we are loading a huge amount of related objects (since the objects are saved on memory), we could end up loading the whole database in memory...

@secmohammed
Copy link

Dataloader is designed to end up the cached version by the end of the request. It was designed to do so. this will help you to batch a multiple requests with the ids and by the end of the promise fetch all of the requests at once. you can integrate with a Redis server or so to accomplish having a cached version of your data. Also, Having a N+1 is basically based on your sql query you are trying to do, it should be where id is in (...your ids), and this one will be batched at the very last of your promise to fetch them at once. However, I don't know how Scope.Request makes the change.

@pablor21
Copy link

pablor21 commented Sep 23, 2019

The problem seems be that the DataLoader is instantiated multiple times inside the request: on each result...
The Scope.DEFAULT, obviously keep the same instance of DataLoader, since it's singleton...

@EdouardBougon
Copy link

I agree with @pablor21, I encountered the same problem with multiple instance by request. May be it's a bug with Nest.
So, for now, I use this solution: nestjs/graphql#2 (comment)

@jeppe-smith
Copy link

@pablor21 How are you sending requests that cause multiple instances of the dataloader? If I send a query like

query {
  first: getFixtureByCode(code: 855172) {
    id
  }
  second: getFixtureByCode(code: 855172) {
    id
  }
  third: getFixtureByCode(code: 855174) {
    id
  }
}

I get a single instance of dataloader and my database is queried once with { codes: [855172, 855174] } as I would expect it to.

@krislefeber
Copy link

I created a small npm package, nestjs-dataloader, since I also struggled with this. Hopefully it helps someone else.

@Mautriz
Copy link

Mautriz commented Mar 22, 2020

I struggled a lot trying to understand this, I'll just write my solution which probably isn't the cleanest but it's easiest for myself at least

PS: the "genericOneToManyDataLoader" can be of course custom made made for every field, I was just experimenting some generic way (btw, if someone can help me make it typesafe I would appreciate a lot, I don't know how to do it, now it just returns "any[]")

export const genericOneToManyDataLoader = (t: any, keyField: string) =>
  new DataLoader<number, typeof t[]>(async (keys: number[]) => {
    const items = await getRepository(t).find({
      where: { [keyField]: In(keys) },
    });

    return keys.map(id => items.filter(el => el[keyField] === id));
  });

export interface MyContext {
  req: Request;
  res: Response;
  commentDataLoader: ReturnType<typeof genericOneToManyDataLoader>;
  postDataLoader: ReturnType<typeof genericOneToManyDataLoader>;
}

@Module({
  imports: [
    PostModule,
    CommentModule,
    UserModule,
    GraphQLModule.forRoot({
      playground: true,
      debug: true,
      autoSchemaFile: true,
      installSubscriptionHandlers: true,
      context: ({ req, res }) => ({
        req,
        res,
        commentDataLoader: genericOneToManyDataLoader(Comment, 'postId'),
        postDataLoader: genericOneToManyDataLoader(Post, 'userId'),
      }),
    }),
    DatabaseModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
// user.resolver.ts -- its literally the same in comments
 @ResolveField(() => [Post], { nullable: 'items' })
  async posts(@Parent() parent: User, @Context() ctx: MyContext) {
    return ctx.postDataLoader.load(parent.id);
  }

@Mautriz
Copy link

Mautriz commented Mar 23, 2020

UPDATE: I needed services in loaders. and the above solution of mine was bad, I ended up using @jeppe-smith s one which worked very well
It generated three queries (as expected) for a users -> posts -> comments (many -> many -> many) query, and to me it seems the easiest solution

Thank you a lot for that, I don't understand why others say this generates multiple dataloaders/queries, I get only one per request as expected

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

@kamilmysliwiec kamilmysliwiec transferred this issue from nestjs/graphql Apr 4, 2020
@TreeMan360
Copy link

https://github.com/TreeMan360/nestjs-graphql-dataloader

@WillSquire
Copy link
Contributor

WillSquire commented Jul 1, 2020

This is what I've got:

import { Injectable, Scope } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import DataLoader from 'dataloader'
import { Repository } from 'typeorm'
import { Project } from './project.entity'

@Injectable({ scope: Scope.REQUEST })
export class ProjectLoader {
  constructor(
    @InjectRepository(Project)
    private readonly projectRepository: Repository<Project>,
  ) {}

  readonly findOneByIds = new DataLoader<string, Project | undefined>(async (ids) => {
    const projects = await this.projectRepository
      .createQueryBuilder('project')
      .where('project.id IN (:...ids)', { ids })
      .getMany()

    return ids.map((id) => projects.find((n) => n.id === id))
  })
}

Perhaps it's a naive attempt, but I've made the loader a data access layer containing the database logic, which the service layer accesses and doesn't overlap with. Trouble I'm having is @Inject(CONTEXT) private context seems to throw an error when implemented like in the docs: https://docs.nestjs.com/fundamentals/injection-scopes. Really need to initialise the dataloaders with the current user within the request to work out access rights, like in the dataloader docs: https://github.com/graphql/dataloader#creating-a-new-dataloader-per-request.

Anyone else having this issue?

EDIT:
Got it working getting the current user through the request, although I couldn't get the param decorator to work at the moment (like with the resolvers):

import { Inject, Injectable, Scope } from '@nestjs/common'
import { CONTEXT } from '@nestjs/graphql'
import { InjectRepository } from '@nestjs/typeorm'
import DataLoader from 'dataloader'
import { Repository } from 'typeorm'
import { Project } from './project.entity'

interface RequestUser {
  req: {
    user: { id: string }
  }
}

@Injectable({ scope: Scope.REQUEST })
export class ProjectLoader {
  constructor(
    @Inject(CONTEXT) private context: RequestUser,
    @InjectRepository(Project)
    private readonly projectRepository: Repository<Project>
  ) {}

  readonly findOneById = new DataLoader<string, Project | undefined>(
    async (ids) => {
      const user = this.context.req.user // Would ideally use decorator for this instead (i.e. from createParamDecorator)
      const projects = await this.projectRepository
        .createQueryBuilder('project')
        .where('project.id IN (:...ids)', { ids })
        .andWhere('project.owner = :userId', { userId: user.id })
        .getMany()

      return ids.map((id) => projects.find((n) => n.id === id))
    }
  )
}

@incompletude
Copy link

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

This is by far the most elegant solution.

@Artimunor
Copy link
Contributor

I too have the problem when implementing the soltion marked by @kamilmysliwiec as Tutorial that I see multiple queries executed.
The only difference between the provided solution and my solution is that I am loading a related entity based on a key of the main value, I do however map them accordingly so don't see how that could be the problem.
Is there something specific to take into account to make sure they keys are batched before sent to the service function?
My solution does not provide multiple queries in one request to from the frontend to the backend either by the way, it asks for a list of entities and then loads sublists of entities which should be batched by the dataloader but are not. I am using @ResolveField for that field.

Any ideas?

@bandreetto
Copy link

bandreetto commented Nov 20, 2020

@Artimunor I was having a problem with @incompletude solution because not every field resolver in my application was using dataloaders, thus desyncing the execution and preventing the dataloaders to batch. When I applied the dataloaders to every field resolver of my graph it worked correcty batching every query on the same depth of the graph.

@johnfrades
Copy link

I came up with the following that seems to work nicely.
fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

This is by far the most elegant solution.

Thanks for this man! I've been looking on this solution, works perfectly on @ResolveField.
I am initially using nestjs-dataloader but encountering problem if there are 2 or more interceptors being applied, it's like you can only use 1 dataloader on your whole app. Thanks for this solution.

@johnfrades
Copy link

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

For some reason, the @nestjs/event-emitter is not working if it detects any injectables that has Scope.REQUEST on it (Even if that service has nothing to do with the module thats using the event emitter). That's why i temporarily remove the Scope.REQUEST on the dataloader injectables to make my event emitter work

@RAllner
Copy link

RAllner commented Feb 3, 2021

Hey guys. I also tried the solution provided by @jeppe-smith. This works fine on ResolveFields and such, but only on Queries. Could be my general misunderstanding of the whole graphQL part of NestJS but I stumbled upon it while implementing this solution. I had as well problems to implement the callBack async like described here. Just to mention that for others: The dataloaders on the ResolveFields are not getting used in case of a mutation or subscriptions.

@babymum7
Copy link

I came up with the following that seems to work nicely.

fixture.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([FixtureRepository])],
  providers: [FixtureService, FixtureLoaders, FixtureResolvers],
})
export class FixtureModule {}

fixture.loaders.ts

@Injectable({ scope: Scope.REQUEST })
export class FixtureLoaders {
  constructor(private readonly fixtureService: FixtureService) {}

  public readonly findByCode = new DataLoader<number, Fixture>(async codes => {
    try {
      const fixtures = await this.fixtureService.findByCodes(codes)

      return codes.map(code => fixtures.find(fixture => fixture.code === code))
    } catch (error) {
      throw error
    }
  })
}

fixture.resolvers.ts

@Injectable()
export class FixtureResolvers {
  constructor(private readonly fixtureLoaders: FixtureLoaders) {}

  @Query('getFixtureByCode')
  async getFixtureByCode(
    @Args('code') code: number,
  ): Promise<FixtureRO | undefined> {
    try {
      const fixture = await this.fixtureLoaders.findByCode.load(code)

      if (fixture !== undefined) {
        return fixture.toResponseObject()
      } else {
        return undefined
      }
    } catch (error) {
      throw error
    }
  }
}

This solution first seems elegant, but it quite a bad design because the dataloader is a request scope provider, so any other providers that depend on it will also become request scope. It means that the resolver provider will be recreated every incoming request. Refer to https://docs.nestjs.com/fundamentals/injection-scopes#scope-hierarchy

@kamilmysliwiec
Copy link
Member

This solution first seems elegant, but it quite a bad design because the dataloader is a request scope provider, so any other providers that depend on it will also become request scope.

Just FYI: there's nothing wrong with that. This is precisely why the "request-scoped" providers feature was added to the framework.

@knownasilya
Copy link
Contributor

knownasilya commented Apr 28, 2021

Haven't seen dataloaders for typeorm relationships like https://github.com/slaypni/type-graphql-dataloader#with-typeorm

Has anyone done anything like that? I'm assuming the above doesn't work with nestjs/graphql since it doesn't use type-graphql.

@kasvith
Copy link

kasvith commented Oct 16, 2021

I strongly believe this should be properly addressed in docs. At least some guidance regarding how to use DataLoader with NestJS(similar to Authz with Casl).

@kasvith
Copy link

kasvith commented Nov 6, 2021

For anyone who is struggling with this, I wrote an article about How I integrated Dataloader with NestJS

Hope this helps anyone else who is struggling with the same problem 😃

https://kasvith.me/posts/using-dataloader-with-nestjs/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests