在TypeScript中使用Sequelize

2,326 阅读8分钟

在你的API中编写原始SQL是如此的过时,或者说,它最多只保留给真正复杂的查询。现在是开发的简单时代,对于大多数API来说,使用许多对象关系映射器(ORM)中的一种就足够了。

ORM还方便地封装了与数据库及其查询语言进行通信的复杂细节。

这意味着你可以在多种数据库类型(如MySQL、PostgreSQL或MongoDb)上使用单一的ORM,这样就可以在数据库之间轻松切换,而不需要重写你的代码你还可以将不同类型的数据库连接到你的项目,同时使用相同的代码来访问它们。

在这篇文章中,你将学习如何使用Sequelize ORM与TypeScript。因此,拿起你的笔记本电脑,打开你的IDE,让我们开始吧!

前提条件

要跟上这篇文章,请安装以下内容。

设置项目

为了开始我们的项目,让我们建立一个简单的Express.js API,以创建一个虚拟的烹饪书,存储食谱和配料,并用流行的类别来标记我们的食谱。

首先,让我们在终端键入以下内容,创建我们的项目目录。

$ mkdir cookbook
$ cd cookbook

在新的cookbook 项目目录内,使用yarn 安装所需的项目依赖项。首先,运行npm init ,用一个package.json 文件来初始化Node.js项目。

$ npm init

在Node.js项目初始化后,从express 开始安装依赖项。

$ yarn add express

接下来,通过运行以下内容将TypeScript添加到项目中。

$ yarn add -D typescript ts-node @types/express @types/node

💡 注意,我们在安装命令中添加了一个标志,-D 。这个标志告诉Yarn将这些库作为开发依赖项添加,这意味着这些库只在项目开发时需要。我们还添加了Express.js和Node.js的类型定义。

将TypeScript添加到我们的项目中后,让我们来初始化它。

$ npx tsc --init

这将创建我们的TypeScript配置文件ts.config ,并设置默认值。

// ts.config
{
    "compilerOptions": {
      "target": "es5",                                
      "module": "commonjs",                           
      "sourceMap": true,                           
      "outDir": "dist",                              
      "strict": true,                                 
      "esModuleInterop": true,                        
      "skipLibCheck": true,                           
      "forceConsistentCasingInFileNames": true        
    }
}

在这里可以找到更多关于定制ts.config 的信息。

最后,让我们为我们的项目定义一个简单的API结构,通过创建项目目录和文件来匹配下面的大纲。

- dist # the name of our outDir set in tsconfig.json
- src
  - api
    - controllers
    - contracts
    - routes
    - services
  - db
    - dal
    - dto
    - models
    config.ts
    init.ts
  - errors
  index.ts
  ts.config

现在我们已经在index.ts 文件中定义了我们的项目结构,这是我们应用程序的起点,添加以下代码来创建我们的Express.js服务器。

# src/index.ts

import express, { Application, Request, Response } from 'express'

const app: Application = express()
const port = 3000

// Body parsing Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', async(req: Request, res: Response): Promise<Response> => {
    return res.status(200).send({ message: `Welcome to the cookbook API! \n Endpoints available at http://localhost:${port}/api/v1` })
})

try {
    app.listen(port, () => {
        console.log(`Server running on http://localhost:${port}`)
    })
} catch (error) {
    console.log(`Error occurred: ${error.message}`)
}

我们还必须包括一些额外的库,以便轻松地运行应用程序并传入环境变量。这些额外的库是nodemon,使用yarn add -D nodemoneslint使用yarn add -D eslintdotenv使用yarn add dotenv

设置Sequelize ORM

在这一点上,Express.js应用程序正在运行,所以现在是时候引入有趣的东西了。Sequelize ORM!

首先,通过运行以下程序将Sequelize添加到项目中。

$ yarn add sequelize
$ yarn add mysql2

虽然我们添加了MySQL的数据库驱动,这完全是基于个人的偏好,但你可以安装任何你喜欢的数据库的驱动来代替。点击这里查看其他可用的数据库驱动

