Closed21

Nest.jsとTypeORMを試す

shuntakashuntaka

プロジェクト作成。ここでパッケージマネージャーは選択可能。パッケージマネージャーは、yarnを指定。

$ nest new nest-sandbox
⚡  We will scaffold your app in a few seconds..

CREATE nest-sandbox/.eslintrc.js (665 bytes)
CREATE nest-sandbox/.prettierrc (51 bytes)
CREATE nest-sandbox/README.md (3340 bytes)
CREATE nest-sandbox/nest-cli.json (118 bytes)
CREATE nest-sandbox/package.json (1997 bytes)
CREATE nest-sandbox/tsconfig.build.json (97 bytes)
CREATE nest-sandbox/tsconfig.json (546 bytes)
CREATE nest-sandbox/src/app.controller.spec.ts (617 bytes)
CREATE nest-sandbox/src/app.controller.ts (274 bytes)
CREATE nest-sandbox/src/app.module.ts (249 bytes)
CREATE nest-sandbox/src/app.service.ts (142 bytes)
CREATE nest-sandbox/src/main.ts (208 bytes)
CREATE nest-sandbox/test/app.e2e-spec.ts (630 bytes)
CREATE nest-sandbox/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? yarn
✔ Installation in progress... ☕

🚀  Successfully created project nest-sandbox
👉  Get started with the following commands:

$ cd nest-sandbox
$ yarn run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest

shuntakashuntaka
$ yarn start
yarn run v1.22.19
$ nest start
[Nest] 50844  - 2022/10/17 19:43:27     LOG [NestFactory] Starting Nest application...
[Nest] 50844  - 2022/10/17 19:43:27     LOG [InstanceLoader] AppModule dependencies initialized +16ms
[Nest] 50844  - 2022/10/17 19:43:27     LOG [RoutesResolver] AppController {/}: +1ms
[Nest] 50844  - 2022/10/17 19:43:27     LOG [RouterExplorer] Mapped {/, GET} route +1ms
[Nest] 50844  - 2022/10/17 19:43:27     LOG [NestApplication] Nest application successfully started +1ms
$ curl localhost:3000
Hello World!
shuntakashuntaka

GraphQLにしたいので、この記事を参考にする

https://zenn.dev/nori_k/articles/2834ca2f339a71

↑のリンクで公式のがあった

https://docs.nestjs.com/graphql/quick-start

3つのパターンがあるみたい。

  • ExpressとAppollo(Default)
    • npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express
  • FastifyとApollo
    • npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-fastify
  • FastifyとMercurius
    • npm i @nestjs/graphql @nestjs/mercurius graphql mercurius fastify
shuntakashuntaka

Fastifyはベンチマークで早いっぽいけど、nestとFastify組み合わせるパターンだとmercuriusを使うのかぁ
appllo使いたいし、ExpressとFastifyの差は体感できな気がするし、デフォルトのExpressとAppolloパターンを使うかなぁ、、

shuntakashuntaka

依存を追加

yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express

ここからはこの記事を通り進める。気になったことをメモしていく。
https://zenn.dev/azukiazusa/articles/e84be9735d357e

モジュール

$ nest generate module books
CREATE src/books/books.module.ts (82 bytes)
UPDATE src/app.module.ts (496 bytes)

モデル

$ nest generate class books/book
CREATE src/books/book.spec.ts (139 bytes)
CREATE src/books/book.ts (21 bytes)

src/books/book.tsは参照記事の通りに変更

リゾルバ追加時に、books.module.ts側に変更が入る

$ nest generate resolver books
CREATE src/books/books.resolver.spec.ts (463 bytes)
CREATE src/books/books.resolver.ts (87 bytes)
UPDATE src/books/books.module.ts (162 bytes)
src/books/books.module.ts
 import { Module } from '@nestjs/common';
+import { BooksResolver } from './books.resolver';

