Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

第一次使用Typeorm的挖坑总结 #54

Open
jiayisheji opened this issue Mar 30, 2022 · 0 comments
Open

第一次使用Typeorm的挖坑总结 #54

jiayisheji opened this issue Mar 30, 2022 · 0 comments

Comments

@jiayisheji
Copy link
Owner

jiayisheji commented Mar 30, 2022

最近一个公司官网需要做后台管理,自告奋勇伸出手接下这活。我本来计划技术栈是 Nestjs + MongoDB,看我的github的人应该发现,我只会这个。和运维一番沟通后,他说不支持 MongoDB,仅支持 Mysql

第一次使用 Mysql

这是一段神奇的开始...

在 nestjs 官网文档有个专门的 database 板块。

首推就是 Typeorm ,这篇也算是一个入门教程。(ps:里面也有无尽的坑)

nestjs 也有其他几个操作数据库的的 orm:

以上都是操作 Mysql 的特有 orm,有些 nestjs 做了专门集成封装模块,方便使用。

既然官网教程首推 Typeorm,那我们就用上。

我电脑里面装了一个 Navicat Premium,可以可视化多种数据的图形化界面。

关于 Mysql ,你可以选择 Docker 安装,也可以直接下载安装文件安装。推荐 Docker

本来我也打算 Docker 安装的,运维给我了一个服务器的 Mysql 的地址和账号密码。那就直接连接就行了。

因为不会 Mysql 语句,那就傻瓜式图形界面创建数据库吧。

也不知道怎么创建,好歹公司后台都是Java,用的全是 Mysql,找个人问下,就解决问题。

图形化界面可以自动生成 Mysql 语句:

CREATE DATABASE `test` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';

连接远程 Mysql 搞定,创建数据库搞定,接下来就是程序连接和建表操作。

根据 nestjs 官网文档,一顿操作下来完美连接运行。

第一个坑,自动建表

关于 Mysql 的表,在 Typeorm 对应叫 EntityEntity 里面字段列和数据库里面的是一一对应的。

换句话来说,在数据库里面建表,要么手动建,设计表结构,另外一种就是 Typeorm 帮我们自动建。

手动建,我肯定搞不懂,自动建那就比较简单,只需要看 Typeorm 文档即可。

Typeorm 载入 Entity 有三种方式:

单独定义

import { User } from './user/user.entity';

TypeOrmModule.forRoot({
    //...
    entities: [User],
}),

用到哪些实体,就逐一在此处引入。缺点就是我们每写一个实体就要引入一次否则使用实体时会报错。

这里需要说一下,我用的 Nx 这个工具,它做 nodejs 打包用的是 webpack,意思就是说会打包到一个 main.js。我只能使用这种模式。

自动加载

TypeOrmModule.forRoot({
      //...
      autoLoadEntities: true,
}),

自动加载我们的实体,每个通过 TypeOrmModule.forFeature() 注册的实体都会自动添加到配置对象的 entities 数组中, TypeOrmModule.forFeature() 就是在某个 service 中的 imports 里面引入的,这个是比较推荐。

自定义引入路径

TypeOrmModule.forRoot({
      //...
      entities: ['dist/**/*.entity{.ts,.js}'],
}),

这是官方推荐的方式。

自动建表还有一个配置需要设置:

TypeOrmModule.forRoot({
      //...
      entities: ['dist/**/*.entity{.ts,.js}'],
      synchronize: true,
}),

问题就处在 synchronize: true 上,自动建表,你修改 Entity 里面字段,或者 *.entity{.ts,.js} 的名字,都会自动帮你修改。

警告:线上一定要关了,不然直接提桶跑路,别挣扎了。

正确姿势是使用 typerom migration 方案:

migrations 会每次记录数据库更改的版本及内容,以及如何回滚,对于数据处理的更多策略就需要团队根据需求去开发。同时修改的entity 保证新的开发人员可以无需 migrations 即可直接使用。

nestjs 使用 migration 很麻烦,所以官网文档里面都没有写,migrations,大写的懵逼。

migrations

把放在 TypeOrmModule.forRoot 里的配置独立出来 ormconfig.ts

// 
export const config: TypeOrmModuleOptions = {
      type: 'mysql',
      host: process.env.host,
      port: parseInt(process.env.port),
      username: process.env.username,
      password: process.env.password,
      database: process.env.schema,
      entities: [User], // 也可以使用:  [__dirname + '/**/*.entity.{ts, js}']
     // 根据自己的需求定义,migrations
      migrations: [UserInitialState],// 也可以使用:   ['src/migration/*{.ts,.js}']
      cli: {
         migrationsDir: 'src/migration'
      },
      synchronize: true,
}

注意:这里不能使用 @nestjs/config 模块动态获取,需要使用 process.env 去获取。

建立 cli 配置 ormconfig-migrations.ts

import {config} from './ormconfig';

export = config;

TypeOrmModule.forRoot 里引入 ormconfig.ts 配置

import {config} from './ormconfig';

TypeOrmModule.forRoot(config);

package.json 里面增加 scripts:

...
 "typeorm:cli": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -f ./ormconfig-migrations.ts",
 "migration-generate": "npm run typeorm:cli -- migration:generate -n"
 "migration-run": "npm run typeorm:cli -- migration:run -n"

然后就可以愉快的玩耍了。

第二个坑,自增主键

