第一次使用Typeorm的挖坑总结
jiayisheji opened this issue · 0 comments
最近一个公司官网需要做后台管理,自告奋勇伸出手接下这活。我本来计划技术栈是 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
对应叫 Entity
。 Entity
里面字段列和数据库里面的是一一对应的。
换句话来说,在数据库里面建表,要么手动建,设计表结构,另外一种就是 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})
满心欢喜插入了数据库,发现数据库里面的数据 id
是 0
。
一开始不懂为什么,按道理我设置自增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
语句:
- 显示所有数据表
show databases;
- 切换指定数据表
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
一个默认值 DEFAULT
, Mysql
就会给它默认一个 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
最终就解决报错问题。
写在最后
无论使用什么技术都没有一帆风顺的,总是有无尽的坑需要填,各方面原因凑在一起就引起未知的坑,我们需要掌握排坑技巧,不断提升解决问题的能力。
今天就到这里吧,伙计们,玩得开心,祝你好运。