前言
mysql
也算是比较经典的免费的关系型数据库了,很多人都在用,且很好用,MongoDB
是一个介于关系型和非关系型数据库之间,性能表现不错
由于主要学习数据库,我们就从最经典的 mysql
开始,使用 typeorm
映射的 mysql
SQL基础教程 (如果数据库基础不是很好,可以参考这里,我也是参考这里回忆起来的数据库操作)
ps
:数据库是不区分大小写的,因此我们设置字段时无需过分关注大小写,我们只要保持和数据库字段一致即可,另外传参时为了避免给客户端带来大小写的麻烦,最好采用蛇形变量(_)
数据库工具仍然使用的是 Database Client
数据表 Entity
编写之前,注意引入 typeorm
和 mysql
,前面有介绍
js
yarn add typeorm @nestjs/typeorm mysql2
是我们用于映射到数据库字段的入口,可以理解为,声明模型等于建立数据库表格
下面定义一个基础用户表格,需要注意的是我们自定义的字段,一定要设置默认值 default
,否则可能会报错
ps
:存放小数时一定要存放字符串,否则会发生意想不到的精度问题,虽然了解后在意料之中,避免小数直接用数字是最好的
js
@Entity() //默认带的 entity
//@Entity('user') //可以设置数据库表格名称
export class User {
//作为主键且创建时自动生成,默认自增
@PrimaryGeneratedColumn()
id: number
//用户名,unique 唯一值, default 默认值
@Column({ unique: true, default: null })
account: string
//默认数据库的列,会根据 ts 类型,自动创建自定类型,默认字符串 255 byte,也就是255个unicode字符
@Column({default: null})
nickname: string
//可以设置唯一值,参数可以点进去看详情
@Column({ unique: true, default: null })
wxId: string
//设置默认值
@Column({ default: null })
age: number
//设置枚举,实际推荐数字 + 文档即可,方便又实惠
// @Column('simple-enum', { enum: ['0', 'woman', 'unknow'], default: 'unknow' })
// sex: string
@Column('simple-enum', { enum: [1, 2, 0], default: 0 })
sex: number
@Column({ default: null }) //默认最大字符串255字节,能储存255个unicode字符
mobile: string
//默认都是可变字节,如果设置最大长度比较小,但内容比较大,也能写入,但是效率可能会变低
//默认最大字节数比较大,65535为text,另一个更大,也可以根据自行设置大小
// @Column('mediumtext', {default: null})
@Column('text', { default: null })
desc: string
//下面是创建内容自动生成,和更新时自动更新的时间戳,分别代表该条记录创建时间和上次更新时间
@CreateDateColumn({ type: 'timestamp' })
createTime: Date
//更新内容时会自动更新时间戳
@UpdateDateColumn({ type: 'timestamp' })
updateTime: Date
...
}
数据表关系
数据表中有下面几种关系一对一、一对多、多对一、多对多
,并且他们的关系往往是双向存在的(除非单纯的包含关系,那样可以是单向,无法反向查询)
OneToOne、OneToMany、ManyToOne、ManyToMany
一对一
上面会发现我们 user表
没有密码等信息,为了演示一对一,这里额外建立了一个 author表
用它来隐藏我们的隐私信息,也方便管理了,查询时也无需隐藏了,方便了很多
可能有人说一对一影响查询速度,没有存在的必要,我想说,我们选择使用关系型数据库,不只是为了性能,更多是为了安全,为了处理繁杂的关系而存在,我们的很多表格都是根据社会分工等关系建立的,否则很多东西全混杂到一个表格那才叫难受
例如:用户表 -- 在职企业信息(假设一个用户只有一个企业) -- 学生信息 等,要不要建立一对一呢,还是揉到一起呢
下面我们用上面的用户表,将 user
和 auth
表关联起来,
给 user
表 设置 OneToOne
关联
js
//user表 添加 auth,并设置 一对一关系
@Entity() //默认带的 entity
export class User {
...
//用户隐私信息表,Auth类型,其中author 的 user 属性对应的自己
@OneToOne(() => Auth, auth => auth.user)
auth: Auth
}
给我们的 auth
表 设置 OneToOne
关联,使用@JoinColumn()
设置外键,如果想自定义外键名称可以传递参数
js
//auth 表,添加用户信息
@Entity() //默认带的 entity
export class Auth {
//作为主键且创建时自动生成,默认自增
@PrimaryGeneratedColumn()
id: number
//密码
@Column({ length: 30, default: null }) //可以设置长度30个字节
password: string
//身份证
@Column({ length: 20, default: null})
idCard: string
...
//设置 User 类型,user 的 auth 属性指向自己
@OneToOne(() => User, (user) => user.auth)
@JoinColumn() //添加外键,建立表关联,会自动生成userId,这个id就是外键为另一个user表的primaryid
user: User
//这样给外键赋值时无需传递对象了,并且获取时还可以额外获得该属性,不需内容时无需连表查询
//增加该属性并不会额外减少或者增加数据库字段,建立关系后数据库会自然的生成该字段,只不过我们没有映射
@Column({ default: null })
userId: number
}
需要注意的是,一定要在一个表格中设置 @JoinColumn()
,其意思是设置外键关联,会给设置的表格自动添加一个外键id
,并指向另外一个表格的 id
属性,
本案例给 auth
添加的外键,在数据库中,默认名字为 userId
, 即,默认外键名称为: 参数 + Id
,并且,我们赋值时,需要给设置外键的auth
表的关联属性user
赋值,这样就会自动取另一个对象user
的id属性
会赋值给我们的外键 userId
保存到数据库中
js
//给谁添加的外键(JoinColumn),谁需要被赋值,这个靠映射关联,因此保存之前确定映射关系也没问题
auth.user = user;
await this.authRepository.save(auth);
为什么给 author
添加外键呢,我们user
是一个重要的表格,又可以称为主表,auth
作为一个子表扩展是比较合理的(ps:外键是用于和其他表建立关系扩展功能用的,因此又叫做子表、关联表)
上面会发现我们我们建立关联时,给外键表格 auth
额外增加了一个 userId
字段,前面说了,这个字段即使不声明数据库也会存在,我们设置他也是为了获取数据时,额外获取一个userId
,某些时候只需要对比 id
,则可以不用联表查询了,另外建立关联的逻辑 是 给外键赋予另一个表的 id
,所以,我们也可以直接给该 id
直接赋值建立关系(一定要确认另一个表的 id 存在哈)
js
auth.userId = user.id;//给谁添加的外键(JoinColumn),谁需要被赋值,注意userid保存前id不存在
await this.authRepository.save(auth);
看看我们建立的数据库表格吧
一对多/多对一
这个也是我们常用的功能,我们就以掘金为例,一个用户可以编写多篇文章,每篇文章只会对应一个用户
,这就是典型的一对多/多对一
,他们两个实际上是一样的,只不过从哪个角度看,一个是多对一、另一个必然是一对多
就以掘金的文章和用户为例,建立 一对多/多对一
关系表
需要注意
的是,一对多/多对一
的外键
一定是建立在 多对一
的一方,因为数据多的那方只需要设置一个外键,即可关联另外一个表格,而另一个表格获取多个时,只需要将外键和自己匹配的全部获取即可
给文章 article
表 添加 多对一
,并添加外键
js
//文章article表
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number
//内容
@Column({ default: null })
title: string
//描述
@Column({ default: null })
desc: string
//内容
@Column('mediumtext', { default: null })
content: string
//文章状态,默认创建即编辑中、等待审核、审核中、成功、失败
//平时可以数字枚举或者个别字符,以提升实际效率和空间,文档注释最重要,这里纯粹为了看着清晰
@Column('simple-enum', {
enum: [ArticleStatus.editing, ArticleStatus.waiting, ArticleStatus.checking, ArticleStatus.success, ArticleStatus.failure],
default: ArticleStatus.editing,
})
status: ArticleStatus
...
//设置多对一的关系,顺道添加一个外键
@ManyToOne(() => User, user => user.articles)
@JoinColumn() //设置外键
user: User
@Column({ default: null })
userId: number
//额外设置一个软删除,当必要时可以恢复
@Column({ default: false, select: false })
isDelete: boolean
}
给 user 表添加一对多关联
js
//user表
@Entity()
export class User {
...
//我发布的文章,一个用户多篇文章,一对多
@OneToMany(() => Article, article => article.user)
articles: Article[]
}
多对多
多对多
就一种,我们还是以掘金的文章和表格为例,一篇文章可以被多个不同的人收藏、一个人可以收藏多篇文章
,这就是典型的一对多
多对多
没有那么多要求,两个都是多对多,我们的关联参数都只能是数组,无法设置外键,因此我们就要使用 @JoinTable()
新建立一个关系表格了(就是单纯的 article、user的id建立关系表)
js
//user表
@Entity()
export class User {
...
//收藏文章
//一篇文章会被多个人收藏,一个人可以收藏多篇文章
@ManyToMany(() => Article, article => article.collects)
collects: Article[]
}
这里我就以文章为主(多对多不分主次表,但使用的位置不同生成的数据库默认名称不同哈),在里面使用 @JoinTable()
建立关联表格
js
//文章article表
@Entity()
export class Article {
...
//假设需要显示收藏数很频繁,但收藏操作很不频繁,可以额外维护一个收藏数count
@Column({ default: 0 })
collectCount: number
//一篇文章会被多个人收藏,一个人可以收藏多篇文章
@ManyToMany(() => User, user => user.collects)
@JoinTable() //多对多,会自动生成两个{nameId} + 主键的新表,表名:当前表名_当前键名_关联表名 例如:article_collects_user
collects: User[]
}
这里在 article
的 collects
上创建的关联 user
的关系表,因此系统给我们的关系表默认起的名字也很中规中矩,article_collects_user
,参数分别为 articleId
、userId
使用 @JoinTable()
时, 我们一般无需自定义名称,毕竟本来就很清晰了(如果我们真的有强迫症,需要改名字改表格,外键也要驼峰式,与@JoinColumn()
类似),如下所示
js
@JoinTable({
name: 'article_collects_user', //表名,前后表名,后面键名,方便某时候直接快速查询关系表
joinColumns: [{ name: 'article_id' }], //本表外键,默认是驼峰式
inverseJoinColumns: [{ name: 'user_id' }], //另一个表的外键
})
看看我们新建立的关系表吧
常用增删改查
我们的增删改查都是用的 typeorm
,因此要参考他的文档进行学习和使用,具体的我们可以在使用时点进方法,相信能看到我们想要的
常用的增删改查,我们就介绍 nestjs
给的 Repository
方法直接进行操作
本篇后面也会介绍,带有部分数据库查询语句的 QueryBuilder, 对复杂操作对部分基础查询和查询进行优化
service编写前的配置
前面讲过,我们的逻辑基本上都是在 service
中编写的,因此 service
中也要进行基本配置,需要哪个模块引入那个模块即可
js
//service
@Injectable()
export class AuthService {
constructor(
//使用我们的的 userRepository 仓库,按照下面方式改成自己的类即可,需要哪个引用哪个即可
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Auth)
private authRepository: Repository<Auth>,
@InjectRepository(User)
private featureRepository: Repository<Feature>,
@InjectRepository(Article)
private articleRepository: Repository<Article>,
) {}
}
另外我们使用时,需要引入其他模块的的数据库 Entity
和 Service
(我的service一开始也没多引用,好像也没事哈,但是数据不引用就会出问题,最好都引用了)
js
//module
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Auth]),
TypeOrmModule.forFeature([Article]),
],
controllers: [UserController],
providers: [
UserService,
AuthService,
ArticleService,
],
})
增改
增加和改动我们都是直接使用的 save
、更新也可以使用 update
,save
最方便实用
js
//增加
let auth = new Auth();
auth.password = loginInfo.password;
...
await this.authRepository.save(auth);
//增加多条
await this.authRepository.save([auth]);
更新和增加不太一样,一般需要先查询,后更改,方便返回数据,其中 update
是不会返回我们的新数据的,另外手动赋值最累,效果最理想
如果想直接更新,不返回内容,可以直接使用 update + 查询条件 + 内容
即可
js
async updateUser(
userInfo: UserUpdateDto,
user: User,
) {
user = await this.userRepository.findOneBy({
id: user.id
})
if (!user) {
// 可以抛出一个异常告诉没找到,一般直接返回
// throw new HttpException('该用户不存在', 204)
return ResponseData.fail('该用户不存在')
}
// //update只更新,不返回那条新user,支持设置查询条件,需要额外处理查询不到的数据
// await this.userRepository.update({
// id: userId, //查询条件
// }, userInfo)
// //也可以直接save,简单粗暴,可能会把不想改的也改了
// await this.userRepository.save({
// ...user,
// ...userInfo
// })
// 手动赋值,最累,但是存在问题最少
if (userInfo.age) user.age = userInfo.age;
if (userInfo.mobile) user.mobile = userInfo.mobile;
if (userInfo.nickname) user.nickname = userInfo.nickname;
if (userInfo.sex) user.sex = userInfo.sex;
await this.userRepository.save(user)
return user
}
删除
delete
直接根据某个参数删除, remove
根据 entity
删除,一般直接用 delete
js
//直接通过某个字段真删除,需要谨慎,一般重要数据软删除的比较多
await this.userRepository.delete({
account: loginInfo.account
})
查询
查询是用的最多的,也是最杂的,findOneBy
、findOne
、findBy
、find
、findAndCountBy
、findAndCount
这些也是用的最多的
by
系列的都是根据 entity
字段直接查询的,因此不具备条件、关系查询
;
findOne
系列查询一个,返回对象
find
系列查询多个,返回数组
findAndCount
系列查询多个的同时,统计总数量,结果返回二维数组,第一个参数为查询数组内容,第二个为统计总数量
by
系列查询
js
//by系列查询
let user = await this.userRepository.findOneBy({
account: loginInfo.account,
});
非 by
系列条件关系查询,where
、relations
、order
js
let user = await this.userRepository.findOne({
//默认选项都为 null 或 undefined时,搜索条件为没条件
//为了避免选填内容,可以加上 Equal(name),如果需要查询为 null 的,用 IsNull()即可
where: {
account: "admin", //设置条件相符
},
relations: {
auth: true, //设置了之后,会自动连接查询,我们设置的外键那整对象都会被赋值了
},
order: {
createTime: 'DESC', //我们按照时间降序排列,升序 ASC,大小写没区别都支持
},
//relations: ['auth'], //也可以通过这种方式
});
分页查询 skip
、take
js
let articles = await this.articleRepository.find({
skip: 1, //自己计算页码和数量,这个是偏移量,第一页默认就是0,第二页默认就是 1 * 10
take: 10, //页码 10
})
或查询,即:多个 where 查询条件取或
js
let user = await this.userRepository.findOne({
where: [{
account: 'admin', //同时查询用户名为 admin 或 年龄为 20 的用户
},{
age: 20
}]
});
关联表条件查询(对关系表中的数据筛选),直接嵌套一层关联对象条件即可
js
let user = await this.userRepository.findOne({
where: {
account: "admin", //设置条件相符
auth: {
password: '123456', //对关联表进行查询
},
},
relations: {
auth: true, //设置了之后,会自动连接查询,我们设置的外键那整对象都会被赋值了
},
});
进阶查询
这里只介绍一部分,如果缺少,可以参考数据库的一些,另外可以使用一个,点进去看看支持哪些即可
Equal
:是否相等,我们默认的筛选虽然也可以,但是无法查询为 null 的参数(如果参数为 null 那么默认的对比,会认为没有该条件,如果全为 null,则返回数据库第一条数据或者全部数据),因此要上 Equal
js
let account = ...
let user = await this.userRepository.findOne({
where: {
account: Equal(account) //下面的也都是用到这里
}
});
Not
: Not("admin")
!=
LessThan
:LessThan(10)
<
LessThanOrEqual
:LessThan(10)
>=
MoreThan
:LessThan(10)
>
MoreThanOrEqual
:LessThan(10)
>=
Like
:Like("%mm_dd%")
,模糊查询,区分大小写,%
为匹配多个,_
匹配一个
ILike
:ILike("%mm_dd%")
,模糊查询,不区分大小写,%
为匹配多个,_
匹配一个
Between
:Between(1, 10)
,介于 1~10 之间 [1, 10)
,后面一般是开区间,这里是开区间
In
:In([1, 2, 3])
为包含关系,包含一个符合条件
IsNull
:IsNull()
查询为空的
Raw
:Raw("age+10=maxAge")
,使用数据库语句作为判断,但参数不能使用用户输入,避免注入攻击
统计
count
:统计有多少条,支持分页
countBy
:和count一样但直接查询,不支持排序等
sum
:单列求和
average
:单列求平均值
minimum
:单列求最小值
maximum
:单列求最大值
没有看到分组等功能,这个只能自己统计了
QueryBuilder 增删改查
如果是日常的查询等功能,上面的就足够用了,对于一些比较比较复杂的查询,通过 QueryBuilder
可以简化代码
,或者减少查询操作提高效率
插入
用的不多,可代替性很强,下面演示一下批量插入
js
await this.userRepository.
.createQueryBuilder()
.insert()
.into(User)
.values([{ account: "admin" }, { account: "admin1" }])
.execute();
更新
直接更新某条数据,可代替性很强
js
await this.userRepository.
.createQueryBuilder()
.update(User)
.set({ account: "admin" })
.where("id = :id", { id: 1 })
.execute();
删除
需要注意的是 from 和 where,别一不小心把全部的都删了哈
js
await this.userRepository.
.createQueryBuilder()
.delete()
.from(User)
.where("id = :id", { id: 1 })
.execute();
查询(重点)
先直接上一个普通的 where、relations
查询,也就是 where、leftjoin
,我们的 where
语句可以使用 AND
也可以直接 andWhere
(或就是 OR、orWhere
)
简单关联查询案例对比
先上一个默认的关联查询
js
await this.articleRepository.findOne({
where: {
id: body.id,
isDelete: false,
},
relations: {
user: true,
},
})
翻一下尝试一下,发现上面的更加清晰一些
js
let article = await this.articleRepository
.createQueryBuilder('article')
//连接对象映射,第一个为映射到赋值的属性名,第二个为其重命名,方便条件查询
.leftJoinAndSelect('article.user', 'user')
.where('article.id=:id AND article.isDelete=:isDelete', {
id: body.id,
isDelete: false,
})
.getOne()
中等关联条件查询案例对比
我们有这么一个需求,做一个收藏功能,需要判断某个用户是否收藏了某篇文章,再多一个表连接和查询条件,复杂了一点,发现也没问题,这次嵌套了关联查询条件
js
let article = await this.articleRepository.findOne({
where: {
id: body.id,
isDelete: false,
collects: {
id: user.id
}
},
relations: {
collects: true,
user: true,
},
})
if (!article) {
return '该文章不存在'
}
//查看是否收藏(这样做是否感觉性能稍低?)
let isCollect = false
if (article.collects?.length > 0) {
isCollect = true
}
article.collects = undefined;//不给用户返回这个
//可以查一下文章是否被收藏
return {
...article,
isCollect
};
完美翻译,发现还是上面的更清晰,我们多添加了一列,发现还是很ok的,只是看着代码多了
js
let article = await this.articleRepository
.createQueryBuilder('article')
.leftJoinAndSelect('article.user', 'user')
.leftJoinAndSelect('article.collects', 'collect')
// .innerJoinAndSelect('article.collects', 'user')//这个一旦找不到主体也不返回,不适合
.where('article.id=:id AND article.isDelete=:isDelete', {
id: body.id,
isDelete: false,
})
.andWhere('collect.id=:userId', {
userId: user.id
})
.getOne()
if (!article) {
return '该文章不存在'
}
let isCollect = false
if (article.collects.length > 0) {
isCollect = true;
}
article.collects = undefined;
return {
...article,
isCollect
};
复杂关联且或混合查询案例对比
在看看下面即有且又有或
的例子,表现就不是那么优秀了,QueryBuilder
的灵活性
获得了大满贯
我们有这么一个需求,查询一篇文章,支持 id、status、内容
三者联合查询取交集
,但内容
需要根据 title、desc、content
模糊查询;搜用户昵称时,支持模糊查询,满足即可,与前面是或的关系
我们使用常用的查询查询一下,发现逻辑很复杂
js
let articles = await this.articleRepository.findAndCount({
//中括号是或的关系
where: [
{
//大括号内,同级是且,单个或使用In,也可使用其他查询
//判断是否相符,如果不存在,就会自动忽略该条件了,由于模糊查询非常优秀
//如果非要插 null 的,那么需要使用 Equal() 包装了
id: body.id,
title: body.name && Like(`%${body.name}%`), //Ilike忽略大小写,like不忽略大小写
status: body.status && In(body.status), //这里面是或的关系
},
{
id: body.id,
desc: body.name && Like(`%${body.name}%`), //Ilike忽略大小写,like不忽略大小写
status: body.status && In(body.status), //这里面是或的关系
},
{
id: body.id,
content: body.name && Like(`%${body.name}%`), //Ilike忽略大小写,like不忽略大小写
status: body.status && In(body.status), //这里面是或的关系
},
{ //查询用户和其他选项是或的关系
user: {
nickname: ILike(`%${body.nickname}%`), //忽略大小写,模糊查询
}
}
],
relations: {
user: true
},
});
使用 QueryBuilder
改进,发现代码简洁了很多
js
let articles = await this.articleRepository
.createQueryBuilder('article')
.leftJoinAndSelect('article.user', 'user')
.where('article.id=:id AND article.status=:status', {
id: body.id,
status: body.status,
})
.andWhere('article.title=:name OR article.desc=:name OR article.content=:name', {
name: body.name
})
.orWhere('user.nickname LIKE :nickname', {
nickname: `%${body.nickname}%`,
})
.getManyAndCount();
关联表更新(重点)
你以为上面就结束了么,关联表更新的操作才是重中之重
,这个除了优化代码
,还是能优化效率
的
我们又有了一个简单的需求,文章需要支持收藏功能,根据传递的参数决定是否收藏
js
//我们发现无法直接更新,我们无法直接查询关联表(建立映射直接报错)
//并且还需要先查询文章的收藏者,我们需要判断自身的索引,然后添加删除根据这个来进行跟新
//倒是可以无需判断索引增加,利用抛出错误来解决,可是删除呢?没办法
let article = await this.articleRepository.findOne({
where: {
id: body.id,
isDelete: false,
},
relations: {
collects: true,
}
})
if (!article) {
return ResponseData.fail('该文章不存在');
}
//查询收藏者
let userId = user.id
let index = article.collects.findIndex(e => e.id == userId)
let isCollect = index >= 0
//既然都对比了,我们可以减少无效操作
if (body.is_collect && !isCollect) {
//尚未收藏、进行收藏
article.collects.push(user);
// article.collects.push({id: user.id} as User);//如果用的不是user,是userid的话,转化一下,关系实际只也用到了主键id
} else if (!body.is_collect && isCollect) {
//已收藏、取消收藏
article.collects.splice(index, 1)
}
await this.articleRepository.save(article)
return ResponseData.ok();
使用 QueryBuilder
改进,发现逻辑清晰了很多
js
//我们创建builder,后面在动态天添加、删除
let builder = this.articleRepository
.createQueryBuilder()
.relation(Article, 'collects')
.of(article) //我们的文章实体和user多对多,我们给collects添加新的user关联
if (body.is_collect == 1) {
try {
//直接收藏,添加已存在的会报错,errno: 1062
await builder.add(user)
} catch(err) {
if (err.errno === 1062) {
//重复,也就是已经收藏
return 'ok'
}else {
//其他的错误还没碰到,做了就做完善点
let msg = err.msg
return err.msg
}
}
}else {
//删除不存在的不会报错
await builder.remove(user)
}
上面就是根据QueryBuilder
对关联表操作的优势,我们直接看看怎么用吧,其他的基本用不到了
js
this.articleRepository
.createQueryBuilder()
.relation(Article, 'collects')
.of(article) //我们的文章实体和user多对多,我们给collects添加新的user关联
// 多对多,一对多的情况使用 add、remove
// .add(user) //添加关联
// .remove(user) //移除关联
// 一对一、多对一的情况使用 set
// .set(user) //只有一个,即添加、更新关联
// .set(null) //删除该关联
// .loadOne(); //需要加载的话,可以直接加载一个到多个关联对象
数据库表操作
对于线上的功能,我们一般不会直接开启同步功能,因此会使用一些常用的表操作,然后更新 entity 表,以减少损失的可能性,下面介绍一些常见的表操作
创建表
js
CREATE TABLE user(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100)
);
删除表(谨慎操作)
js
DROP TABLE aaaaaa;
添加列
其中 ALTER TABLE
表示修改的表,后面是更改语句
js
ALTER TABLE user
ADD idcard VARCHAR(18);
修改列类型
js
ALTER TABLE user
MODIFY name VARCHAR(200);
更改列名
js
ALTER TABLE user
RENAME COLUMN idcard to mobile;
删除列(谨慎操作)
js
ALTER TABLE user
DROP COLUMN mobile;
多人开发与数据表更改注意
平时我们独立开发的时候,一般都是 mysql
的 synchronize: true
,并且 yarn start:dev
,这样个人开发是很舒服的
但团队开发时尽量不要这样,这个 synchronize
会直接将我们更改的 entity
更新同步到数据库,配合yarn start:dev
的及时更新,对于多人开发来说可能就是一个灾难(主要针对于对列的增改)
例如:
- 两个人同时连接一个远端数据库,一个人增加了entity 和数据库字段,另一个人还没来得及拉取,那么会出现没有拉取的人会把拉取的人的数据库字段更改同步没了(他那里并没有新增字段,因此可能会导致新增的一列数据全没了),甚至出现报错
- 对于删除影响不大,最多报个错,多个字段
- 而对于修改字段类型、修改名称,这一个简直是一个灾难片,系统无法进行精准判断,会直接删除该列,然后创建新的一列,这一操作会直接导致原有数据列直接清空,并且多人开发过程中,即使这边进行了正确操作,另外一边没有及时拉取,那边如果开启同步运行的话,会出现修改会原字段的情况
那么我们该如何操作呢
有人说直接使用命令通过数据库,映射成entity
,不开启synchronize
,但仍然会出现多人开发时出现的问题,如果是没更新的人每次运行前都映射一下,会把自己新增或者修改的字段直接全部覆盖掉,因此仍然存在隐性问题,并且会降低开发速度,很不舒适
那怎么解决最好呢?
个人感觉,多人开发过程中,synchronize
可以开启,但一定不要使用 yarn start:dev
,要使用yarn start
,避免更新过于及时,此外还一定要及时沟通,当增加或者修改列
时,及时告诉其他人,立即拉取更新
,避免出现一系列同步问题,对于更新列
出现的该列数据消失
问题,我们可以先通过上一小节的数据库表操作
直接MODIFY、RENAME
直接修改,不会出现数据丢失问题,然后再手动更改我们的表即可,或者同步过去
存在上线应用的操作
当需要部署应用到线上时,并且我们的线上应用已经存在数据库的情况,我们需要对比使用命令手动操作数据库(一般对于修改列名
的操作比较谨慎,因此记录一下修改哪里了,记录下来),即可,即:对于修改列名,只需要手动修改数据库使其一致就行了,对于新增和删除的的这个不会有问题,直接使用 synchronize
就没问题了
因此,开发阶段,我们对于新增、修改列时,一定要及时通知队友拉取更新同步代码
此外,对于存在线上数据库
,还有另外一个操作对于我们来说相对比较安全,基本上不存在上述的同步问题,那就是数据迁移
,当我们进行后续开发时,更改元数据表时,直接新增一个 entity,名字稍微改的不一样,例如:user -> user1
,后面可以理解为版本号,然后全改成使用 新 entity
就行了,这样对于线上的表格就不会有任何影响了,然后需要做的数据迁移也很简单,我们直接写一个数据迁移接口(脚本)
,手动取出原数据库中所有数据,然后映射保存到最新的数据表即可(内存不够大就分页迁移,假如一次迁移10w条),此时,如果还想用之前的用户表名,直接反向操作即可
最后,为了谨慎,部署前我们最好备份
我们的数据库,避免出现其他意外情况,必要时可以恢复
对此你还有什么更好的方案呢,欢迎讨论
最后
这篇文章东西还挺多的,但是如果有了点数据库基础,那么就真的不多了,甚至这篇文章可以收藏当做一个问题参考点,看了这些基本上复杂一些的操作也可以做了
最后,祝大家学习愉快!