Next.js + Typerom 实践 - 博客系统(三) 操作数据库

1,903 阅读5分钟

最近用 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。