启动Sequelize的连接

安装完Sequelize后,我们必须启动它与数据库的连接。一旦启动,这个连接将注册我们的模型。

# db/config.ts

import { Dialect, Sequelize } from 'sequelize'

const dbName = process.env.DB_NAME as string
const dbUser = process.env.DB_USER as string
const dbHost = process.env.DB_HOST
const dbDriver = process.env.DB_DRIVER as Dialect
const dbPassword = process.env.DB_PASSWORD

const sequelizeConnection = new Sequelize(dbName, dbUser, dbPassword, {
  host: dbHost,
  dialect: dbDriver
})

export default sequelizeConnection

创建和注册Sequelize模型

Sequelize提供了两种方法来注册模型:使用sequelize.define或扩展Sequelize模型类。在本教程中,我们将使用模型扩展方法来注册我们的Ingredient 模型。

我们首先创建以下的接口。

  • IngredientAttributes 定义了我们模型的所有可能的属性
  • IngredientInput 定义了传递给Sequelize's的对象的类型。model.create
  • IngredientOuput 定义了从model.create,model.update, 和 返回的对象。model.findOne
# db/models/Ingredient.ts

import { DataTypes, Model, Optional } from 'sequelize'
import sequelizeConnection from '../config'

interface IngredientAttributes {
  id: number;
  name: string;
  slug: string;
  description?: string;
  foodGroup?: string;
  createdAt?: Date;
  updatedAt?: Date;
  deletedAt?: Date;
}
export interface IngredientInput extends Optional<IngredientAttributes, 'id' | 'slug'> {}
export interface IngredientOuput extends Required<IngredientAttributes> {}

接下来,创建一个Ingredient ,该类扩展、初始化并导出import {Model} from 'sequelize' Sequelize模型类。

# db/models/Ingredient.ts

...

class Ingredient extends Model<IngredientAttributes, IngredientInput> implements IngredientAttributes {
  public id!: number
  public name!: string
  public slug!: string
  public description!: string
  public foodGroup!: string

  // timestamps!
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;
  public readonly deletedAt!: Date;
}

Ingredient.init({
  id: {
    type: DataTypes.INTEGER.UNSIGNED,
    autoIncrement: true,
    primaryKey: true,
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  slug: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  },
  description: {
    type: DataTypes.TEXT
  },
  foodGroup: {
    type: DataTypes.STRING
  }
}, {
  timestamps: true,
  sequelize: sequelizeConnection,
  paranoid: true
})

export default Ingredient

💡 注意我们给我们的模型添加了选项paranoid: true ;这通过添加一个deletedAt 属性,在调用destroy 方法时将记录标记为deleted ,对模型施加了一个软删除。

为了完成我们的模型并在连接的数据库中创建其目标表,运行模型sync 方法。

# db/init.ts

import { Recipe, RecipeTags, Tag, Review, Ingredient, RecipeIngredients } from './models'
const isDev = process.env.NODE_ENV === 'development'

const dbInit = () => {
  Ingredient.sync({ alter: isDev })
}
export default dbInit 

💡 sync 方法接受forcealter 选项。force 选项强制重新创建一个表。如果表不存在,alter 选项创建该表,或者更新该表以匹配模型中定义的属性。

💡 专业提示:为开发环境保留使用forcealter ,这样你就不会意外地重新创建你的生产数据库,丢失你所有的数据或对你的数据库应用可能破坏你的应用程序的更改。

在DAL和服务中使用我们的模型

数据访问层(DAL)是我们实现SQL查询的地方,或者在本例中,Sequelize模型查询运行的地方。

# db/dal/ingredient.ts

import {Op} from 'sequelize'
import {Ingredient} from '../models'
import {GetAllIngredientsFilters} from './types'
import {IngredientInput, IngredientOuput} from '../models/Ingredient'

export const create = async (payload: IngredientInput): Promise<IngredientOuput> => {
    const ingredient = await Ingredient.create(payload)
    return ingredient
}

