☠️ TS版Sequelize快速入门(含demo)

1,619 阅读6分钟

如果是经常使用Node来做服务端开发的同学,肯定不可避免的会操作数据库,做一些增删改查(CRUD)的操作,如果是一些简单的操作,类似定时脚本什么的,可能就直接生写SQL语句来实现功能了,而如果是在一些大型项目中,数十张、上百张的表,之间还会有一些(一对多,多对多)的映射关系,那么引入一个工具来帮助我们与数据库打交道就可以减轻一部分不必要的工作量,Sequelize就是其中比较受欢迎的一个。

Sequelize-typescript 是基于Sequelize 针对Typerscript 所实现的一个增强版本,抛弃了之前繁琐的模型定义,使用装饰器直接达到我们想到的目的,写法也与Java的注解十分相似。

npm i sequelize
npm i mysql2    # 以及对应的我们需要的数据库驱动
​
const Sequelize = require('Sequelize')
const sequelize = new Sequelize('mysql://root:jarvis@127.0.0.1:3306/ts_test') // dialect://username:password@host:port/db_name
​
npm i ts-node typescript
npm i sequelize reflect-metadata sequelize-typescript

其次,还需要修改tsconfig.json

{
  "compilerOptions": {
+   "experimentalDecorators": true,
+   "emitDecoratorMetadata": true
  }
}

理论都是枯燥的,我们来动手写一个案例就都明白了。

npm init
tsc --init
// 必选npm包
yarn add koa koa-router
yarn add @types/koa @types/koa-router
yarn add mysql2 // mysql驱动
yarn add koa-logger koa-static // 请求日志与静态文件
yarn add koa-router-ts // 路由控制器
yarn add moment // 时间格式化
yarn add typescript // typescript依赖
yarn add ts-node 
yarn add nodemon dotenv // 热启动依赖
yarn add sequelize-typescript sequelize // sequelize依赖
yarn add @types/node @types/validator reflect-metadata // 使用sequelize-typescript必须
yarn add koa-bodyparser koa2-cors // 跨域与post请求解析 
​
// 可选npm包
yarn add koa-json // json格式
yarn add @koa/multer multer // 实现文件上传安装
yarn add uuid // 随机id
npm install bcrypt  // 密码加密
npm install jsonwebtoken // 生成token
yarn add joi // 校验
​
日志中间件也可以使用
yarn add koa-pino-logger
// 配置tsconfig.json文件
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "ES6",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist"
  },
  "lib": ["es2021"]
}
// 创建并配置nodemon.json文件
{
  "watch": ["src"],
  "ignore": ["build", ".git", "node_modules"],
  "exec": "ts-node -r dotenv/config ./src/index.ts",
  "ext": "js,json,ts,tsx"
}
// 修改package.json文件配置script
"scripts": {
    "build": "tsc",
    "dev": "nodemon --watch",
    "start": "node ./dist/index.js",
    "test": "echo "Error: no test specified" && exit 1"
  },

在项目根目录下创建src文件夹,并分别创建其子文件夹controllersmodelsservicesutilsstaticmiddleware等文件夹以及index.ts文件

import Koa from "koa";
import statics from "koa-static"; // 静态文件
import { loadControllers } from "koa-router-ts"; // 引入路由控制器
import Logger from "koa-logger"; // 日志
import moment from "moment"; // 时间格式化
import bodyParser from "koa-bodyparser"; // post请求
import path from "path";
import cors from "koa2-cors"; // 跨域处理// init db models
import "./models"; // 加载数据库模板文件const PORT = process.env.PORT || 1829;
const staticPath = "./static";  // 静态文件地址
const logger = new Logger(str => { // 日志时间格式化处理
    console.log(moment().format("YYYY-MM-DD HH:mm:ss") + str);
});
​
// 实例化koa
const app = new Koa();
​
// 加载中间件
app.use(bodyParser());
app.use(logger);
app.use(statics(path.join(__dirname, staticPath)));
// 实例化路由
const router = loadControllers(path.join(__dirname, "controllers"), {
    recurse: true,
});
// 设置路由api前缀
router.prefix("/api/v1");
​
app.use(router.routes()).use(router.allowedMethods());
// 设置跨域
app.use(
    cors({
        origin: (ctx: any) => {
            if (ctx.url === "/test") {
                return "*";
            }
            return "http:localhost:8000"; // 允许http:localhost:8000请求跨域
        },
        maxAge: 5,
        credentials: true,
        allowMethods: ["GET", "POST", "PUT", "DELETE"],
        allowHeaders: ["Content-Type", "Authorization", "Accept"],
        exposeHeaders: ["WWW-Authenticate", "Server-Authorization"],
    })
);
​
app.listen(PORT);

首先,在models文件夹下创建index.ts文件,配置连接数据库

import { Sequelize } from "sequelize-typescript";
​
const sequelize = new Sequelize("mysql://root:root@127.0.0.1:3306/demo",{
    timezone: "+08:00",
    models: [`${__dirname}/**/*.model.ts`, `${__dirname}/**/*.model.js`], // 数据库模板存放地址
});
​
sequelize.sync({ force: false });
​
// 导出相应模块
export { sequelize };
export { Sequelize };
export default sequelize.models;

其次,创建相应的数据库模板(以user模板表为例),在models文件夹中创建user子文件夹,并在该文件夹下创建user.model.ts文件。

