Express+MySQL+Sequelize实作API

本文章为观看哔站视频 Express+MySQL+Sequelize实作API所作随笔,边敲代码便跟学。现发布只作为参考,有空再整理😀,同时也会补充在学习时自己的一些疑问及心得。
本人自学前端后初次涉猎nodejs,个人觉得此教程完备,适合初学者,推荐大家学习。

创建项目

  1. 创建项目

    # 安装 express-generator
    yarn global add  express-generator
    
    # 创建名为demo的项目
    express --no-view demo
    
    # 进入项目目录
    cd demo
    
    # 安装依赖包
    npm i
    
  2. 安装 nodemon,监听代码变动

    npm i nodemon
    

    ​ 修改package.json

    "scripts": {
      "start": "nodemon ./bin/www"
    },
    

    删除public/index.html后可以看见route/index.js下的JSON响应文段

    安装Docker

  3. 安装

Windows安装使用Docker http://t.csdnimg.cn/eNFqZ

使用WSL安装Ubuntu Linux系统教程http://t.csdnimg.cn/OIBsE

  1. 配置

    根目录创建docker-compose.yml文件,内容:

    version: '3.8'
    services:
      mysql:
        image: mysql:8.3.0
        command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
        environment:
          - MYSQL_ROOT_PASSWORD=xx(输入实际密码)
          - MYSQL_LOWER_CASE_TABLE_NAMES=0
        ports:
          - "3307:3306"
        volumes:
          - ./data/mysql:/var/lib/mysql
    

    运行docker-compose up -d命令,下载mysql并连接
    安装好后,如图:![外链图片转存失败,源站可能有防盗链在这里插入图片描述

    由于我安装了MySql占用了3306端口,所以上方ports改为"3307:3306"

  2. 客户端管理

    安装好后只是安装了MySql服务端,若想要更好管理客户端需要安装Navicat Premium 17破解版(http://t.csdnimg.cn/cN8Pu)

    (这样之后就还可以通过使用Navicat来操作数据库 )

创建数据库和数据表

1.如何创建数据库与数据表?
2.MySQL中常见的数据类型
3.主键是什么?
4.给数据表,增加标题和内容

  1. 连接docker以及navicat连接mysql
  2. 创建数据库、表、基础查询语句

使用Sequelize ORM

  1. 安装
npm i -g sequelize-cli
npm i sequelize mysql2
sequelize init

自动生成文件

  1. 配置环境(config.json),着重修改开发环境

    "development": {
        "username": "root",
        "password": "xx(输入实际密码)",
        "database": "clwy_api_development",
        "host": "127.0.0.1",
        "dialect": "mysql",
        "timezone": "+08:00",
        "port": 3307
      },
    
  2. 模型用于操作数据库

    删除原先Articles表后,执行下方命令,创建模型:

    sequelize model:generate --name Article --attributes title:string,content:text
    
  3. 迁移文件用来创建、修改表

    模型名字是单数:Article;表的名字一定是复数形式,也就是Articles

    对应数据库用单数,表用复数

    执行下方命令,可在数据库客户端看到表

    sequelize db:migrate
    

    注意需要config上添加port:“3307”;以及Navicat上需要已经创建好同名数据库

    之后就可在Navicat上看到Articles表了(另一个SequelizeMeta表是记录已经迁移的文件,下次迁移时就不会重复执行)

  4. 种子文件用于填充数据

    sequelize seed:generate --name article
    

    在seeders目录可见新建的种子文件

    up部分用来填充数据,down部分用来删除数据的

    参照默认案例代码,得到up部分

    async up (queryInterface, Sequelize) {
      const articles = [];
      const counts = 100;
       
      for (let i = 1; i <= counts; i++) {
        const article = {
          title: `文章的标题 ${i}`,
          content: `文章的内容 ${i}`,
          createdAt: new Date(),
          updatedAt: new Date(),
        };
       
        articles.push(article);
      }
       
      await queryInterface.bulkInsert('Articles', articles, {});
    },
       
    

    得到down部分

    async down (queryInterface, Sequelize) {
      await queryInterface.bulkDelete('Articles', null, {});
    }
    

运行种子

sequelize db:seed --seed xxx-article

(xxx-article这个是自己具体种子名)我的命令如下:

sequelize db:seed --seed 20240823095240-article

总结开发步骤:

步骤命令说明
第一步sequelize model:generate --name Article --attributes …建模型和迁移文件
第二步人工处理根据需求调整迁移文件
第三步sequelize db:migrate运行迁移,生成数据表
第四步sequelize seed:generate --name article新建种子文件
第五步人工处理将种子文件修改为自己想填充的数据
第六步sequelize db:seed --seed xxx-article运行种子文件,将数据填充到数据表中

这里的种子部分并不是必须的,但对于一些需要一下子插入大量数据的情况来说,使用种子还是非常方便的。我们可以根据实际的需求来灵活运用。

接口1:查询文章列表(GET)

routes文件夹下是我们的核心部分

  1. 编写路由,admin/articles,在app.js中配置好。接下来要访问数据库,使用模型:

    注意:await Article.findAll();返回的是promise,所以需要使用async…await

    下方代码除标准外 还加上了排序(逆序)、try catch

    const express = require('express');
    const router = express.Router();
    const { Article } = require('../../models');
    
    /* GET home page. */
    router.get('/', async function (req, res, next) {
        try {
            // 进行排序(逆序)
            const condition = { order: [['id', 'DESC']] };
            const articles = await Article.findAll(condition);
            res.json({
                status: true,
                message: '查询文章列表成功。',
                data: { articles }
            });
        } catch (error) {
            // 如果发生错误,返回错误信息
            res.status(500).json({
                status: false,
                message: '查询文章列表失败。',
                error: [error.message]
            });
        }
    });
    
    module.exports = router;
    
    

接口2:查询文章详情(GET)

  1. 在路由中添加id参数
  2. 如何查询文章详情?
  3. 查询不存在的数据,应该怎么办?
router.get('/:id', async function (req, res, next) {
    try {
        const article = await Article.findByPk(req.params.id);
        if (article) {
            res.json({
                status: true,
                message: '查询文章详情成功。',
                data: { article }
            })
        }else{
            res.status(404).json({
                status: false,
                message: '查询文章详情失败。',
                error: [`文章不存在。`]
            })
        }
    }
    catch (error) {
        res.status(500).json({
            status: false,
            message: '查询文章详情失败。',
            error: [error.message]
        })
    }
})

使用Apifox

①创建目录
②写入地址http://localhost:3000/admin/articles
③点击发送查看结果并保存
④前缀优化:配置开发环境前缀http://localhost:3000;api地址就为/admin/articles
⑤分享

接口3:创建文章(POST)

  1. POST请求是怎么回事?
  2. 如何使用Apifox发送POST请求?
  3. 如何获取到POST请求的数据,并存入数据库中?
  4. 200与201状态码的区别

注意点:

  • 接口地址和查询地址相同,但是是用POST
  • 获取表单数据,使用req.body
  • 通过Apifox提交表单,使用Body ->x-www-form-unlencoded
  • 创建完成后使用状态码 201 返回
router.post('/', async function (req, res, next) {
    try {
        const article = await Article.create(req.body);
        res.status(201).json({
            status: true,
            message: '创建文章成功。',
            data: { article }
        })
    }catch(error){
        res.status(500).json({
            status: false,
            message: '创建文章失败。',
            error: [error.message]
        })
    }
})

接口4:删除文章

  1. 什么是DELETE请求?
  2. 删除文章如何实现?
router.delete('/:id', async function (req, res, next) {
    try {
        const article = await Article.findByPk(req.params.id);
        if (article) {
            await article.destroy();
            res.json({
                status: true,
                message: '删除文章成功。'
                // data: { article }
            })
        }else{
            res.status(404).json({
                status: false,
                message: '删除文章失败。',
                error: [`文章不存在。`]
            })
        }
    }catch(error){
        res.status(500).json({
            status: false,
            message: '删除文章失败。',
            error: [error.message]
        })
    }
})

接口5:更新文章

  1. 什么是PUT请求
  2. 更新文章如何实现

注意:更新文章params传入id,body传入title和content

router.put('/:id', async function (req, res, next) {
    try {
        const article = await Article.findByPk(req.params.id);
        if (article) {
            await article.update(req.body);
            res.json({
                status: true,
                message: '更新文章成功。',
                data: { article }
            })
        }else{
            res.status(404).json({
                status: false,
                message: '更新文章失败。',
                error: [`文章不存在。`]
            })
        }
    }catch(error){
        res.status(500).json({
            status: false,
            message: '更新文章失败。',
            error: [error.message]
        })
    }
})

接口6:模糊搜索

1.如何搜索文章?
2.SQL语句中的like与%
3.req.query获取搜索参数值
4.Node.js中如何实现模糊搜索?

注意:

  • 此搜索近搜索标题单个参数,若多个参数则:/articles?title=标题 10&content=xxx
  • SQL代码:SELECT * FROM articles WHERE title LIKE '%特定关键字%';
  • 在接口1上更新,添加上查询内容
/* 
 * 接口1 - 查询文章列表(允许模糊查询) 
 * GET  /admin/articles?title=xxx
 * */
const { Op } = require('sequelize');//注意引入Op
router.get('/', async function (req, res, next) {
    try {
        const { title } = req.query;
        const condition = { order: [['id', 'DESC']] };

        if(title){
            condition.where = { title: { [Op.like]: `%${title}%` } };
        }
        const articles = await Article.findAll(condition);
        // 返回成功响应,包含文章列表
        res.json({
            status: true,
            message: '查询文章列表成功。',
            data: { articles }
        });
    } catch (error) {
        // 发生错误时返回错误响应
        res.status(500).json({
            status: false,
            message: '查询文章列表失败。',
            error: [error.message]
        });
    }
});

接口7:数据分页

1.分页是什么?
2.分页实现的原理与计算公式
3.在Node.js中如何实现分页?

  • SQL代码:SELECT* FROM Articles LIMIT offset,pageSize
  • api查询:/articles?pageSize=15&currentPage=2
  • 计算:offset = (currentPage - 1) * pageSize
  • 在接口1上更新
  • Math.abs是绝对值,Number是将字符串转换为数字
当前页数(current Page)从哪开始(offset)每页显示多少条(pageSize)
第1页010
/* 
 * 接口1 - 查询文章列表(允许模糊查询,分页查询若不传参则默认显示第一页10条) 
 * GET  /admin/articles?title=xxx
 * */
router.get('/', async function (req, res, next) {
    try {
        const query = req.query;
        // 当前是第几页,如果不传,那就是第一页(Math.abs是绝对值,Number是将字符串转换为数字)
        const currentPage = Math.abs(Number(query.currentPage)) || 1;

        // 每页显示多少条数据,如果不传,那就显示10条
        const pageSize = Math.abs(Number(query.pageSize)) || 10;

        const condition = {
            order: [['id', 'DESC']],
            limit: pageSize, //文档要求使用limit和offset
            offset: (currentPage - 1) * pageSize //计算出当前页的偏移量
        };

        if (query.title) {
            condition.where = { title: { [Op.like]: `%${query.title}%` } };
        }
        const { rows, count } = await Article.findAndCountAll(condition);//将findAll改为findAndCountAll,其返回的结果是一个对象,包含rows和count两个属性
        // 返回成功响应,包含文章列表
        res.json({
            status: true,
            message: '查询文章列表成功。',
            data: {
                articles: rows, //文章列表
                pagination: {
                    currentPage, //当前页码
                    pageSize, //每页显示多少条数据
                    total: count //总条数
                }
            }
        });
    } catch (error) {
        // 发生错误时返回错误响应
        res.status(500).json({
            status: false,
            message: '查询文章列表失败。',
            error: [error.message]
        });
    }
});

问题1:白名单过滤表单数据

  1. 存在问题:

    • 例子:订单表有商品编号和订单状态两个字段。若用户找到api地址,同时传送stauts:true,导致此订单为已支付。
    • 原因:使用的req.body会接收传送过来的所有数据。
  2. 解决办法:白名单过滤(强参数过滤)

    ​ 使用过滤后的body,而不是req.body

            // 使用白名单过滤
            const body={
                title:req.body.title,
                content:req.body.content
            } 
      const article = await Article.create(body); 
    

    优化:将过滤封装成函数,更新文章时也可使用

    function filterBody(req) {
        return {
            title: req.body.title,
            content: req.body.content
        }
    }
    
    /*
     * 接口5 - 更新文章
     * PUT /admin/articles/:id
     * */
    router.put('/:id', async function (req, res, next) {
        try {
            const article = await Article.findByPk(req.params.id);
            if (article) {
                const body = filterBody(req);
                await article.update(body);
                res.json({
                    status: true,
                    message: '更新文章成功。',
                    data: { article }
                })
            } else {
                res.status(404).json({
                    status: false,
                    message: '更新文章失败。',
                    error: [`文章不存在。`]
                })
            }
        } catch (error) {
            res.status(500).json({
                status: false,
                message: '更新文章失败。',
                error: [error.message]
            })
        }
    })
    
  3. 总结:

    1. 永远不要相信用户提交的数据
    2. 用户提交的数据一定要过滤

问题2:验证表单数据

  1. 问题:存在用户不填写数据或者填写数据不符合。

  2. 解决办法

    • 解决办法1:在路由中编写if语句进行校验。

      仍存在问题,导致路由代码繁多且不易复用

    • 解决办法2:①模型中添加校验,②接口catch(error)处反馈校验错误提示

      1. 在模型中添加校验(找到Article.init中title)
      title: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
              notNull: {
                msg: '标题必须存在。'
              },
              notEmpty: {
                msg: '标题不能为空。'
              },
              len: {
                args: [2, 45],
                msg: '标题长度需要在2 ~ 45个字符之间。'
              }
            }
          },
      
      1. 在接口的catch(error)处反馈校验错误提示

        catch (error) {
                if (error.name === 'SequelizeValidationError') //error.name为这个即为不符合模型中校验
                {
                    const errors = error.errors.map(e => e.message);//获取模型校验错误信息
                
                res.status(400).json({
                    status: false,
                    message: '请求参数错误。',
                    errors
                });
            }else {
                res.status(500).json({
                    status: false,
                    message: '创建文章失败。',
                    errors: [error.message]
                });
            }
        }
        