export const update = async (id: number, payload: Partial<IngredientInput>): Promise<IngredientOuput> => {
    const ingredient = await Ingredient.findByPk(id)
    if (!ingredient) {
        // @todo throw custom error
        throw new Error('not found')
    }
    const updatedIngredient = await (ingredient as Ingredient).update(payload)
    return updatedIngredient
}

export const getById = async (id: number): Promise<IngredientOuput> => {
    const ingredient = await Ingredient.findByPk(id)
    if (!ingredient) {
        // @todo throw custom error
        throw new Error('not found')
    }
    return ingredient
}

export const deleteById = async (id: number): Promise<boolean> => {
    const deletedIngredientCount = await Ingredient.destroy({
        where: {id}
    })
    return !!deletedIngredientCount
}

export const getAll = async (filters?: GetAllIngredientsFilters): Promise<IngredientOuput[]> => {
    return Ingredient.findAll({
        where: {
            ...(filters?.isDeleted && {deletedAt: {[Op.not]: null}})
        },
        ...((filters?.isDeleted || filters?.includeDeleted) && {paranoid: true})
    })
}

paranoid: true 选项添加到findAll 模型方法中,在结果中包括设置了deletedAt 的软删除记录。否则,结果默认不包括软删除的记录。

在我们上面的DAL中,我们使用我们的ModelInput 类型定义定义了一些常用的CRUD查询,并将任何额外的类型放在db/dal/types.ts

# db/dal/types.ts

export interface GetAllIngredientsFilters {
    isDeleted?: boolean
    includeDeleted?: boolean
}

💡 Sequelize ORM有一些非常酷的模型方法,包括findAndCountAll ,它可以返回一个记录列表和所有符合过滤条件的记录的数量。这对于在API中返回分页的列表响应非常有用。

现在我们可以创建我们的服务,作为控制器和DAL之间的中介。

# api/services/ingredientService.ts

import * as ingredientDal from '../dal/ingredient'
import {GetAllIngredientsFilters} from '../dal/types'
import {IngredientInput, IngredientOuput} from '../models/Ingredient'

export const create = (payload: IngredientInput): Promise<IngredientOuput> => {
    return ingredientDal.create(payload)
}
export const update = (id: number, payload: Partial<IngredientInput>): Promise<IngredientOuput> => {
    return ingredientDal.update(id, payload)
}
export const getById = (id: number): Promise<IngredientOuput> => {
    return ingredientDal.getById(id)
}
export const deleteById = (id: number): Promise<boolean> => {
    return ingredientDal.deleteById(id)
}
export const getAll = (filters: GetAllIngredientsFilters): Promise<IngredientOuput[]> => {
    return ingredientDal.getAll(filters)
}

用路由和控制器为模型提供动力

我们已经走了很远的路!现在我们有了从数据库中获取数据的服务,是时候使用路由和控制器将所有的魔法带给公众了。

让我们先在src/api/routes/ingredients.ts 中创建我们的Ingredients 路由。

# src/api/routes/ingredients.ts

import { Router } from 'express'

const ingredientsRouter = Router()
ingredientsRouter.get(':/slug', () => {
  // get ingredient
})
ingredientsRouter.put('/:id', () => {
  // update ingredient
})
ingredientsRouter.delete('/:id', () => {
  // delete ingredient
})
ingredientsRouter.post('/', () => {
  // create ingredient
})

export default ingredientsRouter

我们的cookbook API最终会有几个路由,比如RecipesTags 。因此,我们必须创建一个index.ts 文件,将不同的路由注册到它们的基本路径上,并有一个中央出口来连接到我们先前的Express.js服务器。

# src/api/routes/index.ts

import { Router } from 'express'
import ingredientsRouter from './ingredients'

const router = Router()

router.use('/ingredients', ingredientsRouter)

export default router

让我们更新我们的src/index.ts ,导入我们导出的路由,并将它们注册到我们的Express.js服务器。

