最近用 Next.js 和 typeorm 完成了前端小白的第一个全栈项目,本文会记录我在做项目的过程中学习到的一些知识点,和遇到的那些奇奇怪怪的 Bug
Github - 献上源码地址
博客系统 - 献上预览地址,喜欢的话就留下一篇博客吧
Next.js + typerom 实践 - 博客系统(一) 初始化项目
Next.js + typerom 实践 - 博客系统(二) 初始化数据库
上篇文章中已经介绍了如果使用初始化数据库了,那么这篇文章就一起来使用 Typeorm 操作数据库吧
设计数据表
还记得一篇文章中,我们设计了项目的需求吗?
P.S. 一开始是使用知乎写的,所以打着知乎的水印
按照这个需求,我设计了这样的数据表
通过 migration 创建数据表
由于要创建三个表,下面就用 Posts 表为例子介绍吧
Posts 表
首先通过 typeorm 初始化 migration
npx typeorm migration:create -n CreatePosts
我们会得到这样一个文件
[TIMESTAMP]-CreatePosts.ts
├── src
│ ├── entity
│ ├── index.ts
│ └── migration
│ └── 1606662312046-CreatePosts.ts // 新增文件
快填入代码吧
import {MigrationInterface, QueryRunner, Table} from 'typeorm';
export class CreatePosts1606662312046 implements MigrationInterface {
// 运行写入操作时执行 typeorm migration:run
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(new Table(
{
name: 'posts',
columns: [
{name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment'},
{name: 'author_id', type: 'int'},
{name: 'title', type: 'varchar'},
{name: 'content', type: 'text'}
]
}
))
}
// 运行恢复操作时执行 typeorm migration:revert
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('posts')
}
}
运行 migration 数据迁移
由于我们写的时 ts 文件,需要使用 babel 转成 ts 才能运行,上篇文章已经提到转换的命令是这样的
npx babel ./src --out-dir dist --extensions ".ts,.tsx"
但是这个命令每次修改后都有执行,不是自动的,那就改进一下吧,加上 watch -w
监听文件,每次文件修改后自动编译
npx babel -w ./src --out-dir dist --extensions .ts,.tsx\
还是觉得麻烦吗?
把这个命令写到 package.json 里面吧
"scripts": {
...
"typeorm:build": "npx babel -w ./src --out-dir dist --extensions \".ts,.tsx\"",
"m:create": "typeorm migration:create",
"m:run": "typeorm migration:run",
"m:revert": "typeorm migration:revert"
...
},
我们先运行 yarn typeorm:build
不要关闭
然后运行 yarn m:create
就能写入数据库
如果出错了,也不要紧运行 yarn m:run
撤回吧
创建实体 Entity
我们已经将 Posts 表写入数据库了,那么如何读写 Post 呢?
需要将数据映射到 Entity 实体,使用命令创建实体 typeorm entity:create -n Post
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn
} from 'typeorm';
import {User} from './User';
import {Comment} from './Comment';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('increment')
id: number
@Column('varchar')
title: string
@Column('text')
content: string
@CreateDateColumn()
createdAt: Date
@UpdateDateColumn()
updatedAt: Date
@ManyToOne(() => User, user => user.posts) // author 和 user.posts 的对应
author: User;
@OneToMany(() => Comment, comment => comment.post) // comments 和 comment.post 的对应
comments: Comment[]
constructor(title: string, content: string, author: User) {
this.title = title
this.content = content
this.author = author
}
}
使用实体填充数据
操作实体 Typeorm 提供了两种方法 EntityManager 或 Repository
这只是两种不同的封装思路而已,需要灵活使用
下面就以 EntityManager 为例,实现 seed.ts 填充数据
src/seed.ts
import 'reflect-metadata';
import {createConnection} from 'typeorm';
import {User} from './entity/User';
import {Post} from './entity/Post';
import {Comment} from './entity/Comment';
createConnection().then(async connection => {
const manager = connection.manager
const users = await manager.find(User)
const posts = await manager.find(Post)
const comments = await manager.find(Comment)
if (users.length === 0 && posts.length === 0 && comments.length === 0) {
const user = new User('Jacky', '123456')
await manager.save([user])
const post = new Post('第一篇文章', '这是写得最好的文章', user)
await manager.save([post])
const comment = new Comment('第一个评论', user, post)
await manager.save([comment])
}
await connection.close()
}).catch(error => console.log(error));
运行
node dist/seed.js
一个难题
通过上面的操作我们知道了怎么操作数据库,其中需要用到 connection
这个对象,而上述操作都是单次运行的,那么如果需要 Next.js 的开发环境中使用 connection
就会有问题
我尝试通过 createConnection
来创建连接,并提供一个 getDatabaseConnection 的方法
/lib/getDatabaseConnection.ts
import {createConnection} from 'typeorm';
const getDatabaseConnection = async () => {
return createConnection();
}
export default getDatabaseConnection
首次运行没有问题,但是一旦我修改了代码,Next.js 会进行热重置,就会出现报错
这个报错的意思是名字为 default 的 connection 已经存在不可以重复创建
这个应该是重复 create 引起的,所以我改变了思路,只在第一次执行的使用使用 create, 后续都使用 get 的方法获取 connection,这样就不会重复了
/lib/getDatabaseConnection.ts
const getDatabaseConnection = async () => {
const connection = getConnection()
if (connection) {
return connection
} else {
return createConnection();
}
}
但实际运行起来,还是报错了,说明热重载的时候 create 又重新运行了
在网上找了好久后,终于发现了可以使用 typeorm 的 getConnectiongManger 可以得到是否已创建 connection 的信息,改造成功
/lib/getDatabaseConnection.ts
const getDatabaseConnection = async () => {
const manage = getConnectionManager()
if (!manage.has('default')) {
return createConnection();
} else {
const current = manage.get('default')
if (current.isConnected) {
return current
} else {
return createConnection();
}
}
}
于是我尝试自己用闭包的方式写一个 manager 提供 get has create 的方法,发现还是实现不了,每次 manager 都会被置空,热重载的受还是会重新运行
/lib/manager.ts
let connection: Connection = null;
export const create = asycn () => {
connection = await createConnection()
return connection
}
export const get = () => {
return connection
}
export const has = () => {
return connection !== null
}
/lib/getDatabaseConnection.ts
import * as manager from ./manager
const getDatabaseConnection = async () => {
if (!manage.has()) {
return manager.create()
} else {
const current = manager.get()
if (current.isConnection) {
return current
} else {
return manaer.create()
}
}
}
结论是只有 node_modules 在热重载的时候不会更新,而其他文件则会重新运行
总结
- Migration 数据迁移,用来对数据库进行写入或回撤。
- Entity 实体,用类和对象来操作数据表和数据行。
- Connection 连接,与数据库连接,默认最多 10 个。
- Manager / repo,两种 API 封装风格,用于操作 Entity。