Nest.jsとTypeORMを試す
tauriやりたいけど、これもやらないといけないので。
やりたいこと
- (step1)Nest.jsでGraphQLを実行する
- (step2)TypeORMとMySQL(local or docker)の繋ぎ込み
- (step3)nest.jsとMySQLのコンテナ化
最終的に動くリポジトリはこちら
とりあえず公式ベースで試していく
npm i -g @nestjs/cli
プロジェクト作成。ここでパッケージマネージャーは選択可能。パッケージマネージャーは、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
$ 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!
GraphQLにしたいので、この記事を参考にする
↑のリンクで公式のがあった
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
Fastifyはベンチマークで早いっぽいけど、nestとFastify組み合わせるパターンだとmercurius
を使うのかぁ
appllo
使いたいし、ExpressとFastifyの差は体感できな気がするし、デフォルトのExpressとAppolloパターンを使うかなぁ、、
依存を追加
yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
ここからはこの記事を通り進める。気になったことをメモしていく。
モジュール
$ 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)
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を入れれば解決
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 {}
アプリを実行
nest start
http://localhost:3000/graphql
でクエリが動作した 🎉
nest startすると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!]!
}
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;
}
}
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コンテナ準備
[mysql]
default_character_set=utf8mb4
[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
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
mutationでDBに対してクエリが発行されているのに、Rollbackしてしまい、DBに反映されない問題が発生。。
[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にデータが入った。
@@ -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のエラー詳細
{
"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
}
}
}
改めてMutation成功 🎉
mutation AddBook {
addBook(newBook: { title : "test" price: 201 author: "Bob" }) {
title
createdAt
price
}
}
Mutation成功時に発行したクエリに疑問が発生した。
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 |
流れとしては
- id=nullのレコードチェック
- トランザクションスタート
- レコードinsert
- インサートしたレコードをSELECT
- コミット
4でid
= 1であることを特定できるのはなぜなのか疑問に感じる、、(これは1->2のときでも同じ、アプリ側で保持している感じもしない。別トランザクションで手動で追加して、ロールバックしても正しく挿入される)
SELECT LAST_INSERT_ID()
を使えば、直前のinsert結果のauto_incrementの値が取れるようだが、general_logからは見つからない。もちろんこのクエリは手動で発行してみても、SELECTのサブクエリに入れてもgeneral_logに追記される。。謎、、
SELECT LAST_INSERT_ID()
を使っているがgeneral_logに残っていない説、、?
本件と全く関係ないが、こういう仕様もあったな、、
save自体が悪手かもなぁ、、
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;
}
よく使うクエリリスト
DBのクリア
TRUNCATE TABLE book;
TRUNCATE TABLE mysql.general_log;
commit;
発行クエリチェック
select a.event_time, convert(a.argument using utf8) from mysql.general_log a;
save
メソッドのreloadオプションは、insertした後に再度selectする仕様っぽい。falseの場合、insert後に確定するDEFULT指定の値が型で補完されない。よくできている。今回だとcreatedAtとid。
idの取得ロジックはここら辺かな
ログを仕込んでみたが、おそらくDriver側で取れる値が多いように見える。SQLを普通に叩く分には、影響与えたレコード数しか取得できないため。
/**
* 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}
ここをみてもPrimitive API for MySQLでは取れる情報が違いそう。パケット取ってみたらわかりそうだね。
プロトコルスタックまでは調べてないが、ざっくりinsertIdっぽい挙動しているbyteは見つかった。truncateしてinsertしたら1に戻るところまで確認したので多分あっているはず。
# TLS無効化
mysql -h 127.0.0.1 -u root --ssl-mode=DISABLED
なんかめちゃくちゃ話逸れたな、、戻るとするか。
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の環境変数が設定されないのは痛いため。
とりあえずコンテナ間で起動できた。
コンテナ設定
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 /app/dist /app/dist
COPY /app/node_modules /app/node_modules
COPY /app/package.json /app/package.json
EXPOSE 3000
ENTRYPOINT [ "node" ]
CMD [ "dist/main.js" ]
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}}}
他にも色々やりたいことがあったけど、ひとまずここでクローズ
作った成果物は↑の固定コメントに書いた