创建一个 egg 项目并且支持数据库增删改查

创建一个 egg 项目

egg 中文官网 在官网中可以查到创建一个 egg 项目只需要三步命令行

mkdir egg-example && cd egg-example // 这一步甚至可以省略
npm init egg --type=simple
npm i

通过上面三步命令行就可以创建出一个 egg 项目了,然后通过 npm run dev 命令运行代码。

路由管理

为了方便项目的路由管理可以在 app 目录下面创建一个 router 目录,这里面专门放置路由文件。类似于这样的文件夹。并且在 router.js 文件中通过 require('./router/user')(app); 导入

image.png

Cors 设置

如果项目中有使用 post 方式去请求,并且还是通过 postman 形式的话,不设置 cors,他会报错。因此得设置一下 cors 这样才不会出错。

  1. 通过命令行 npm i -S egg-cors 安装 egg-cors 的包
  2. config/plugin.js 文件中添加以下代码
'use strict';

/** @type Egg.EggPlugin */ 
module.exports = {   cors: {
    enable: true,
    package: 'egg-cors'
  }
};
  1. 在 config/config.default.js 中添加以下代码,自此通过外部访问的 post 请求就可以正常访问了
/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  // 关闭csrf 开启跨域
  config.security = {
    csrf: {
      enable: false,
    },
    // 这个是添加白名单
    domainWhiteList: []
  } 

  config.cors = {
    origin: "*",
    allowMethods: 'GET, PUT, POST, DELETE, PATCH'
  }

  return {
    ...config,
    ...userConfig,
  };
};

Sequelize 设置

  1. 经过命令行 npm install --save-dev sequelize-cli 安装 sequelize-cli 工具
  2. 在项目根目录下面创建一个 .sequelizerc 文件,并且在这个文件中写入以下代码
'use strict';

const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};
  1. 安装 npm i -S egg-mysql egg-sequelize, 并且在 config/plugin.js和 config/config.default.js 两个文件中去配置
// config/plugin.js
'use strict';

/** @type Egg.EggPlugin */
module.exports = {
  mysql: {
    enable: true,
    package: 'egg-mysql',
  },
  sequelize: {
    enable: true,
    package: 'egg-sequelize',
  }
};

// config/config.default.js
/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  // sequelize 设置
  config.sequelize = {
    dialect: 'mysql',
    host: '127.0.0.1',
    port: 3306,
    username: 'root',
    password: 'admin123',
    timezone: '+8:00',
    database: 'eggapi',
    define: {
      // 取消数据表名复数
      freezeTableName: true,
      // 自动写入时间戳 created_at updated_at
      timestamps: true,
      // 自动生成软删除时间戳
      // paranoid: true,
      createdAt: 'created_at',
      updatedAt: 'updated_at',
      // deletedAt: 'deleted_at',
      // 所有驼峰命名格式化
      underscored: true
    }
  };

  return {
    ...config,
    ...userConfig,
  };
};
  1. 初始化 migrations 配置文件
  • npx sequelize init:config
  • npx sequelize init:migrations

通过上面这两个命令行会在根目录下创建一个 database 文件夹,并且会在 database 文件夹下创建一个 migrations 文件夹 和 一个 config.json 配置文件。

  1. 修改 /database/config.json 文件,将自己对应的数据库用户名,密码,表名全都写进去
{
  "development": {
    "username": "数据库的用户名",
    "password": "数据库的密码",
    "database": "数据库的表名",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "timezone": "+8:00"
  },
  "test": {
    "username": "root",
    "password": "admin123",
    "database": "eggapi",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "timezone": "+8:00"
  },
  "production": {
    "username": "root",
    "password": "admin123",
    "database": "eggapi",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "timezone": "+8:00"
  }
}
  1. 以上的配置项全都配置完成以后,接下来创建子表
  • 运行npx sequelize migration:generate --name=init-子表名字 命令行,会在 database/migrations 底下创建一个 migration 文件,文件格式是 一个时间戳+ ‘-init-’ + 子表名 (${timestamp}-init-users.js)
  • 在这个文件中去写入这个表对应的格式 ,代码如下
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, DATE, STRING, ENUM } = Sequelize;
    await queryInterface.createTable('user', {
      // UNSIGNED 这个属性是 无符号 的意思,也就是这个类型的值为非负数
      // primaryKey 这是主键的意思
      // auto_increment 自动递增
      // allowNull 是否允许为 null
      // defaultValue 默认值
      // comment 为字段或者列添加注释
      // unique 唯一标识
      // 如果 type 为 ENUM 枚举类型的话,那么得给 values 赋值上 数组选项
      id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true },
      username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true },
      password: { type: STRING(200), allowNull: false, defaultValue: '' },
      avatar_url: { type: STRING(200), allowNull: false, defaultValue: '' },
      sex: { type: ENUM, values: ['男', '女', '保密'], allowNull: false, defaultValue: '男', comment: '用户性别' }, 
      created_at: DATE,
      updated_at: DATE,
    });
  },
  // 在执行数据库降级时调用的函数,删除 user 表
  down: async (queryInterface) => {
    await queryInterface.dropTable('user');
  },
};
  1. 运行 npx sequelize db:create 来创建数据库表
  2. 运行 npx sequelize db:migrate 将 migrations 和 mysql进行关联