-@Module({})
+@Module({
+  providers: [BooksResolver],
+})
 export class BooksModule {}

リゾルバの実装は一旦飛ばして、以下の項目をやる

import { NotFoundException } from '@nestjs/common';
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Book } from './book';
import { BooksService } from './books.service';
import { NewBookInput } from './dto/newBook.input';

@Resolver((of) => Book)
export class BooksResolver {
  constructor(private booksService: BooksService) {}

  @Query(() => [Book])
  books(): Promise<Book[]> {
    return this.booksService.finedAll();
  }

  @Query((returns) => Book)
  async getBook(@Args({ name: 'id', type: () => Int }) id: number) {
    const book = await this.booksService.findOneById(id);
    if (!book) {
      throw new NotFoundException(id);
    }
    return book;
  }

  @Mutation((returns) => Book)
  addBook(@Args('newBook') newBook: NewBookInput): Promise<Book> {
    return this.booksService.create(newBook);
  }

  @Mutation((returns) => Boolean)
  async removeBook(@Args({ name: 'id', type: () => Int }) id: number) {
    return this.booksService.remove(id);
  }
}

全て記事通りだが、ここでエラーが発生

エラー内容
$ yarn start
yarn run v1.22.19
$ nest start
[Nest] 41696  - 2022/10/18 7:18:17   ERROR [GraphQLModule] Missing "driver" option. In the latest version of "@nestjs/graphql" package (v10) a new required configuration property called "driver" has been introduced. Check out the official documentation for more details on how to migrate (https://docs.nestjs.com/graphql/migration-guide). Example:

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
})

/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/@nestjs/graphql/dist/graphql.module.js:121
            throw new Error(errorMessage);
                  ^
Error: Missing "driver" option. In the latest version of "@nestjs/graphql" package (v10) a new required configuration property called "driver" has been introduced. Check out the official documentation for more details on how to migrate (https://docs.nestjs.com/graphql/migration-guide). Example:

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
})
    at Function.assertDriver (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/@nestjs/graphql/dist/graphql.module.js:121:19)
    at Function.forRoot (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/@nestjs/graphql/dist/graphql.module.js:36:14)
    at Object.<anonymous> (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/app.module.ts:10:19)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/main.ts:3:1)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

driverを入れれば解決

src/app.module.ts
 import { Module } from '@nestjs/common';
-import { AppController } from './app.controller';
-import { AppService } from './app.service';
+import { GraphQLModule } from '@nestjs/graphql';
+import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

 @Module({
-  imports: [],
-  controllers: [AppController],
-  providers: [AppService],
+  imports: [
+    GraphQLModule.forRoot<ApolloDriverConfig>({
+      driver: ApolloDriver,
+    }),
+  ],
 })
 export class AppModule {}
shuntakashuntaka
nest startするとsrc/schema.gqlが生成される
src/schema.gql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------

type Book {
  id: ID!
  title: String!
  author: String!
  price: Int!
  createdAt: DateTime!
}

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

type Query {
  books: [Book!]!
  getBook(id: Int!): Book!
}

type Mutation {
  addBook(newBook: NewBookInput!): Book!
  removeBook(id: Int!): Boolean!
}

input NewBookInput {
  title: String!
  price: Int!
  author: [String!]!
}

shuntakashuntaka

TypeORMの導入、引き続き以下の記事を参考に進めていく
https://zenn.dev/azukiazusa/articles/e84be9735d357e#typeormの導入

yarn add -D @nestjs/typeorm typeorm mysql2
$ brew install mysql
$ mysql --version
mysql  Ver 8.0.31 for macos12.6 on arm64 (Homebrew)
$ mysql.server start --skip-grant-tables
Starting MySQL
.. SUCCESS!
$ mysql -uroot
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.31 Homebrew

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

データベース作成と接続は記事通り。

生地と違う点は以下。typeormのバージョン違いによるものだと推測。^0.2.31は-^0.3.10は+

