Skip to content
On this page

实战篇 4-小程序列表获取 —— 使用 Sequelize


实战篇 4:小程序列表获取 —— 使用 Sequelize

上一节,我们的店铺 shops 表与商品 goods 表都已经通过 migrate 与 seed,完成了表结构的创建与数据的填充。本小节我们把知识点聚焦在 Sequelize 插件库本身的数据模型 model 的查询能力,来实现小程序店铺的列表查询与商品列表查询。

Sequelize 连接 MySQL 数据库

Sequelize 连接数据库的核心代码主要就是通过 new Sequelize(database, username, password, options) 来实现,其中 options 中的配置选项,除了最基础的 host 与 port、数据库类型外,还可以设置连接池的连接参数 pool,数据模型命名规范 underscored 等等。具体可以查阅官方手册 基础使用

由于前一小节通过 sequelize-cli init 初始化了 models 的目录,sequelize-cli 已经特别友好地为我们准备了一个动态加载 models 目录中具体数据库表模型的入口模块 index.js。由于我们希望遵循 MySQL 数据库表字段的下划线命名规范,所以,需要全局开启一个 underscore: true 的定义,来使系统中默认的 createdAt 与 updatedAt 能以下划线的方式,与表结构保持一致。

微调 models/index.js 中的代码:

// models/index.js

// 将 const config = configs[env] 调整为如下结构

const config = {
  ...configs[env],
  define: {
    underscored: true,
  },
};

定义数据库业务相关的 model

结合业务所需,我们在 models 目录下继续创建一系列的 model 来与数据库表结构做对应:

├── models                       # 数据库 model
│   ├── index.js                 # model 入口与连接
│   ├── goods.js                 # 商品表
│   ├── shops.js                 # 店铺表

1. 定义店铺的数据模型 shops

// models/shops.js

module.exports = (sequelize, DataTypes) => sequelize.define(
  'shops',
  {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true,
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    thumb_url: DataTypes.STRING,
  },
  {
    tableName: 'shops',
  },
);

2.定义商品的数据模型 goods

module.exports = (sequelize, DataTypes) => sequelize.define(
  'goods',
  {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true,
    },
    shop_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    thumb_url: DataTypes.STRING,
  },
  {
    tableName: 'goods',
  },
);

实现店铺列表页接口

1. 实现简单的店铺列表接口

// routes/shops.js

// 引入 models
const models = require("../models");
module.exports = [
  {
    method: 'GET',
    path: '/shops',
    handler: async (request, reply) => {
      // 通过 await 来异步查取数据
      const result = await models.shops.findAll();
      reply(result)
    }
  }
]

2. 隐藏返回列表中不需要的字段

很多时候,我们并不希望 findAll 来将数据表中的所有数据全都暴露出来,比如在查询用户列表时,用户的密码的值,便是特别敏感的数据。 我们可以在 findAll 中加入一个 attributes 的约束,可以是一个要查询的属性(字段)列表,或者是一个 key 为 includeexclude 对象的键,比如对于用户表,findAll({ attributes: { exclude: ['password'] } }),就可以排除密码字段的查询露出。

在我们的店铺表中,我们希望有如下的字段露出:


const result = await models.shops.findAll({
  attributes: [
    'id', 'name'
  ]
});

3. 列表分页

当列表的数据开始增多,每次的列表展示数据,并不需要拉取全部,这时,我们需要为系统框架引入分页插件,hapi-pagination。


# 安装适配 hapi v16 的 hapi-pagination
$ npm i hapi-pagination@1

在 plugins 目录下新增一个 hapi-pagination 的插件。options 的具体配置参数细节说明,参见 hapi-pagination

考虑到,并非所有的接口都需要支持分页,所以,笔者建议对需要分页的相关接口,在 routes.include 配置中,逐条添加:

// plugins/hapi-pagination.js
const hapiPagination = require('hapi-pagination');

const options = {
  query: {
    // ... 此处篇幅考虑省略 query 入参配置代码,参看章节  github 案例
  },
  meta: {
    name: 'meta',
    // ... 此处篇幅考虑省略 meta 的相关配置代码,参看章节  github 案例
  },
  results: {
    name: 'results'
  },
  reply: {
    paginate: 'paginate'
  },
  routes: {
    include: [
      '/shops'  // 店铺列表支持分页特性
    ],
    exclude: []
  }
}

module.exports = {
  register: hapiPagination,
  options: options,
}