终极版:增删改查(封装响应,优化代码)

1.何为DRY原则?
2.如何自定义异常?
3.如何将重复的代码封装成函数?

  1. 编程届有句话叫做Done’t Repeat Yourself. 代码段中尽可能不要重复代码。
  2. 方法:
    • 定义一个公共方法用于查询文章
  1. 封装查询:创建一个公共方法(如getArticle)来查询当前文章,减少代码重复,无论在哪个地方需要查询,都可以直接调用。
  2. 异常处理:使用抛出异常的方式,如NotFoundError,在查询失败时,让调用者捕获异常,而不是在代码中直接处理。
  3. 定义异常类:为特定错误创建自定义类,如NotFoundError,提供自定义错误信息。
  4. 简化响应:定义函数如successfail,处理成功和失败的响应格式,统一状态码和提示信息。
  5. 清理重复代码:删除重复的查询、更新和删除文章的代码段,替换为调用封装好的方法。
  6. 错误处理函数:创建一个函数来处理不同类型的错误,如表单验证错误和找不到资源(404)错误,统一使用标准响应格式。
  7. 优化路由:使用封装好的函数替换路由中的具体逻辑,保持代码简洁和清晰。
  8. 检查和测试:确保优化后的代码能正常工作,并在API测试工具中验证接口。