差分を表示
import { Injectable } from '@nestjs/common';
import { Book } from './book';
import { NewBookInput } from './dto/newBook.input';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class BooksService {
  constructor(
    @InjectRepository(Book)
    private booksRepository: Repository<Book>,
  ) {}

  finedAll(): Promise<Book[]> {
    return this.booksRepository.find();
  }

  findOneById(id: number): Promise<Book> {
-     return this.booksRepostiory.findOne(id);
+    return this.booksRepository.findOne({ where: { id: id } });
  }

  async create(data: NewBookInput): Promise<Book> {
    const book = this.booksRepository.create(data);
    await this.booksRepository.save(book);

    return book;
  }

  async remove(id: number): Promise<boolean> {
    const result = await this.booksRepository.delete(id);
    return result.affected > 0;
  }
}
shuntakashuntaka
MySQLに接続できない問題
$ yarn start                                                                                                                                                                                                                                                                                                            
$ nest start
[Nest] 75965  - 2022/10/18 17:01:33     LOG [NestFactory] Starting Nest application...
[Nest] 75965  - 2022/10/18 17:01:33     LOG [InstanceLoader] TypeOrmModule dependencies initialized +41ms
[Nest] 75965  - 2022/10/18 17:01:33     LOG [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 75965  - 2022/10/18 17:01:33     LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +1ms
[Nest] 75965  - 2022/10/18 17:01:33     LOG [InstanceLoader] GraphQLModule dependencies initialized +0ms
[Nest] 75965  - 2022/10/18 17:01:33   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
Error: connect ECONNREFUSED 127.0.0.1:3306
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1161:16)
[Nest] 75965  - 2022/10/18 17:01:36   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (2)...
Error: connect ECONNREFUSED 127.0.0.1:3306
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1161:16)
[Nest] 75965  - 2022/10/18 17:01:39   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (3)...
Error: connect ECONNREFUSED 127.0.0.1:3306
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1161:16)
[Nest] 75965  - 2022/10/18 17:01:42   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (4)...
Error: connect ECONNREFUSED 127.0.0.1:3306
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1161:16)
[Nest] 75965  - 2022/10/18 17:01:45   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (5)...

MySQLでは、127.0.0.1とlocalhostは別物。ただNest.js側のコンフィグにlocalhostを指定しても127.0.0.1に変換されてしまう。コンテナ経由でMySQL立てる方針へ変更。

mysqlサーバーの停止

mysql.server stop
MySQLコンテナ準備
_tools/mysql/conf.d/mysql.cnf
[mysql]
default_character_set=utf8mb4
_tools/mysql/conf.d/mysqld.cnf
[mysqld]
default-authentication-plugin=mysql_native_password
character_set_server=utf8mb4
sql_mode=TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY
general_log=1
log_output=TABLE
slow_query_log=1
long_query_time=2
autocommit=0
docker-compose.yml
version: "3.9"
services:
  nest-db:
    image: mysql:8.0.29
    platform: linux/amd64
    container_name: nest-db
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
      MYSQL_USER: nest
      MYSQL_PASSWORD: nest
      MYSQL_DATABASE: nest_sample_app
    volumes:
      - nest-db-data:/var/lib/mysql
      - $PWD/_tools/mysql/conf.d:/etc/mysql/conf.d:cached
    ports:
      - "3306:3306"
volumes:
  nest-db-data:
$ mysql -h 127.0.0.1 -u nest -pnest
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.29 MySQL Community Server - GPL

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| nest_sample_app    |
+--------------------+
2 rows in set (0.02 sec)

起動した 🎉