在 app.js 中注册使用 hapi-pagination:

// app.js
const pluginHapiPagination = require('./plugins/hapi-pagination');
await server.register([
  pluginHapiPagination,
])

为 GET /shops 的接口添加分页的入参校验,同时更新 Swagger 文档的入参契约。考虑到系统中未来会有不少接口需要做分页处理,我们在 utils/router-helper.js 中,增加一个公共的分页入参校验配置:

// utils/router-helper.js
const paginationDefine = {
  limit: Joi.number().integer().min(1).default(10)
    .description('每页的条目数'),
  page: Joi.number().integer().min(1).default(1)
    .description('页码数'),
  pagination: Joi.boolean().description('是否开启分页,默认为true'),
}

module.exports = { paginationDefine }

最终,回到 router/shops.js,实现最后的分页配置逻辑。考虑到分页的查询功能除了拉取列表外,还要获取总条目数,Sequelize 为我们提供了 findAndCountAll 的 API,来为分页查询提供更高效的封装实现,返回的列表与总条数会分别存放在 rowscount 字段的对象中。

const { paginationDefine } = require('../utils/router-helper');
// ...省略上下文
{
  method: 'GET',
  path: `/${GROUP_NAME}`,
  handler: async (request, reply) => {
    const { rows: results, count: totalCount } = await models.shops.findAndCountAll({
      attributes: [
        'id',
        'name',
      ],
      limit: request.query.limit,
      offset: (request.query.page - 1) * request.query.limit,
    });
    // 开启分页的插件,返回的数据结构里,需要带上 result 与 totalCount 两个字段
    reply({ results, totalCount });
  },
  config: {
    tags: ['api', GROUP_NAME],
    auth: false,
    description: '获取店铺列表',
    validate: {
      query: {
        ...paginationDefine
      }
    }
  }
}
// ...省略上下文

通过 Swagger 文档工具 http://localhost:3000/documentation 查看店铺列表的接口调用返回数据,结果应该和下图相仿:

实现获取单个店铺的商品列表接口

根据传入的店铺 ID,查询特定店铺 ID 下的商品列表,此处使用到了 sequelize 的 where 条件查询。同时,我们为店铺商品列表也加入分页的特性。


// router/shops.js

const models = require("../models");
module.exports = [
  // ...省略上下文
  {
    method: 'GET',
    path: `/${GROUP_NAME}/{shopId}/goods`,
    handler: async (request, reply) => {
      // 增加带有 where 的条件查询
      const { rows: results, count: totalCount } = await models.goods.findAndCountAll({
        // 基于 shop_id 的条件查询
        where: {
          shop_id: request.params.shopId,
        },
        attributes: [
          'id',
          'name',
        ],
        limit: request.query.limit,
        offset: (request.query.page - 1) * request.query.limit,
      });
    },
  }
  // ...省略上下文
]

记得在 plugins/hapi-pagination.js 的 include 中加入 /shops/{shopId}/goods 的分页路由白名单。

// plugins/hapi-pagination.js
const options = {
  // ...
  routes: {
    include: [
      '/shops/goods',
      '/shops/{shopId}/goods',
    ],
    exclude: []
  }
  // ...
}

更多关于 models 的操作请查看官方手册 Model 使用

GitHub 参考代码 chapter8/hapi-tutorial-1

小结

关键词:Sequelize,Model定义,列表查询,分页

本小节,我们学习了如何使用 Sequelize 提供的数据库查询的方法,来获取列表数据。我们在大多数的使用场景下,都可以利用 Sequelize 简洁的函数式方法调用,避开直接拼写晦涩冗长的数据库查询语句,最终获取我们想要的业务数据查询结果。掌握 Sequelize 插件库,是 Node.js 下使用 MySQL 数据库的必修课。相关的使用手册文档,希望能经常温故而知新,掌握更多高级的使用技巧。

思考:我们发现实际的业务中,店铺的数量会很多,我们希望通过一些关键词的输入,来模糊查询匹配的店铺列表,路由该如何设计?Sequelize 该如何查询?

本小节参考代码汇总

Sequelize 连接 MySQL 数据库 - Sequelize 具体细节: 基础使用

实现店铺列表页接口 - 列表分页 - hapi-pagination 的 options 配置细节: hapi-pagination

实现获取单个店铺的商品列表接口:chapter8/hapi-tutorial-1

models 更多操作:Model 使用