# src/index.ts

import express, { Application, Request, Response } from 'express'
import routes from './api/routes'

const app: Application = express()

...

app.use('/api/v1', routes)

在创建和连接路由之后,让我们创建一个控制器来链接到我们的路由并调用服务方法。

为了支持在路由和控制器之间输入参数和结果,让我们添加数据传输对象(DTO)和映射器来转换结果。

# src/api/controllers/ingredient/index.ts

import * as service from '../../../db/services/IngredientService'
import {CreateIngredientDTO, UpdateIngredientDTO, FilterIngredientsDTO} from '../../dto/ingredient.dto'
import {Ingredient} from '../../interfaces'
import * as mapper from './mapper'

export const create = async(payload: CreateIngredientDTO): Promise<Ingredient> => {
    return mapper.toIngredient(await service.create(payload))
}
export const update = async (id: number, payload: UpdateIngredientDTO): Promise<Ingredient> => {
    return mapper.toIngredient(await service.update(id, payload))
}
export const getById = async (id: number): Promise<Ingredient> => {
    return mapper.toIngredient(await service.getById(id))
}
export const deleteById = async(id: number): Promise<Boolean> => {
    const isDeleted = await service.deleteById(id)
    return isDeleted
}
export const getAll = async(filters: FilterIngredientsDTO): Promise<Ingredient[]> => {
    return (await service.getAll(filters)).map(mapper.toIngredient)
}

现在,用对控制器的调用来更新路由器。

# src/api/routes/ingredients.ts

import { Router, Request, Response} from 'express'
import * as ingredientController from '../controllers/ingredient'
import {CreateIngredientDTO, FilterIngredientsDTO, UpdateIngredientDTO} from '../dto/ingredient.dto'

const ingredientsRouter = Router()

ingredientsRouter.get(':/id', async (req: Request, res: Response) => {
    const id = Number(req.params.id)
    const result = await ingredientController.getById(id)
    return res.status(200).send(result)
})
ingredientsRouter.put('/:id', async (req: Request, res: Response) => {
    const id = Number(req.params.id)
    const payload:UpdateIngredientDTO = req.body

    const result = await ingredientController.update(id, payload)
    return res.status(201).send(result)
})
ingredientsRouter.delete('/:id', async (req: Request, res: Response) => {
    const id = Number(req.params.id)

    const result = await ingredientController.deleteById(id)
    return res.status(204).send({
        success: result
    })
})
ingredientsRouter.post('/', async (req: Request, res: Response) => {
    const payload:CreateIngredientDTO = req.body
    const result = await ingredientController.create(payload)
    return res.status(200).send(result)
})
ingredientsRouter.get('/', async (req: Request, res: Response) => {
    const filters:FilterIngredientsDTO = req.query
    const results = await ingredientController.getAll(filters)
    return res.status(200).send(results)
})
export default ingredientsRouter 

在这一点上,我们可以添加一个构建脚本来运行我们的API。

# package.json

...
"scripts": {
  "dev": "nodemon src/index.ts",
  "build": "npx tsc"
},
...

要想看到最终产品,请使用yarn run dev 来运行API,并访问我们的配料端点:http://localhost:3000/api/v1/ingredients

总结

在这篇文章中,我们用Express.js设置了一个简单的TypeScript应用程序,以使用Sequelize ORM,并通过初始化Sequelize,创建我们的模型,以及通过ORM运行查询。

在我们的项目中使用Sequelize和TypeScript可以帮助我们写更少的代码,并抽象出数据库引擎,同时为模型输入和输出定义严格的类型。这使得我们的代码更加一致,即使我们改变了数据库类型,也可以防止对我们的表进行SQL注入的情况发生。

篇文章的全部代码可以在Github上找到。我希望你觉得这篇文章很容易理解,我很想听听你对在你的应用程序中使用Sequelize的很酷的方法有什么想法,或者你在评论区有什么问题!

The postUsing Sequelize with TypeScriptappeared first onLogRocket Blog.