$ yarn start
yarn run v1.22.19
$ nest start
[Nest] 12544  - 2022/10/18 19:01:42     LOG [NestFactory] Starting Nest application...
[Nest] 12544  - 2022/10/18 19:01:42     LOG [InstanceLoader] TypeOrmModule dependencies initialized +52ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +0ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [InstanceLoader] GraphQLModule dependencies initialized +0ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +172ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [InstanceLoader] BooksModule dependencies initialized +1ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [RoutesResolver] AppController {/}: +10ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [RouterExplorer] Mapped {/, GET} route +1ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [GraphQLModule] Mapped {/graphql, POST} route +55ms
[Nest] 12544  - 2022/10/18 19:01:42     LOG [NestApplication] Nest application successfully started +1ms

shuntakashuntaka

mutationでDBに対してクエリが発行されているのに、Rollbackしてしまい、DBに反映されない問題が発生。。

nestエラーログ
[Nest] 38276  - 2022/10/18 20:06:24   ERROR [ExceptionsHandler] Cannot update entity because entity id is not set in the entity.
TypeORMError: Cannot update entity because entity id is not set in the entity.
    at /Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/query-builder/ReturningResultsEntityUpdator.ts:217:27
    at Array.map (<anonymous>)
    at ReturningResultsEntityUpdator.insert (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/query-builder/ReturningResultsEntityUpdator.ts:210:40)
    at InsertQueryBuilder.execute (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/query-builder/InsertQueryBuilder.ts:175:53)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at SubjectExecutor.executeInsertOperations (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/persistence/SubjectExecutor.ts:428:42)
    at SubjectExecutor.execute (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/persistence/SubjectExecutor.ts:137:9)
    at EntityPersistExecutor.execute (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/persistence/EntityPersistExecutor.ts:194:21)
    at BooksService.create (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/src/books/books.service.ts:28:5)
    at target (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/@nestjs/core/helpers/external-context-creator.js:77:28)

mysql> select a.event_time, convert(a.argument using utf8) from mysql.general_log a;
+----------------------------+-----------------------------------------------------------------------------------------------------------------+
| event_time                 | convert(a.argument using utf8)                                                                                  |
+----------------------------+-----------------------------------------------------------------------------------------------------------------+
| 2022-10-18 11:05:52.775098 | commit                                                                                                          |
| 2022-10-18 11:06:08.874581 | SELECT DATABASE()                                                                                               |
| 2022-10-18 11:06:08.879834 | nest_sample_app                                                                                                 |
| 2022-10-18 11:06:08.888048 | show databases                                                                                                  |
| 2022-10-18 11:06:08.956523 | show tables                                                                                                     |
| 2022-10-18 11:06:08.976068 | book                                                                                                            |
| 2022-10-18 11:06:10.500410 | truncate book                                                                                                   |
| 2022-10-18 11:06:13.811616 | commit                                                                                                          |
| 2022-10-18 11:06:17.254653 | truncate book                                                                                                   |
| 2022-10-18 11:06:21.199720 | commit                                                                                                          |
| 2022-10-18 11:06:24.222725 | START TRANSACTION                                                                                               |
| 2022-10-18 11:06:24.232145 | INSERT INTO `book`(`id`, `title`, `author`, `price`, `createdAt`) VALUES (DEFAULT, 'test', 'Bob', 201, DEFAULT) |
| 2022-10-18 11:06:24.250653 | ROLLBACK                                                                                                        |
| 2022-10-18 11:06:54.435046 | select a.event_time, convert(a.argument using utf8) from mysql.general_log a                                    |
+----------------------------+-----------------------------------------------------------------------------------------------------------------+
14 rows in set, 1 warning (0.01 sec)


ここら辺を眺めて、修正したところ一応COMMITまでクエリされて、DBにデータが入った。

https://github.com/typeorm/typeorm/issues/7643