import {Column, DataType, HasMany, IsEmail, Length, Table, Unique} from 'sequelize-typescript';
import BaseModel from "../base/BaseModel";
​
@Table({ tableName: 'user' })
export default class User extends BaseModel {
    @Length({
        min: 2,
        max: 10,
        msg: "userName must between 2 to 10 characters"
    })
    @Column({
        type: DataType.STRING,
        comment: "用户名称"
    })
    userName: string;
​
    @Column({
        type: DataType.STRING,
        comment: "密码"
    })
    password: string;
}
class BaseModel extends Model {
    @PrimaryKey
    @AutoIncrement
    @Column({
        type: DataType.BIGINT,
        comment: "自增ID"
    })
    id: bigint;
​
    @CreatedAt
    @Column({
        type: DataType.DATE,
        comment: "创建时间"
    })
    createdAt: Date | null;
​
    @UpdatedAt
    @Column({
        type: DataType.DATE,
        comment: "修改时间"
    })
    updatedAt: Date | null;
​
    @DeletedAt
    @Column({
        type: DataType.DATE,
        comment: "删除时间"
    })
    deletedAt: Date | null;
}

这就形成了一个简单的User的基本骨架,User类继承了BaseModel类,BaseModel继承了Sequelize的Model类,拥有了Model.findAll()这一系列的方法。 可以直接在controller直接操作User模型执行查询语句。

const result = await sequelize.transaction(async (transaction) => {
  return await User.findAll({
    where: {
      id: user.id,
    },
    transaction
  });
});
const result = await sequelize.transaction(async (transaction) => {
  return await User.findByPk(id, {
    transaction,
  });
});

我们可以像这样操作数据库获取我们想要的数据,其实花括号里面还能配置很多其他参数,我认为最重要的有以下用法:

const result = await sequelize.transaction(async (transaction) => {
  return await User.findAll({
    include: ["bills", "labels"],
      attributes: [
        "userName",
        "phone",
        "email",
        "signature",
        "avatar",
      ],
      transaction,
  });
});

attributes 是返回的参数有哪些,如下图

image.png

也可以重命名返回的key 名称,用中括号括起来即可,就像这:

attributes: [["userName", "user-name"],"phone",],

这个可以用于返回过滤信息,比如不返回密码之类的敏感信息,默认attributes 括号内是include还能配置exclude 进行反向筛选。

Model.findAll({
  attributes: { exclude: ['password'] }
});

Sequlize 还有一个非常强大的功能,连表查询功能,有以下使用注意点:

  1. 注意关联名称,例如users在使用HashMany会生成createUser关联方法,对多就是复数,对一则为单数。
  2. 关联关系上方不允许添加@Column
  3. 加了关联关系的类才能使用对应的关联关系操作 没关联的类不能因为被关联而使用关联操作

userId 列上面加上@ForeignKey(() => User) 注解,在外键所在表加上@BelongsTo(() => User) 即可完成一对多。

@ForeignKey(() => User)
@Column({
    type: DataType.BIGINT,
    comment: "账单关联人ID"
})
userId: bigint;
​
@BelongsTo(() => User)
bill_user: User;

user 类上添加@HasMany(() => Bill) 即为多对一(多个bill对一个User)

@HasMany(() => Bill)
bills: Bill[];

在查询的时候,要使用多表联查需要这样写,include 里面的as写的是类里面的多对一或者一对多的属性名称,如上方的billslabels

const result = await sequelize.transaction( async (transaction) => {
    return await User.findByPk(id, {
        include: [{
            model: Bill,
            as: "bills"
        },{
            model: Label,
            as: "labels"
        }],
        attributes: [
            "userName",
            "phone",
            "email",
            "signature",
            "avatar",
        ],
        transaction,
    });
});
// 也可以这样写:include: ["bills", "labels"],

而连表查询后想要返回自定义response 则需要嵌套编写:

const users = await Bill.findAll({
    include: [{
        model: User,
        as: "bill_user",
        attributes: {
            exclude: [
                "password",
                "createdAt",
                "updatedAt",
                "deletedAt",
            ]
        }
    }, {
        model: Label,
        as: "bill_label",
        attributes: [
            "id",
            "name",
            "type",
            "userId"
        ],
        include: [{
            model: User,
            as: "label_user",
            attributes: ["userName"]
        }]
    }],
    attributes: {
        exclude: [
            "password",
            "createdAt",
            "updatedAt",
            "deletedAt",
        ]
    }
});
{
    "status": "ok",
    "message": "成功获取数据",
    "data": [
        {
            "id": 4,
            "payType": true,
            "amount": "100",
            "date": null,
            "labelId": 1,
            "labelName": null,
            "userId": 1,
            "remark": "mark",
            "bill_user": {
                "id": 1,
                "userName": "陈世杰",
                "phone": "18506613888",
                "email": null,
                "signature": null,
                "avatar": null
            },
            "bill_label": {
                "id": 1,
                "name": "测试标签",
                "type": "1",
                "userId": 1,
                "label_user": {
                    "userName": "陈世杰"
                }
            }
        }
    ]
}

也可以把exclude抽出来变成一个对象,然后替换即可:

const exclude = [
    "password",
    "createdAt",
    "updatedAt",
    "deletedAt",
];