utils/response.js文件:

  1. 自定义404错误
  2. 查询文章方法
  3. 响应函数success

这里是导入,是将模块化文件导入。所以单个文件需要依赖完整。所以在response.js文件中需要引用

const { Article } = require('../models');
// 引入Article模型
const { Article } = require('../models');
/**
 * 自定义404错误
 */
class NotFoundError extends Error {
    constructor(message) {
        super(message);
        this.name = 'NotFoundError';
    }
}
/**
 * 公共方法:查询当前文章
 */
async function getArticle(req) {
    const article = await Article.findByPk(req.params.id)
    if (!article) {
        throw new NotFoundError(`ID:${req.params.id}文章不存在。`);
    }
    return article;
}

/**
 * 请求成功success函数
 * @param res
 * @param message
 * @param data
 * @param code
 */
function success(res, message, data = {}, code = 200) {
    res.status(code).json({
        status: true,
        message,
        data
    })
}
/**
 * 请求失败failure函数
 * @param res
 * @param error
 */
function failure(res, error) {
    if (error.name === 'SequelizeValidationError') {
        const errors = error.errors.map(e => e.message);//获取校验错误信息
        return res.status(400).json({
            status: false,
            message: '请求参数错误。',
            errors
        });
    }
    else if (error.name === 'NotFoundError') {
        return res.status(404).json({
            status: false,
            message: '资源不存在',
            errors: [error.message]
        })
    }
    else {
        return res.status(500).json({
            status: false,
            message: '服务器错误。',
            errors: [error.message]
        });
    }
}

