Nestjs Typeorm Graphql Dataloader tutorial with Typescript

栏目: IT技术 · 发布时间: 4年前

内容简介:Before we deep dive into integrating all three into a single project. And to take advantage of GraphQL query language, and Typeorm relational database with PostgreSQL (PSQL) or MySQL or any other DB. Let’s understand the individual pieces separately.I assu
 
# All source is available here, you can either download 
# or follow the tutorial below to understand 
# each and every component individually  
 
 
https://github.com/codersera-repo/typeorm-graphql-nestjs-dataloader-starter-kit 
 
 

Before we deep dive into integrating all three into a single project. And to take advantage of GraphQL query language, and Typeorm relational database with PostgreSQL (PSQL) or MySQL or any other DB. Let’s understand the individual pieces separately.

I assume that you have basic knowledge of ORMs, Express, Node, Graphql, NestJs. In case you are missing not, here is the brief introduction

Table Of Contents

    • 1. Installing the NestJS and creating a scaffold project.
    • Install the dependencies for GraphQL, dataloader, typeorm, SQLite.
    • Creating our Migrations
    • Creating our Model entities to map our database tables.
    • TypeORM Repositories as a global module to query the database
    • Testing our first controller to see if we are able to query the database correctly
  • Integrating GraphQL with Nestjs and TypeORM
    • Adding GraphQL TypeORM Resolvers which includes our mutations and queries.
    • The problem in the current approach.
  • Introduction To Dataloader

GraphQL

GraphQL is a query language for the API. When a request (query in GraphQL world) triggers, It decides the data flows over the network.

Graphql trigger requests using a smart single endpoint unlike in traditional REST API. Where an endpoint is triggered according to the data and resource.

NestJS

NestJs is a framework used to serve our server needs. It uses Express and Fastify under the hood and has robust support for TypeScript. Which is designed and employed to make the backend structured that is in easy to maintain modules.

TypeORM

TypeORM is an Object Relational Mapping Tool that can be used with DataBase like Postgres, SQL, Mongo-DB. It supports multiple databases in the application and writing code in the modern JavaScript for our DataBase needs.

Lets us start building a basic author-books-genres program using TypeORM, NestJS, Graphql, RestAPI, Dataloader

1. Installing the NestJS and creating a scaffold project.

Either scaffold the project with the Nest CLI or clone a starter project (both will produce the same outcome).

 
npm i -g @nestjs/cli
 
 
nest new user-typeorm-graphql-dataloader
 

You can either choose yarn or npm, we are going with yarn route.

Install the dependencies for GraphQL, dataloader, typeorm, SQLite.

Once you have installed the new project, change your directory to the project we created and installed the following dependencies

 
yarn add dataloader graphql graphql-tools type-graphql typeorm graphql apollo-server-express voyager @types/graphql @nestjs/graphql sqlite3 @nestjs/typeorm
 

Create a .env file, where we will be putting our environment constants. For now, we will be using this file to populate the typeorm configuration and port where to run our servers. We are using SQLite for this tutorial purpose, but you can use any SQL database, typeorm supports multiple drivers

# .env 
 