/src/books/books.service.ts b/src/books/books.service.ts
@@ -23,7 +23,7 @@ export class BooksService {

   async create(data: NewBookInput): Promise<Book> {
     const book = this.booksRepository.create(data);
-    await this.booksRepository.save(book);
+    await this.booksRepository.save(book, { reload: false });

     return book;
   }

mysql> select a.event_time, convert(a.argument using utf8) from mysql.general_log a;
+----------------------------+-----------------------------------------------------------------------------------------------------------------+
| event_time                 | convert(a.argument using utf8)                                                                                  |
+----------------------------+-----------------------------------------------------------------------------------------------------------------+
| 2022-10-18 11:15:10.149738 | commit                                                                                                          |
| 2022-10-18 11:15:13.503339 | START TRANSACTION                                                                                               |
| 2022-10-18 11:15:13.512292 | INSERT INTO `book`(`id`, `title`, `author`, `price`, `createdAt`) VALUES (DEFAULT, 'test', 'Bob', 201, DEFAULT) |
| 2022-10-18 11:15:13.533430 | COMMIT                                                                                                          |
| 2022-10-18 11:15:28.340107 | select a.event_time, convert(a.argument using utf8) from mysql.general_log a                                    |
+----------------------------+-----------------------------------------------------------------------------------------------------------------+
5 rows in set, 1 warning (0.01 sec)

mysql> select * from book;
+----+-------+--------+-------+----------------------------+
| id | title | author | price | createdAt                  |
+----+-------+--------+-------+----------------------------+
|  0 | test  | Bob    |   201 | 2022-10-18 11:15:13.511516 |
+----+-------+--------+-------+----------------------------+
1 row in set (0.01 sec)

ただgraphQL playground側のエラーの内容は変わったものの別のエラーが発生

playgroundのエラー詳細
playgroundエラー
{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Book.createdAt.",
      "locations": [
        {
          "line": 4,
          "column": 5
        }
      ],
      "path": [
        "addBook",
        "createdAt"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Cannot return null for non-nullable field Book.createdAt.",
            "    at completeValue (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/graphql/execution/execute.js:594:13)",
            "    at executeField (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/graphql/execution/execute.js:489:19)",
            "    at executeFields (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/graphql/execution/execute.js:413:20)",
            "    at completeObjectValue (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/graphql/execution/execute.js:914:10)",
            "    at completeValue (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/graphql/execution/execute.js:635:12)",
            "    at completeValue (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/graphql/execution/execute.js:584:23)",
            "    at /Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/graphql/execution/execute.js:486:9",
            "    at processTicksAndRejections (node:internal/process/task_queues:96:5)",
            "    at execute (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/apollo-server-core/src/requestPipeline.ts:501:14)",
            "    at processGraphQLRequest (/Users/shuntaka/repos/github.com/shuntaka9576/nest-sandbox/node_modules/apollo-server-core/src/requestPipeline.ts:407:22)"
          ]
        }
      }
    }
  ],
  "data": null
}

先ほどのissueを読み直して、やはりidの指定が問題なきがしたので、reloadオプションは戻した。
MySQLのNO_AUTO_VALUE_ON_ZERO設定が起因指定そう。issue通り、null指定で回避できた気がする。

playgroundのエラーはcreatedAt付きをreturnしてなかったのが原因なので、保存された値を返却するようにした。

   async create(data: NewBookInput): Promise<Book> {
     const book = this.booksRepository.create(data);
-    await this.booksRepository.save(book);
+    const savedBook = await this.booksRepository.save({ id: null, ...book });

-    return book;
+    return savedBook;
   }
{
  "data": {
    "addBook": {
      "title": "test",
      "createdAt": "2022-10-18T02:42:32.320Z",
      "price": 201
    }
  }
}
shuntakashuntaka

改めてMutation成功 🎉

クエリ
mutation AddBook {
  addBook(newBook: { title : "test" price: 201 author: "Bob" }) {
    title
    createdAt
    price
  }
}
shuntakashuntaka

Mutation成功時に発行したクエリに疑問が発生した。