module.exports = {
    NotFoundError,
    getArticle,
    success,
    failure
}

route/admin/articles.js文件:

  1. 引入

    const {NotFoundError,getArticle} = require('../../utils/response');
    
  2. 并将查询文章详情、更新、删除进行代码修改: const article = await Article.findByPk(req.params.id);改为 const article = getArticle(req),将if else删去

  3. 将成功处(res.json代码段)改为使用success函数,将错误处catch (error) 内代码改为使用 failure(res, error)

  • 16
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个使用 Express.js、Sequelizemysql2 实现的简单 demo,将其拆分为多个模块: 1. 安装依赖 ``` npm install express sequelize mysql2 ``` 2. 创建数据库和数据表 ``` CREATE DATABASE IF NOT EXISTS demo; USE demo; CREATE TABLE IF NOT EXISTS users ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, email VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, PRIMARY KEY (id) ); ``` 3. 创建 Sequelize 模型 在 models/user.js 文件中: ```javascript const { DataTypes } = require('sequelize'); const sequelize = require('../config/database'); const User = sequelize.define('User', { name: { type: DataTypes.STRING, allowNull: false, }, email: { type: DataTypes.STRING, allowNull: false, }, password: { type: DataTypes.STRING, allowNull: false, }, }); module.exports = User; ``` 4. 创建 Express.js 应用 在 app.js 文件中: ```javascript const express = require('express'); const bodyParser = require('body-parser'); const userRoutes = require('./routes/userRoutes'); const app = express(); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); app.use('/users', userRoutes); app.listen(3000, () => console.log('Server running on port 3000')); ``` 5. 创建 Sequelize 数据库连接 在 config/database.js 文件中: ```javascript const { Sequelize } = require('sequelize'); const sequelize = new Sequelize('demo', 'root', 'password', { host: 'localhost', dialect: 'mysql', }); module.exports = sequelize; ``` 6. 创建用户路由 在 routes/userRoutes.js 文件中: ```javascript const express = require('express'); const router = express.Router(); const UserController = require('../controllers/UserController'); router.get('/', UserController.getAllUsers); router.get('/:id', UserController.getUserById); router.post('/', UserController.createUser); router.put('/:id', UserController.updateUser); router.delete('/:id', UserController.deleteUser); module.exports = router; ``` 7. 创建用户控制器 在 controllers/UserController.js 文件中: ```javascript const User = require('../models/User'); exports.getAllUsers = async (req, res) => { const users = await User.findAll(); res.json(users); }; exports.getUserById = async (req, res) => { const user = await User.findByPk(req.params.id); res.json(user); }; exports.createUser = async (req, res) => { const { name, email, password } = req.body; const user = await User.create({ name, email, password }); res.json(user); }; exports.updateUser = async (req, res) => { const { name, email, password } = req.body; await User.update({ name, email, password }, { where: { id: req.params.id } }); res.json({ message: 'User updated successfully.' }); }; exports.deleteUser = async (req, res) => { await User.destroy({ where: { id: req.params.id } }); res.json({ message: 'User deleted successfully.' }); }; ``` 现在,我们已经将应用拆分为多个模块,可以通过运行 `node app.js` 启动应用。然后可以使用 Postman 或其他工具测试 API
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值