设置数据模型

  1. 在 app/model 下创建对应子表名的文件
  2. 在这个文件中写入以下代码,里面的代码其实跟在 migration 里面写的差不多,几乎是照搬过来的。自此数据模型搭建成功
module.exports = app => {
  const { STRING, INTEGER, DATE, ENUM } = app.Sequelize;

  const User = app.model.define('user', {
    id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true },
    username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true },
    password: { type: STRING(200), allowNull: false, defaultValue: '' },
    avatar_url: { type: STRING(200), allowNull: false, defaultValue: '' },
    sex: { type: ENUM, values: ['男', '女', '保密'], allowNull: false, defaultValue: '男', comment: '用户性别' }, 
    created_at: DATE,
    updated_at: DATE,
  });

  return User;
}

Sequelize 增删改查(CRUD)

  • 新增一个数据,创建数据模型以后,可以通过 this.app.model.User.create({username: '测试', password: '123456', sex: "男"}) 通过这个方法就能创建数据
async create() {
    const { ctx } = this;
    let res = await this.app.model.User.create({
      username: '测试2',
      password: '1234567',
      sex: "男"
    })
    ctx.body = {
      msg: 200,
      data: res
    }
}
  • 批量新增,可以通过 this.app.model.User.bulkCreate([]) 来批量新增数据,代码如下
async create() {
    const { ctx } = this;
    // 这里的 try catch 是因为在一开始设计子表的时候, username必须是唯一不能重复的,因此这里用 try catch 进行包裹一下
    let res = [];
    try {
      res = await this.app.model.User.bulkCreate([
        {
          username: '测试10',
          password: '1234567',
          sex: "男"
        },
        {
          username: '测试11',
          password: '1234567',
          sex: "男"
        },
        {
          username: '测试12',
          password: '1234567',
          sex: "男"
        },
        {
          username: '测试13',
          password: '1234567',
          sex: "男"
        },
        {
          username: '测试14',
          password: '1234567',
          sex: "男"
        },
      ])
    } catch(e) {
      res = {
        message: e.errors[0].message
      }
    }
    ctx.body = {
      msg: 200,
      data: res
    }
  }

2.1 查找单个

  • 通过主键的形式去查找 this.ctx.model.User.findByPk(主键id),也可以通过 this.app.model.User.findByPk(主键id) 这两个都是一样的效果,都会找到对应主键id的数据。
  • 通过 findOne 条件查找的方式去查,this.ctx.model.User.fineOne({where: {username: '测试'}}) 通过这个方法就可以查找到对应的数据。如果是用findOne的where形式去查找数据的话,有可能会查找到多条数据,他只会返回第一条数据。

2.2 查询所有数据

  • 可以通过 this.ctx.model.User.findAll() 查找所有的数据
  • 可以通过 this.ctx.model.User.findAndCountAll() 来查找所有数据,这个查找到的数据还有 count 数量
  • findAll 和 findAndCountAll 里面的配置都是一样的,可以通过 where 去查找,代码如下 并且
result = await ctx.model.User.findAndCountAll({
  where: {
    username: {
      [Op.like]: "%测试%"
    }
  }
}); 

sequelize 还提供了 Op可以用来模糊查询之类的方法,常用的方法有以下几种。