Mutation成功時のMySQL側で発行されたクエリ
mysql> select a.event_time, convert(a.argument using utf8) from mysql.general_log a;
+----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| event_time                 | convert(a.argument using utf8)                                                                                                                                                                                           |
+----------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 2022-10-18 22:26:17.822622 | commit                                                                                                                                                                                                                   |
| 2022-10-18 22:26:25.854035 | nest@172.24.0.1 on nest_sample_app using TCP/IP                                                                                                                                                                          |
| 2022-10-18 22:26:25.860218 | SELECT `Book`.`id` AS `Book_id`, `Book`.`title` AS `Book_title`, `Book`.`author` AS `Book_author`, `Book`.`price` AS `Book_price`, `Book`.`createdAt` AS `Book_createdAt` FROM `book` `Book` WHERE `Book`.`id` IN (NULL) |
| 2022-10-18 22:26:25.886099 | START TRANSACTION                                                                                                                                                                                                        |
| 2022-10-18 22:26:25.890628 | INSERT INTO `book`(`id`, `title`, `author`, `price`, `createdAt`) VALUES (NULL, 'test', 'Bob', 201, DEFAULT)                                                                                                             |
| 2022-10-18 22:26:25.895463 | SELECT `Book`.`id` AS `Book_id`, `Book`.`createdAt` AS `Book_createdAt` FROM `book` `Book` WHERE `Book`.`id` = 1                                                                                                         |
| 2022-10-18 22:26:25.900121 | COMMIT                                                                                                                                                                                                                   |

流れとしては

  1. id=nullのレコードチェック
  2. トランザクションスタート
  3. レコードinsert
  4. インサートしたレコードをSELECT
  5. コミット

4でid = 1であることを特定できるのはなぜなのか疑問に感じる、、(これは1->2のときでも同じ、アプリ側で保持している感じもしない。別トランザクションで手動で追加して、ロールバックしても正しく挿入される)

SELECT LAST_INSERT_ID()を使えば、直前のinsert結果のauto_incrementの値が取れるようだが、general_logからは見つからない。もちろんこのクエリは手動で発行してみても、SELECTのサブクエリに入れてもgeneral_logに追記される。。謎、、
SELECT LAST_INSERT_ID()を使っているがgeneral_logに残っていない説、、?

本件と全く関係ないが、こういう仕様もあったな、、
https://qiita.com/y_sone/items/8b2e631ed6d881151efb

save自体が悪手かもなぁ、、
https://dev.to/rishit/optimizing-typeorm-tips-from-experience-part-1-dont-use-save-4ke9

NO_AUTO_VALUE_ON_ZERO外すと流れの項1は発行されないっぽいね。そしてうまくいく。この設定したところでこの疑問自体はそのままだけど、、結局どうやってwhereに指定するidの値を取ってきているか不明。

他のうまく動作するパターン
diff --git a/_tools/mysql/conf.d/mysqld.cnf b/_tools/mysql/conf.d/mysqld.cnf
index 714213c..b8aa197 100644
--- a/_tools/mysql/conf.d/mysqld.cnf
+++ b/_tools/mysql/conf.d/mysqld.cnf
@@ -1,7 +1,7 @@
 [mysqld]
 default-authentication-plugin=mysql_native_password
 character_set_server=utf8mb4
-sql_mode=TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY
+sql_mode=TRADITIONAL,ONLY_FULL_GROUP_BY
 general_log=1
 log_output=TABLE
 slow_query_log=1