Typeorm 提供的主键的装饰器 PrimaryGeneratedColumn,里面支持四种模式:

  • increment (默认)
  • uuid(Typeorm 帮我们自动添加)
  • rowid
  • identity

基本所有教程文章都是用默认的 increment

然后问题就出现了,使用 increment 在插入数据会出现错误:

Typeorm error 'Cannot update entity because entity id is not set in the entity.'

这个问题困扰我很久,搜索这个问题,也没有得到最终的解答

一开始找到的答案是 .save(entity, {reload: false})

满心欢喜插入了数据库,发现数据库里面的数据 id0

一开始不懂为什么,按道理我设置自增id,起始位置1开始,那么第一条应该是1才对,应该这个是不对的。

我又插入一条数据:

Mysql error ‘Duplicate entry '0' for key 'PRIMARY'

问题原因:我用的 int,它的默认值就是 0。为什么每次会插入默认值。

带着这个疑惑,寻找解决方案,配置里面有个 logging: true, 我把它打开,可以输出执行的 Mysql 语句。

然后使用 .save(entity, {reload: false}) 插入数据:

INSERT INTO `users`(`id`, `username`, `password`, `created_at`, `updated_at`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT) --PARAMETERS: ["jiayi", "123456"]

虽然看不懂是什么,大概理解一下,第一个括号插入的字段名,第二括号就是对应的值,DEFAULT 就是 Mysql 默认值,也就是我们设置的 default 属性。? 就和后面的参数一一对应。

既然 Typeorm 插入有问题,那我是不是可以直接用 Mysql 语句插入,就算玩挂了,也就是一个删库跑路。

使用 Navicat Premium 执行 Mysql ,网上找了一下简单的 Mysql 语句:

  1. 显示所有数据表
show databases;
  1. 切换指定数据表
use test
# Database changed 表示成功

MongoDB 操作差不多。

然后我在执行插入语句:

INSERT INTO `users`(`id`, `username`, `password`, `created_at`, `updated_at`) VALUES (DEFAULT, ?, ?, DEFAULT, DEFAULT) --PARAMETERS: ["jiayi", "123456"]

还是一样报错 ‘Duplicate entry '0' for key 'PRIMARY'

思考:id 是自增的应该不需要传递 id,这个字段吧。带着个这个猜想:

INSERT INTO `users`(`username`, `password`, `created_at`, `updated_at`) VALUES (?, ?, DEFAULT, DEFAULT) --PARAMETERS: ["jiayi", "123456"]

成功插入数据,真是激动万分。

这锅就是 Typeorm 的坑了。

那需要解决问题, Typeorm 提供的可以直接写语句的 query,对于我这种完全不会人肯定无法搞定,那就换个思路解决。

Typeorm 会自动给 id 一个默认值 DEFAULTMysql 就会给它默认一个 0。那如果我不设置默认, Mysql 应该没有 undefined,这种玩意,但是有一个 null,和 js 意思一样,都表示空,那我给 id 设置 null

INSERT INTO `users`(`id`, `username`, `password`, `created_at`, `updated_at`) VALUES (null, ?, ?, DEFAULT, DEFAULT) --PARAMETERS: ["jiayi", "123456"]

又成功插入数据。

意思就是说我在 .save(entity, {reload: false}) 插入数据之前,设置 entity.id = null 即可。

每次创建都是去设置太麻烦了,

@Entity('users')
export class User {
  @PrimaryGeneratedColumn({
    type: 'int',
  })
  id: number = null;
   ...
}

Entity 类型,设置默认值,这个默认值和数据库 default 是有区别的,这是实例属性值。

最后发现设置默认值 null,不光解决 Mysql 语句重复添加问题,还解决了 Typeorm 报错问题。

Typeorm 插入最终都会 https://github.com/typeorm/typeorm/blob/master/src/query-builder/ReturningResultsEntityUpdator.ts 里的 ReturningResultsEntityUpdator.insert 方法:

这是错误来源代码:

const entityIds = entities.map((entity) => {
                const entityId = metadata.getEntityIdMap(entity)!

                // We have to check for an empty `entityId` - if we don't, the query against the database
                // effectively drops the `where` clause entirely and the first record will be returned -
                // not what we want at all.
                if (!entityId)
                    throw new TypeORMError(
                        `Cannot update entity because entity id is not set in the entity.`,
                    )

                return entityId
            })

通过 https://github.com/typeorm/typeorm/blob/master/src/metadata/EntityMetadata.ts 里的 EntityMetadata.getValueMap() 静态方法获取。

在通过 https://github.com/typeorm/typeorm/blob/master/src/metadata/ColumnMetadata.ts 里的 ColumnMetadata.getEntityValueMap() 实例方法:

if() {}
else {
   if () {}
  else {
      // 如果不设置 null ,默认就直接 undefined
      if (entity[this.propertyName] !== undefined && (returnNulls === false || entity[this.propertyName] !== null))
          return { [this.propertyName]: entity[this.propertyName] };

      return undefined;
  }
}

设置默认值实例属性 id = null 最终就解决报错问题。

写在最后

无论使用什么技术都没有一帆风顺的,总是有无尽的坑需要填,各方面原因凑在一起就引起未知的坑,我们需要掌握排坑技巧,不断提升解决问题的能力。

今天就到这里吧,伙计们,玩得开心,祝你好运。

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

No branches or pull requests

1 participant