TYPEORM_CONNECTION = sqlite
TYPEORM_DATABASE = data/dev.db
TYPEORM_LOGGING = true
TYPEORM_ENTITIES = src/db/models/*.entity.ts
TYPEORM_MIGRATIONS = src/db/migrations/*.ts
TYPEORM_MIGRATIONS_RUN = src/db/migrations
TYPEORM_ENTITIES_DIR = src/db/models
TYPEORM_MIGRATIONS_DIR = src/db/migrations
EXPRESS_PORT = 3000
 

Next, we need to create some directories, where our typeorm entities, typeorm migrations, and SQLite database are gonna resides

 
mkdir -p src/db/models  # our entites here
mkdir -p src/db/migrations # our migrations here
mkdir -p data # here our sqlite.db
 

Creating our Migrations

We gonna use typeorm CLI to generate our migrations, luckily typeorm migrations CLI come intact with typeorm package

  1. Author: We gonna create Author here
  2. Book: We gonna create Book here
  3. Genres: We gonna create Genres here
  4. BookGeneres: We gonna create Many-Many mapping between books and genres

That’s all the migration we need, below is the command to create typeorm migration since .env file already knows where to create migrations that we have already provided above.

 
 ts-node ./node_modules/typeorm/cli.js migration:create -n CreateAuthor    
 ts-node ./node_modules/typeorm/cli.js migration:create -n CreateBook 
 ts-node ./node_modules/typeorm/cli.js migration:create -n CreateGenre   
 ts-node ./node_modules/typeorm/cli.js migration:create -n CreateBookGenre 
 

The above commands should create 4 files in your src/db/migrations directory, here is how a single migration would look like.

# src/db/migrations/1563360242539-CreateAuthor.ts 
 
import {MigrationInterface, QueryRunner} from "typeorm";
 
export class CreateAuthor1563360242539 implements MigrationInterface {
 
    public async up(queryRunner: QueryRunner): Promise<any> {
    }
 
    public async down(queryRunner: QueryRunner): Promise<any> {
    }
 
}
 

Typeorm uses epoch time as the prefix for migrations to run migrations in order. You can populate your migrations now with the creation of the table, here is how your migration should look like.

 
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
 
export class CreateAuthor1563360242539 implements MigrationInterface {
 
    private authorTable = new Table({
        name: 'authors',
        columns: [
            {
                name: 'id',
                type: 'INTEGER',
                isPrimary: true,
                isGenerated: true,
                generationStrategy: 'increment',
            },
            {
                name: 'name',
                type: 'varchar',
                length: '255',
                isNullable: false,
            },
            {
                name: 'created_at',
                type: 'timestamptz',
                isNullable: false,
                default: 'now()',
            },
            {
                name: 'updated_at',
                type: 'timestamptz',
                isNullable: false,
                default: 'now()',
            }],
    });
 
    public async up(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.createTable(this.authorTable);
    }
 
    public async down(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.dropTable(this.authorTable);
    }
 
}
 
 
 
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
 
export class CreateBook1563360267250 implements MigrationInterface {
 
    private bookTable = new Table({
        name: 'books',
        columns: [
            {
                name: 'id',
                type: 'INTEGER',
                isPrimary: true,
                isUnique: true,
                isGenerated: true,
                generationStrategy: 'increment',
            },
            {
                name: 'title',
                type: 'varchar',
                length: '255',
                isNullable: false,
            },
            {
                name: 'author_id',
                type: 'INTEGER',
                isNullable: false,
            },
            {
                name: 'created_at',
                type: 'timestamptz',
                isPrimary: false,
                isNullable: false,
                default: 'now()',
            },
            {
                name: 'updated_at',
                type: 'timestamptz',
                isPrimary: false,
                isNullable: false,
                default: 'now()',
            }],
    });
 
    private foreignKey = new TableForeignKey({
        columnNames: ['author_id'],
        referencedColumnNames: ['id'],
        onDelete: 'CASCADE',
        referencedTableName: 'authors',
    });
 
    public async up(queryRunner: QueryRunner): Promise<any> {
      await queryRunner.createTable(this.bookTable);
      await queryRunner.createForeignKey('books', this.foreignKey);
    }
 
    public async down(queryRunner: QueryRunner): Promise<any> {
      await queryRunner.dropTable(this.bookTable);
    }
 
}
 
 
 
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
 
export class CreateGenre1563360272082 implements MigrationInterface {
 
    private genreTable = new Table({
        name: 'genres',
        columns: [
            {
                name: 'id',
                type: 'INTEGER',
                isPrimary: true,
                isUnique: true,
                isGenerated: true,
                generationStrategy: 'increment',
            },
            {
                name: 'genre_name',
                type: 'varchar',
                length: '255',
                isNullable: false,
            },
            {
                name: 'created_at',
                type: 'timestamptz',
                isPrimary: false,
                isNullable: false,
                default: 'now()',
            },
            {
                name: 'updated_at',
                type: 'timestamptz',
                isPrimary: false,
                isNullable: false,
                default: 'now()',
            }],
    });
 
 
    public async up(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.createTable(this.genreTable);
    }
 
    public async down(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.dropTable(this.genreTable);
    }
 
}
 
 
 
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
 
export class CreateBookGenre1563360276498 implements MigrationInterface {
 
    private genreBookTable = new Table({
        name: 'books_genres',
        columns: [
            {
                name: 'id',
                type: 'INTEGER',
                isPrimary: true,
                isUnique: true,
                isGenerated: true,
                generationStrategy: 'increment',
            },
            {
                name: 'book_id',
                type: 'INTEGER',
                isNullable: true,
            },
            {
                name: 'genre_id',
                type: 'INTEGER',
                isNullable: true,
            },
            {
                name: 'created_at',
                type: 'timestamptz',
                isPrimary: false,
                isNullable: false,
                default: 'now()',
            },
            {
                name: 'updated_at',
                type: 'timestamptz',
                isPrimary: false,
                isNullable: false,
                default: 'now()',
            }],
    });
 
    public async up(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.createTable(this.genreBookTable);
    }
 
    public async down(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.dropTable(this.genreBookTable);
    }
 
}
 
 

Once migrations are setup, you need to run Typeorm migrations now, Typeorm provides a very easy to use CLI to run all the migrations, it will create a migrations table in the database, where it will keep a record of all the migrations it has applied.

 
ts-node ./node_modules/typeorm/cli.js migration:run
 

Once you execute this command dev.db file in created in src/data folder, Use SQLite browser to view the database, it must have all the tables you created and also the migration table

Creating our Model entities to map our database tables.

Let’s create Entity now, we are going to use typeorm data mapper method for our models, However, if you want to use typeorm active records, then you just need to extend all your Models from BaseEntity class which can be imported from like

 
import { BaseEntity } from 'typeorm';
class Auther extends BaseEntity
......
 
# src/db/models/author.entity.ts
 
import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import Book from './book.entity';
 
@Entity()
export default class Author {
 
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column()
  name: string;
 
  @CreateDateColumn({name: 'created_at'})
  createdAt: Date;
 
  @UpdateDateColumn({name: 'updated_at'})
  updatedAt: Date;
 
  // Associations
  @OneToMany(() => Book, book => book.authorConnection)
  bookConnection: Promise<Book[]>;
 
}
 
 
 
import {
  Entity,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  Column, OneToMany,
  JoinColumn,
  ManyToOne,
} from 'typeorm';
import BookGenre from './book-genre.entity';
import Author from './author.entity';
import { Field, ObjectType } from 'type-graphql';
 
@ObjectType()
@Entity({name: 'books'})
export default class Book {
 
  @Field()
  @PrimaryGeneratedColumn()
  id: number;
 
  @Field()
  @Column()
  title: string;
 
  @Field()
  @Column({name: 'author_id'})
  authorId: number;
 
  @Field()
  @CreateDateColumn({name: 'created_at'})
  createdAt: Date;
 
  @Field()
  @UpdateDateColumn({name: 'updated_at'})
  updatedAt: Date;
 
  @Field(() => Author)
  author: Author;
 
  // Associations
 
  @ManyToOne(() => Author, author => author.bookConnection, {primary:
      true})
  @JoinColumn({name: 'author_id'})
  authorConnection: Promise<Author>;
 
  @OneToMany(() => BookGenre, bookGenre => bookGenre.genre)
  genreConnection: Promise<BookGenre[]>;
}
 
 
 
# src/db/models/genre.entity.ts
 
import {
  Entity,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany, PrimaryGeneratedColumn,
} from 'typeorm';
import BookGenre from './book-genre.entity';
 
@Entity()
export default class Genre {
 
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column({name: 'genre_name'})
  name: string;
 
  @CreateDateColumn({name: 'created_at'})
  createdAt: Date;
 
  @UpdateDateColumn({name: 'updated_at'})
  updatedAt: Date;
 
  // Associations
  @OneToMany(() => BookGenre, bookGenre => bookGenre.book)
  bookConnection: Promise<BookGenre[]>;
}
 
# src/db/models/book-genre.entity.ts
 
 
import {
  Entity,
  PrimaryColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne, JoinColumn, PrimaryGeneratedColumn,
} from 'typeorm';
import Genre from './genre.entity';
import Book from './book.entity';
 
@Entity()
export default class BookGenre {
 
  @PrimaryGeneratedColumn()
  id: number;
 
  @PrimaryColumn({name: 'book_id'})
  bookId: number;
 
  @PrimaryColumn({name: 'genre_id'})
  genreId: number;
 
  @CreateDateColumn({name: 'created_at'})
  createdAt: Date;
 
  @UpdateDateColumn({name: 'updated_at'})
  updatedAt: Date;
 
  // Associations
  @ManyToOne(() => Book, book => book.genreConnection, {primary:
      true})
  @JoinColumn({name: 'book_id'})
  book: Book[];
 
  @ManyToOne(() => Genre,  genre => genre.bookConnection, {primary:
      true})
  @JoinColumn({name: 'genre_id'})
  genre: Genre[];
}
 

That’s All. Once you have all the entities setup, you are ready to CRUD records in the database using repositories and map to the above entity models.

TypeORM Repositories as a global module to query the database

Since we took the data mapper route, we need to define repositories for each of our entities, so we are going to create a global service repo.service.ts which can be accessed across the nest application to query any table or entity. Before that, we have to configure TypeORM in the main module, which in our case is app.module.ts.

src/app.module.ts
 
@Module({
 
  imports: [
     TypeOrmModule.forRoot(), 
     RepoModule   // Don't worry, we will create this next
  ],  // to use typeorm
  controllers: [AppController],  
  providers: [AppService],
})
export class AppModule {}
 

Next create two files, repo.service.ts and repo.module.ts

# src/repo.service.ts
 
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import Author from './db/models/author.entity';
import Book from './db/models/book.entity';
import Genre from './db/models/genre.entity';
import BookGenre from './db/models/book-genre.entity';
 
@Injectable()
class RepoService {
  public constructor(
    @InjectRepository(Author) public readonly authorRepo: Repository<Author>,
    @InjectRepository(Book) public readonly bookRepo: Repository<Book>,
    @InjectRepository(Genre) public readonly genreRepo: Repository<Genre>,
    @InjectRepository(BookGenre) public readonly bookGenreRepo: Repository<BookGenre>,
  ) {}
}
 
export default RepoService;
# src/repo.module.ts
 
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import RepoService from './repo.service';
import Author from './db/models/author.entity';
import Book from './db/models/book.entity';
import Genre from './db/models/genre.entity';
import BookGenre from './db/models/book-genre.entity';
 
@Global()
@Module({
  imports: [
    TypeOrmModule.forFeature([
      Author,
      Book,
      Genre,
      BookGenre,
    ]),
  ],
  providers: [RepoService],
  exports: [RepoService],
})
class RepoModule {
 
}
export default RepoModule;
 
 

Wow! We have just completed our setup for Typeorm, using data mapper and we have created migrations and models using typeorm one too many and typeorm many to many relationships.

Testing our first controller to see if we are able to query the database correctly

Open the app.service.ts file, we gonna use the existing function generated by nestjs CLI scaffolding to test our structure.

# src/app.service.ts
 
import { Injectable } from '@nestjs/common';
import RepoService from './repo.service';
 
@Injectable()
export class AppService {
 
  constructor(private readonly repoService: RepoService) {
 
  }
 
  async getHello(): Promise<string> { // querying database
    return `Total books are ${await this.repoService.bookRepo.count()}`;
  }
}
 
# src/app.controller.ts
......
  @Get()
  async getHello(): Promise<string> {
    return this.appService.getHello();
  }
.......
 

Once you have made changes to both app.service and app.controller file, you can run your project by ts-node src/main.ts. Once it runs, open the browser and you can see the following line

Total books are  

Hurray! Your project is able to query the database. But that’s not all, We are going to employ the graphql integration into this project.

Integrating GraphQL with Nestjs and TypeORM

Since we already have installed the required packages above, in case you missed it, install the following package for graphql nestjs typeorm

 
yarn add type-graphql graphql dataloader @nestjs/graphql apollo-server-express
yarn add --dev @types/graphql 
 

Once the above packages are installed, we will modify our entities with type-graphql decorators, so the GraphQL types corresponding to entities are created inside our GraphQL schemas.

To create a type corresponding to the entity we will use @ObjectType() decorator from the type-graphql package. So you entities would look something like this

# src/db/models/author.entity.ts
 
......
@ObjectType()
@Entity({name: 'authors'})
export default class Author {
 
  @Field()
  @PrimaryGeneratedColumn()
  id: number;
.......
 

Once the type is exposed to the schema, we will start exposing our fields for the type using the @Field() decorator again from the type-graphql package.

Convert all your typeorm entities into GraphQL schemas.

Remember don’t annotate your associations with @Field(), we are going to deal with them separately.

Next, we have to import the GraphQL module in our app.module.ts so that NestJS will know about it. Change your app.module.ts file

......
import { GraphQLModule } from '@nestjs/graphql';
 
@Module({
 
  imports: [TypeOrmModule.forRoot(),
     RepoModule,
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      playground: true,
    }),
  ],
........

GraphQL will generate the schema at the schema.gql file, in your root directory. You don’t have to worry about this file as this is maintained and updated by NestJS graphql as per the Schema, resolvers, and mutations.

Adding GraphQL TypeORM Resolvers which includes our mutations and queries.

Create a new directory inside src where resolvers for all the typeorm entities would be there

 
mkdir -p src/resolvers
 

In our resolvers directory, we will create our first resolver author.resolver.ts for the author entity.

In our resolver directory, we will create a directory to hold our graphql input types.

 
mkdir -p src/resolvers/input
 

Since we are working on the author resolver first. We will begin by creating the author.input.ts which will hold the input types for the author entity.

The first input type will be the one required for our create author mutation responsible for creating a new author record in our database.

 
#src/resolvers/input/author.input.ts
 
import { Field, InputType } from 'type-graphql';
 
@InputType()
class AuthorInput {
  @Field()
  readonly name: string;
}
 
export default AuthorInput;
 
 

As we have set up input for our resolver, we will start with our first resolver. Each resolver class is annotated with @Resolver decorator which is imported from the nestjs-graphql package (@nestjs/graphql).

 
#src/resolvers/author.resolver.ts
 
import { Resolver } from '@nestjs/graphql';
 
@Resolver()
class AuthorResolver {
}
 
 

After we have created the class for the resolver, we will add the repo service as a dependency in the constructor. As our RepoService is Global, so no need to worry about providing this service at the module level.

 
#src/resolvers/author.resolver.ts
 
import { Resolver } from '@nestjs/graphql';
 
@Resolver()
class AuthorResolver {
   constructor(private readonly repoService: RepoService) {}
}
 

To create a query or mutation the class field should be annotated with a @Query() or @Mutation resolver respectively again import from NestJS-GraphQL package ( @nestjs/graphql ).

 
#src/resolvers/author.resolver.ts
 
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 
@Resolver()
class AuthorResolver {
   constructor(private readonly repoService: RepoService) {}
 
  @Query(() => [Author])
  public async authors(): Promise<Author[]> {
    return this.repoService.authorRepo.find();
  }
  @Query(() => Author, {nullable: true})
  public async author(@Args('id') id: number): Promise<Author> {
    return  this.repoService.authorRepo.findOne(id);
  }
 
  @Mutation(() => Author)
  public async createAuthor(@Args('data') input: AuthorInput): 
    Promise<Author> {
      const author = this.repoService.authorRepo.create({name: 
      input.name});
      return  this.repoService.authorRepo.save(author);
  }
}
export default AuthorResolver;
 

To create the above the mutations and queries in our GraphQL schema. We have to add all the resolvers to our main module which in this case is app.module.ts imports. To keep things simple we will create a separate array of resolvers, and use the ES6 spread operator to import it.

#src/app.module.ts
 
........
const graphQLImports = [
  AuthorResolver,
];
 
@Module({
  imports: [TypeOrmModule.forRoot(),
    RepoModule,
    ...graphQLImports,
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      playground: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
 
 

The above resolver was an easy part, as it did not involve any associations to deal with. Though at the database level, the associations are handled by the typeorm. Each associative property needs to be resolved for the GraphQL.

Now we will begin writing out resolver for the Book entity. The input type for the Book will have two options. Either create the book for an existing author, by his id. Or create a new book and a new author for the same

  1. To connect to an existing author in the database by his ID
  2. To create a new author along with the book.

The input type for the book is shown below with the above functionality implemented.

 
#src/resolvers/input/book.input.ts
 
import { Field, InputType } from 'type-graphql';
import AuthorInput from './author.input';
 
@InputType()
class BookAuthorConnectInput {
  @Field()
  readonly id: number;
}
 
@InputType()
class BookAuthorInput {
  @Field({nullable: true})
  readonly connect: BookAuthorConnectInput;
 
  @Field({nullable: true})
  readonly create: AuthorInput;
}
 
@InputType()
class BookInput {
  @Field()
  readonly title: string;
 
  @Field()
  readonly author: BookAuthorInput;
}
 
export default BookInput;
 
 

According to the associations defined in our typeorm migrations and typeorm entities. Each book must have one author. And from our GraphQL playground while fetching records for books the author record for the book can also be fetched. To fetch the author record we need to resolve the property. And to do so we will use @ResolveProperty() decorator from the @nestjs/graphql package.

For using the @ResolveProperty() you must pass the entity to the @Resolve(), decorator.

The @ResolveProperty() will expect the property to resolve as a parameter. Or the corresponding function name should match the name of the property to resolve. And GraphQL parent objects to its corresponding function.

 
#src/resolver/book.resolver.ts
......
  @ResolveProperty()
  public async author(@Parent() parent): Promise<Author> {
    return this.repoService.authorRepo.findOne(parent.authorId);
  }
......
 

Now we will begin with the final block of our application the Genre resolver. Below is the input type for the Genre and GenreBook

#src/resolvers/input/genre.input.ts
 
import { Field, InputType } from 'type-graphql';
 
@InputType()
class GenreInput {
  @Field()
  readonly name: string;
}
export default GenreInput;
 
 
 
#src/resolvers/input/book-genre.input.ts
import { Field, InputType } from 'type-graphql';
 
@InputType()
class GenreBookInput {
  @Field()
  readonly genreId: number;
  @Field()
  readonly bookId: number;
}
 
export default GenreBookInput;
 

Now that is finished with the input types for the Genre and BookGenre, we will create resolver for the same.

#src/resolvers/genre.resolver.ts
 
import { Args, Mutation, Parent, Query, ResolveProperty, Resolver } from '@nestjs/graphql';
import RepoService from '../repo.service';
import Author from '../db/models/author.entity';
import Book from '../db/models/book.entity';
import BookInput from './input/book.input';
import Genre from '../db/models/genre.entity';
import GenreInput from './input/genre.input';
import BookGenre from '../db/models/book-genre.entity';
 
@Resolver(Genre)
class GenreResolver {
 
  constructor(private readonly repoService: RepoService) {}
  @Query(() => [Genre])
  public async genres(): Promise<Genre[]> {
    return this.repoService.genreRepo.find();
  }
  @Query(() => Genre, {nullable: true})
  public async genre(@Args('id') id: number): Promise<Genre> {
    return this.repoService.genreRepo.findOne(id);
  }
 
  @Mutation(() => Genre)
  public async createGenre(@Args('data') input: GenreInput): Promise<Genre> {
    const genre = new Genre();
    genre.name = input.name;
    return this.repoService.genreRepo.save(genre);
  }
 
  @ResolveProperty()
  public async book(@Parent() parent): Promise<Book[]> {
    const bookGenres = await this.repoService.bookGenreRepo.find({where: 
    {genreId: parent.id}, relations: ['book']});
    const books: Book[] = [];
    bookGenres.forEach(async bookGenre => books.push(await 
      bookGenre.book));
    return books;
  }
}
 
export default GenreResolver;
 
#src/resovlers/book-genre.resolver.ts
 
import { Args, Mutation, Parent, Query, ResolveProperty, Resolver } from '@nestjs/graphql';
import RepoService from '../repo.service';
import Author from '../db/models/author.entity';
import Book from '../db/models/book.entity';
import BookInput from './input/book.input';
import Genre from '../db/models/genre.entity';
import GenreInput from './input/genre.input';
import BookGenre from '../db/models/book-genre.entity';
import BookGenreInput from './input/book-genre.input';
import { Arg } from 'type-graphql';
 
@Resolver()
class BookGenreResolver {
 
  constructor(private readonly repoService: RepoService) {}
  @Mutation(() => BookGenre)
  public async createBookGenre(@Args('data') input: BookGenreInput): Promise<BookGenre> {
    const bookGenre = new BookGenre();
    const {bookId, genreId} = input;
    bookGenre.bookId = bookId;
    bookGenre.genreId = genreId;
    return this.repoService.bookGenreRepo.save(bookGenre);
  }
 
  @Query(() => [BookGenre])
  public async bookGenres(): Promise<BookGenre[]> {
    return this.repoService.bookGenreRepo.find();
  }
 
  @Query(() => BookGenre)
  public async bookGenre(@Arg('id') id: number): Promise<BookGenre> {
    return this.repoService.bookGenreRepo.findOne(id);
  }
}
 
export default BookGenreResolver;
 

The queries and mutations created can be run on the GraphQL playground, you can navigate to GraphQL playground on the route “/graphql”

Nestjs Typeorm Graphql Dataloader tutorial with Typescript
The GraphQL playground.

The queries and mutations to run can be written on the left side of the graphql playground whose result is displayed on the right side after clicking the play button.

For your reference, below are some graphql queries and mutations with their results fetched from the playground.

 
// mutation to create an author
mutation {
  createAuthor(data: {
    name: "Sahil"
  }) {
    id
    name
    createdAt
    updatedAt
  }
}
Nestjs Typeorm Graphql Dataloader tutorial with Typescript
Create Author GraphQL Mutation
Nestjs Typeorm Graphql Dataloader tutorial with Typescript
Authors GraphQL Query.

The problem in the current approach.

The problem is not an obvious problem i.e our code is not going to blow up everything is going to work, the problem is an intuitive problem, using the dataloader we can make our application much more efficient. To illustrate the current problem we will fetch the books record using our genres query.

  • Nestjs Typeorm Graphql Dataloader tutorial with Typescript
    Fetching genres and their books.

In the above query whenever the book is fetched it triggers the @ResolveProperty() method and the book records are fetched. i.e for n Genres n database queries will run and a total of n+1 queries will be executed. The database connections are the most expensive task we have. To resolve this problem we will use dataloaders.

Introduction To Dataloader

The Dataloader is a generic utility developed by facebook used to abstract request batching and caching.

The dataloader will wait for a single event loop cycle before it executes. And it calls back function and by the time an event loop cycle is completed, all the Book ids for the Book records to be fetched will have arrived. And instead of running n queries for n Genres we will run a single query. Which is a huge improvement over the previous approach

To use dataloader we need the dataloader package. Which in case you haven’t installed already can be installed by the command mentioned below.

 
yarn add dataloader
 

Now that our package is installed we will make the directory where our packages will sit.

 
mkdir -p src/db/loaders

Now that our directory for the resolver is created. We will write our loader to fetch Books based on the Genre Ids passed.

#src/db/loaders/books.loader.ts
 
import DataLoader = require('dataloader');
import Book from '../models/book.entity';
import { getRepository } from 'typeorm';
import BookGenre from '../models/book-genre.entity';
 
const batchBooks = async (genreIds: string[]) => {
  const bookGenres = await getRepository(BookGenre)
    .createQueryBuilder('bookGenres')
    .leftJoinAndSelect('bookGenres.book', 'book')
    .where('bookGenres.id IN(:...ids)', {ids: genreIds})
    .getMany();
  const genreIdToBooks: {[key: string]: Book[]} = {};
  bookGenres.forEach(bookGenre => {
    if (!genreIdToBooks[bookGenre.genreId]) {
      genreIdToBooks[bookGenre.genreId] = [(bookGenre as any).__book__];
    } else {
      genreIdToBooks[bookGenre.genreId].push((bookGenre as any).__book__);
    }
  });
  return genreIds.map(genreId => genreIdToBooks[genreId]);
};
const genreBooksLoader = () => new DataLoader(batchBooks);
 
export {
  genreBooksLoader,
};
 

The loaders are passed to each of our queries and mutations as a part of the context. Therefore, we will now begin writing our GraphQL context type.

 
mkdir -p src/types/
 
#src/types/graphql.types.ts
 
import { genreBooksLoader } from '../db/loaders/books.loader';
 
export interface IGraphQLContext {
  genreBooksLoader: ReturnType<typeof genreBooksLoader>;
}

Once the types for the GraphQL context created, we will create a GraphQL context. The context gets created in our GraphQL module present in the main app.module.ts

#src/app.module.ts
 
......
@Module({
 
  imports: [TypeOrmModule.forRoot(),
    RepoModule,
    ...graphQLImports,
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      playground: true,
      context: {
        genreBooksLoader: genreBooksLoader(),
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
......

Now that the loader is available to each of our queries and mutation via context. We can use it and modify our book to resolve property in our Genre resolver.

#src/resolvers/genre.resolver.ts
 
........
  @ResolveProperty()
  public async book(@Parent() parent, @Context() {genreBooksLoader}: 
  IGraphQLContext): Promise<Book[]> {
    return genreBooksLoader.load(parent.id);
  }
 
........

The author’s property is now resolve using the dataloader function exposed in our GraphQL context. And we have resolved the n+1 problem instead of running n database queries. The records will now be fetched using a single query. Thanks to Dataloader

How useful was this post?

Click on a star to rate it!

Post Views: 9,903


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

零售的哲学:7-Eleven便利店创始人自述

零售的哲学:7-Eleven便利店创始人自述

[日] 铃木敏文 / 顾晓琳 / 江苏文艺出版社 / 2014-12-1 / 36

全球最大的便利店连锁公司创始人——铃木敏文,结合40多年零售经验,为你讲述击中消费心理的零售哲学。铃木敏文的很多创新,现在已经成为商界常识,本书把那些不可思议的零售创新娓娓道来。关于零售的一切:选址、订货、销售、物流、管理……他一次又一次地在一片反对声中创造出零售界的新纪录。 翻开本书,看铃木敏文如何领导7-11冲破层层阻碍,成为世界第一的零售哲学。一起来看看 《零售的哲学:7-Eleven便利店创始人自述》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具