[Op.and]:{a:5}                // 且 a=5
[Op.or]:[{a:5},{a:6}]         // a=5或者a=6
[Op.gt]:6                     // id > 6
[Op.gte]:6                    // id >=6
[Op.lt]:10                    // id <6
[Op.lte]:10                   // id <=10
[Op.ne]:20                    // id != 20
[Op.eq]:3                     // =3
[Op.not]:true                 // 不是true
[Op.between]:[1,10]           // 在1和10之间,这个区间范围内全部选中
[Op.notBetween]:[1,10]        // 不在1和10之间
[Op.in]:[1,2]                 // 在  [1,2]之中,这几个全都选中
[Op.notIn]:[1,2]              // 不在[1,2]中
[Op.like]:'%adm'              // 模糊查询,包含 '%adm'
[Op.notLike]:'%adm'           // 不包含 '%adm'
[Op.iLike]:'%adm'             // 包含'%adm' (不区分大小写)(仅限PG)
[Op.notILike]:'%adm'          //不包含 '%adm'  (不区分大小写)(仅限PG)
[Op.startswith]:'adm'         // 类似 'adm%',以 'adm'开头
[Op.endswith]:'adm'           // 类似 '%adm',以adm结尾
[Op.substring]:'adm'          // 类似 '%adm%',包含有'adm'
[Op.regexp]:'^[a|d|m]'        // 匹配正则表达式  (仅限Mysql/PG)
[Op.notRegexp]: '^[a|d|m]'    // 不匹配正则表达式/!~ '^[a|d|m]' (仅限 MySQL/PG)
[Op.iRegexp]: '^[a|d|m]'      // ~* '^[a|d|m]' (仅限 PG)
[Op.notIRegexp]: '^[a|d|m]'   // !~* '^[a|d|m]' (仅限 PG)
[Op.like]: { [Op.any]: ['cat', 'hat']}   // 包含任何数组['cat', 'hat'] - 同样适用于 iLike 和 notLike
[Op.overlap]: [1, 2]          // && [1, 2] (PG数组重叠运算符)
[Op.contains]: [1, 2]         // @> [1, 2] (PG数组包含运算符)
[Op.contained]: [1, 2]        // <@ [1, 2] (PG数组包含于运算符)
[Op.any]: [2,3]               // 任何数组[2, 3]::INTEGER (仅限PG)
[Op.col]: 'user.id'           // 使用数据库语言特定的列标识符,其实就是子表对应的列
  • 只想返回一些字段的结果,比如说一条数据里面包含了 username, password, id, create_at,但是我并不想返回 password 返回给他,这里可以通过 attributes 字段来确认哪些字段返回,或者排除掉哪些字段返回
// 包含
async list() {
    let Op = this.app.Sequelize.Op;
    let res = await this.ctx.model.findAndCountAll({
        where: {
            [Op.like]: "%测试%"
        },
        // 这个情况是只返回数据里面有 username 和 id 的
        attributes: ["username", "id"] // [{username: '阿达', id: 1}]
    })
}

// 排除掉
async list() {
    let Op = this.app.Sequelize.Op;
    let res = await this.ctx.model.findAndCountAll({
        where: {
            [Op.like]: "%测试%"
        },
        // 这个情况是排除掉这些字段,只返回剩下来的字段
        attributes: {
            exclude: ["password", "created_at", "update_at"]
        } // [{username: '阿达', id: 1}]
    })
}
  • 查询排序,可以通过给 findAndCountAll 或者 findAll 里面通过添加order配置进行排序,order 接受一个二维数组,里面的每一项数组代表着一个字段的排序,越在上面的字段排序优先级越高
async list() {
    let Op = this.app.Sequelize.Op;
    let res = await this.ctx.model.findAndCountAll({
        where: {
            [Op.like]: "%测试%"
        },
        // 这个情况是排除掉这些字段,只返回剩下来的字段
        attributes: {
            exclude: ["password", "created_at", "update_at"]
        }, // [{username: '阿达', id: 1}]
        order: [
            ["id", "DESC"]
        ]
    })
}
// "DESC" 是倒序排序
// "ASC"  是升序排序
  • 分页,这里的分页跟平时我们前端所传的字段相同,offset 代表的是偏移量(也就是第几页),limit 代表的是一页几条数据。
async list() {
    let Op = this.app.Sequelize.Op;
    let { page, offset, limit } = this.ctx.query
    page = page && Number(page);
    offset = page - 1;
    limit = limit? Number(limit): 5
    let res = await this.ctx.model.findAndCountAll({
        where: {
            [Op.like]: "%测试%"
        },
        attributes: {
            exclude: ["password", "created_at", "update_at"]
        }, 
        order: [
            ["id", "DESC"]
        ],
        offset,
        limit
    })
}
  1. 改。修改某一条数据的话,得先通过findByPk,findOne去查找到对应的数据
  • 通过调用查找到的数据 .update() 方法可以进行更新,代码如下
async update() {
    const { ctx } = this;
    // 通过路径的后缀去获取对应的 id 类似于 http://xxx.com/api/update/1
    let id = ctx.params.id;
    // 获取请求体中的数据,也就是前端部分所传过来的数据
    let params = ctx.request.body;
    let data = await ctx.model.User.findByPk(id); // 这里直接通过主键调用,也可以通过findOne
    let res = await data.update(params); // 将前端传过来的数据进行更新
    /*
    *  可以给 update 传第二个参数, fields代表哪些字段可以被修改
    *  这里给 fields 传入 username 字段,代表 username 字段可以被修改,其他都不行
    *  data.update(params, { fields: ["username"] })
    */
    ctx.body = {
        msg: '200',
        data: res
    }
}
  • 查找到数据以后,得手动修改这条数据里面的值,在通过 save() 的形式去触发更新,但这个方式很麻烦也很不优雅,代码如下