diff --git a/src/books/books.service.ts b/src/books/books.service.ts
index 6951c1e..cb564b1 100644
--- a/src/books/books.service.ts
+++ b/src/books/books.service.ts
@@ -23,7 +23,7 @@ export class BooksService {

   async create(data: NewBookInput): Promise<Book> {
     const book = this.booksRepository.create(data);
-    const savedBook = await this.booksRepository.save({ id: null, ...book });
+    const savedBook = await this.booksRepository.save({ ...book });

     return savedBook;
   }

https://github.com/typeorm/typeorm/issues/5258

shuntakashuntaka

よく使うクエリリスト

DBのクリア

TRUNCATE TABLE book;
TRUNCATE TABLE mysql.general_log;
commit;

発行クエリチェック

select a.event_time, convert(a.argument using utf8) from mysql.general_log a;
shuntakashuntaka

saveメソッドのreloadオプションは、insertした後に再度selectする仕様っぽい。falseの場合、insert後に確定するDEFULT指定の値が型で補完されない。よくできている。今回だとcreatedAtとid。

idの取得ロジックはここら辺かな

https://github.com/typeorm/typeorm/blob/cdabaa30287d357c0ae994209e573f97f92dad22/src/persistence/EntityPersistExecutor.ts#L41

https://github.com/typeorm/typeorm/blob/cdabaa30287d357c0ae994209e573f97f92dad22/src/persistence/SubjectExecutor.ts#L428-L439

https://github.com/typeorm/typeorm/blob/cdabaa30287d357c0ae994209e573f97f92dad22/src/query-builder/InsertQueryBuilder.ts#L46

https://github.com/typeorm/typeorm/blob/cdabaa30287d357c0ae994209e573f97f92dad22/src/query-builder/InsertQueryBuilder.ts#L163

https://github.com/typeorm/typeorm/blob/cdabaa30287d357c0ae994209e573f97f92dad22/src/driver/mysql/MysqlQueryRunner.ts#L182

ログを仕込んでみたが、おそらくDriver側で取れる値が多いように見える。SQLを普通に叩く分には、影響与えたレコード数しか取得できないため。

node_modules/typeorm/driver/mysql/MysqlQueryRunner.js
    /**
     * Executes a raw SQL query.
     */
    async query(query, parameters, useStructuredResult = false) {
        if (this.isReleased)
            throw new QueryRunnerAlreadyReleasedError_1.QueryRunnerAlreadyReleasedError();
        return new Promise(async (ok, fail) => {
            try {
                const databaseConnection = await this.connect();
                this.driver.connection.logger.logQuery(query, parameters, this);
                const queryStartTime = +new Date();
                databaseConnection.query(query, parameters, (err, raw) => {
                    // log slow queries if maxQueryExecution time is set
	            console.log(`driver: query: ${query}`)
                    const maxQueryExecutionTime = this.driver.options.maxQueryExecutionTime;
                    const queryEndTime = +new Date();
                    const queryExecutionTime = queryEndTime - queryStartTime;
                    if (maxQueryExecutionTime &&
                        queryExecutionTime > maxQueryExecutionTime)
                        this.driver.connection.logger.logQuerySlow(queryExecutionTime, query, parameters, this);
                    if (err) {
                        this.driver.connection.logger.logQueryError(err, query, parameters, this);
                        return fail(new QueryFailedError_1.QueryFailedError(query, parameters, err));
                    }
                    const result = new QueryResult_1.QueryResult();
                    result.raw = raw;
			console.log(`driver: result.raw: ${JSON.stringify(result.raw)}`)
結果
driver: query: INSERT INTO `book`(`id`, `title`, `author`, `price`, `createdAt`) VALUES (DEFAULT, ?, ?, ?, DEFAULT)
driver: result.raw: {"fieldCount":0,"affectedRows":1,"insertId":108,"info":"","serverStatus":3,"warningStatus":0}

shuntakashuntaka

ここをみてもPrimitive API for MySQLでは取れる情報が違いそう。パケット取ってみたらわかりそうだね。

https://neue.cc/2017/08/07_556.html
https://labs.gree.jp/blog/2014/10/11400/
https://stackoverflow.com/questions/20372808/what-do-affected-rows-and-last-insert-id-in-mysql-sever-client-protocol-us

https://twitter.com/h0z1_576/status/1582654080173756416

プロトコルスタックまでは調べてないが、ざっくりinsertIdっぽい挙動しているbyteは見つかった。truncateしてinsertしたら1に戻るところまで確認したので多分あっているはず。

# TLS無効化
mysql -h 127.0.0.1 -u root --ssl-mode=DISABLED

shuntakashuntaka

なんかめちゃくちゃ話逸れたな、、戻るとするか。

shuntakashuntaka

autocommit=0にしたせいか、イメージを作り直すとこける

docker compose down --rmi all --volumes --remove-orphans
$ docker compose up
中略
nest-db  | 2022-10-19 22:25:41+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.29-1.el8 started.
nest-db  | 2022-10-19 22:25:41+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
nest-db  | 2022-10-19 22:25:41+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.29-1.el8 started.
nest-db  | 2022-10-19 22:25:42+00:00 [Note] [Entrypoint]: Initializing database files
nest-db  | 2022-10-19T22:25:42.674766Z 0 [Warning] [MY-010918] [Server] 'default_authentication_plugin' is deprecated and will be removed in a future release. Please use authentication_policy instead.
nest-db  | 2022-10-19T22:25:42.674955Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.29) initializing of server in progress as process 99
nest-db  | 2022-10-19T22:25:42.756930Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
nest-db  | 2022-10-19T22:25:43.282942Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
nest-db  | 2022-10-19T22:25:46.931723Z 6 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
nest-db  | 2022-10-19T22:25:50.656353Z 6 [ERROR] [MY-000061] [Server] 1694  Cannot modify @@session.sql_log_bin inside a transaction.
nest-db  | 2022-10-19T22:25:50.661657Z 0 [ERROR] [MY-013236] [Server] The designated data directory /var/lib/mysql/ is unusable. You can remove all files that the server added to it.
nest-db  | 2022-10-19T22:25:50.661802Z 0 [ERROR] [MY-010119] [Server] Aborting
nest-db  | 2022-10-19T22:25:51.349305Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.29)  MySQL Community Server - GPL.
nest-db exited with code 1

妥協的に初回コンテナ作成時はautocommit=0設定を削除する方法とした、、docker-compose.ymlの環境変数が設定されないのは痛いため。

shuntakashuntaka

とりあえずコンテナ間で起動できた。

コンテナ設定
FROM node:16.13.1-alpine AS build

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . ./
RUN npm run build && npm prune --production


FROM node:16.13.1-alpine

ENV PORT=3000
# productionの場合、http://localhost:3000/graphqlは閲覧不可
ENV NODE_ENV=production
WORKDIR /app

COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/package.json /app/package.json

EXPOSE 3000
ENTRYPOINT [ "node" ]
CMD [ "dist/main.js" ]

docker-compose.yml
version: "3.9"
services:
  nest-app:
    container_name: nest-app
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    networks:
      - app-net
  nest-db:
    image: mysql:8.0.29
    platform: linux/arm64/v8
    container_name: nest-db
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
      MYSQL_USER: nest
      MYSQL_PASSWORD: nest
      MYSQL_DATABASE: nest_sample_app
    volumes:
      - nest-db-data:/var/lib/mysql
      - $PWD/_tools/mysql/conf.d:/etc/mysql/conf.d:cached
    ports:
      - "3306:3306"
    networks:
      - app-net
networks:
  app-net:
    driver: bridge
volumes:
  nest-db-data:

docker compose up --build
curl -X POST 'http://localhost:3000/graphql' \
  -H 'Content-Type: application/json' \
  --data '{"query": "mutation AddBook {addBook(newBook: { title : \"test\" price: 201 author: \"Bob\" }) { title createdAt price}}"}'
{"data":{"addBook":{"title":"test","createdAt":"2022-10-20T00:26:41.045Z","price":201}}}
shuntakashuntaka

他にも色々やりたいことがあったけど、ひとまずここでクローズ
作った成果物は↑の固定コメントに書いた

このスクラップは2022/10/27にクローズされました