async update() {
    const { ctx } = this;
    let id = ctx.params.id;
    let data = ctx.model.User.findByPk(id);
    // 通过这种手动赋值的方式来修改数据,一个字段还好,那要是多个字段或者不确定个数的字段的话,就很不优雅
    data.username = "测试01";
    let res = await data.save();
    /*
    *  跟上面一样 但是 save 的 fields 有个区别 如果我在代码中还修改了 password 的话,save 的返回值是被修改后的 password,但是在数据库里面,他并没有被修改
    *  data.save({ fields: ["username"] })
    */
    ctx.body = {
        msg: "200",
        data: res
    }
}
  • 单独删除一条数据,如果是只删一条数据的话,跟修改一样,得先通过查找对应的数据,然后在通过 destroy 的方法去删除
async destroy() {
    const { ctx } = this;
    let id = ctx.params.id;
    let data = await ctx.model.User.findByPk(id);
    if (!data) {
        // 如果没有对应的数据的话处理
    }
    let res = await data.destroy();
    ctx.body = {
        msg: '200',
        data: res
    }
}
  • 多条数据删除的话,其实也是调用 destroy 方法,但是不会再去查找了,而是通过 where 语句,用 sequelize 提供的Op属性来进行多个删除
async destroy() {
    const { ctx } = this;
    let Op = this.app.Sequelize.Op;
    let res = ctx.model.User.destroy({
        where: {
            id: {
                [Op.between]: [1, 10]   // 这个是选中在1-10这个区间的所有数据
                //[Op.in]: [1, 10] 这个是选中1和10的数据
            }
        }
    })
    
}

修改器

修改器名字听着高大上,其实就是在数据模型里面在某个字段里面去设置对应值的 set 属性,修改器中修改数据通常都是最后调用 setDataValue 去修改数据。

password: { 
  type: STRING(200), 
  allowNull: false, 
  defaultValue: '',
  // 这玩意就是修改器
  set(val) {
    let hash = val + Math.floor(Math.random() * 100);
    this.setDataValue('password', hash);
  }
},

获取器

获取器其实也是修改数据模型中某个字段对应的 get 属性,获取器最后如果要修改值的话,那她最后要调用 getDataValue 去获取值,然后把值给 return 出去

created_at: {
  type: DATE,
  get() {
    // 就是通过 getDataValue() 去获取对应字段的值
    let val = this.getDataValue('created_at')
    return (new Date(val)).getTime();
  }
},

获取器和修改器的区别

  1. 获取器并不影响原本数据库里面的数据,修改器会影响数据库里面的数据。
  2. 获取器一般是查的时候用,修改器一般是更新,新增的时候会用。

统一的错误处理

在 egg 里面可以通过在 app 目录下面创建一个 middleware 目录,这个目录主要是放置一些中间件的,我们可以把错误处理放在这里面,并且如果在 middleware 目录下创建完实例以后,还得去 config/config.default.js 文件中去进行配置。代码如下

// middleware/error_handler.js
module.exports = () => {
    return async function errorHandler(ctx, next) {
        try {
            await next();
        } catch(e) {
            ctx.status = e.status;
            ctx.body = {
                msg: e.status,
                data: e.message
            }
        }
    }
}

// config/config.default.js
config.middleware = ["errorHandler"]

经过以上两个文件的配置就可以捕捉到错误了

中间件的配置

在上面的统一错误处理中我们用到了中间件,如果我只想匹配一段路由,那么这个时候就涉及到了中间件的配置。

config.errorHandler = {
    enable: true, // 是否开启这个中间件,默认为 true 开启
    match: '/user/list', // 匹配这段路由 match 接受数组,字符串和函数,数组的话就是在数组中的这几段路由都是符合条件的,函数的话可以通过正则去校验路径
    ignore: 'user/create' // 除了这个路径 其他都匹配
}

参数验证

下载一个插件 egg-valparams,命令行是 npm i egg-valparams --save,安装完成以后得分别在 config/plugin.js 和 config/config.default.js 文件中去修改配置

// config/plugin.js
valparams: {
    enable : true,
    package: 'egg-valparams'
}

// config/config.default.js
config.valparams = {
    locale: 'zh-cn',
    throwError: true
 };
 
 // 在代码中去用
 async create() {
     const { ctx } = this;
     let params = ctx.request.body;
     ctx.validate({
         username: {
             type: "string", //类型
             required: true, // 是否必填
             desc: "用户名称", // 描述
             defValue: ""     // 默认值
         }
     })
     let res = ctx.model.User.create(params);
     ctx.body = {
         msg: "200",
         data: res
     }
 }

自此一个基本的 egg 项目搭建成功!!!

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容