为什么选择 mysql

市面上流行的数据库很多,在稳定性、性能、拓展性等方面有各自的优势,按照数据组织方式区分与两类

  1. 关系型数据库:由二维表及其之间的联系组织数据
    1. mysql/mariadb
    2. Oracle
    3. SQL Server
    4. DB2
    5. PostgreSQL
  2. NoSQL:利用文档、键值对等对数据进行组织
    1. mongoDB
    2. Hypertable
    3. Hbase
    4. redis/memcache
    5. CouchDB
    6. Neo4j

两者在不同领域都有广泛的应用,并且在互相学习,但关系型数据是理解数据库的基础,因为先发优势在各企业应用中普及度比 NoSQL 高很多,mysql 免费、稳定、高性能的特性让其成为最流行的关系型数据库

mysql 安装

mysql 程序可以在官网下载安装,Mac 用户可以使用 homebrew 安装

  1. brew install mysql

安装完成后可以在命令行工具启动、操作 mysql 数据库,基础操作可以参考 MySQL入门——看这一篇就够了,但命令行操作比较复杂,可以使用图形化工具管理 mysql 数据库,对于简单的管理 mysql 官方提供的 workbenchphpMyAdmin 等选择很多

环境准备

  1. 使用 root 登录,创建一个普通用户 sunluyong ,赋予用户操作数据库的权限(一般不会在程序中使用 root 账户)
  2. 使用普通账户登录,创建 schema(数据库) demo ,后面的示例需要连接此数据库
  3. 在 demo 数据库中创建 Table user
    1. CREATE TABLE `user` (
    2. `id` int NOT NULL AUTO_INCREMENT COMMENT ''用户 ID,主键'',
    3. `name` varchar(50) NOT NULL COMMENT ''用户名称'',
    4. `config` tinytext COMMENT ''用户配置'',
    5. `deleted` int unsigned NOT NULL DEFAULT ''0'' COMMENT ''记录是否被删除'',
    6. `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''创建时间'',
    7. `updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT ''最后修改时间'',
    8. PRIMARY KEY (`id`)
    9. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    user 表的结构非常简单
id 自增长数字,用户 ID,做为表的主键
name 用户名
config 用户的一些配置信息,可以为空
deleted 该条记录是否被软删除,0: 未删除 1: 已删除
created_at 本条数据的创建时间,数据库自动插入 CURRENT_TIMESTAMP
updated_at 最后一次修改时间,每次该条数据 update 时候数据库自动更新

环境准备好后就可以在 egg.js 中连接数据库,进行数据的 CRUD 操作了

配置插件

在 egg.js 中操作 mysql 需要使用 egg-mysql 插件,配置方式和模板引擎插件的方式一致

1. 安装插件

  1. npm i --save egg-mysql

2. 开启插件

修改 config/plugin.js

  1. exports.mysql = {
  2. enable: true,
  3. package: 'egg-mysql',
  4. };

3. 数据库连接配置

修改 config/config.default.js

  1. config.mysql = {
  2. client: {
  3. host: 'localhost',
  4. port: '3306',
  5. user: 'sunluyong',
  6. password: '123456',
  7. database: 'demo',
  8. },
  9. // 在 this.app 上挂载 mysql 对象
  10. app: true,
  11. };

在代码中写明文密码是极度危险的处理方式,直接配置是为了 demo 方便,线上需要使用加密手段

多环境配置

完成上面的配置就可以在本地测试了,但很多时候本地测试数据库和线上数据库并不一致,egg.js 通过 /config 目录下不同文件名的方式提供了多环境配置的支持

  1. config
  2. |- config.default.js
  3. |- config.prod.js
  4. |- config.unittest.js
  5. |- config.local.js

config.default.js 为默认的配置文件,所有环境都会加载这个配置文件,一般也会作为开发环境的默认配置文件

指定 env 时会同时加载对应的配置文件,并覆盖默认配置文件的同名配置。如 prod 环境会加载 config.prod.jsconfig.default.js 文件,config.prod.js 会覆盖 config.default.js 的同名配置

因此可以创建 /config/config.prod.js 对线上环境进行特殊配置,在 egg.js 启动时候可以通过 EGG_SERVER_ENV 自定义环境

CRUD

CRUD 是 Create、Read、Update、Delete 的缩写,是数据库最常用的四种操作,egg-mysql 对应的操作也非常简单

  1. this.app.mysql.insert(表名, 数据对象)
  2. this.app.mysql.get(表名, where 条件 })
  3. await this.app.mysql.query(sql 语句, [sql 语句中的变量])
  4. this.app.mysql.update(表名, 更新字段对象, where 条件)
  5. this.app.mysql.delete(表名, where 条件)

看一下官网示例

  1. // 插入
  2. await this.app.mysql.insert('posts', { title: 'Hello World' });
  3. => INSERT INTO `posts` (`title`) VALUES('Hello World');
  4. // 查询
  5. await this.app.mysql.get('posts', { id: 12 });
  6. => SELECT * FROM `posts` WHERE `id` = 12;
  7. // 自定义 SQL 语句
  8. await this.app.mysql.query('update posts set hits = (hits + ?) where id = ?', [1, postId]);
  9. => update posts set hits = (hits + 1) where id = 1;
  10. // 删除
  11. await this.app.mysql.delete('posts', { author: 'fengmk2'});
  12. => DELETE FROM `posts` WHERE `author` = 'fengmk2';
  13. // 更新
  14. const row = {
  15. name: 'fengmk2',
  16. otherField: 'other field value', // any other fields u want to update
  17. };
  18. const options = {
  19. where: {
  20. id: 456
  21. }
  22. };
  23. await this.app.mysql.update('posts', row, options);
  24. => UPDATE `posts`
  25. SET `name` = 'fengmk2', `otherField` = 'other field value'
  26. WHERE id = 456 ;
  27. // 因为大部分数据库的主键 key 都是 id,所以更新有个快捷的写法
  28. // 只要第一个参数里面包含 `id` 并且不提供第二个参数,就会默认把 id 当做 where 的条件
  29. const row = {
  30. id: 123,
  31. name: 'fengmk2',
  32. otherField: 'other field value', // any other fields u want to update
  33. };
  34. await this.app.mysql.update('posts', row);

示例

接下来写一个用户管理的示例展示

  1. 接口使用 RESTful 风格,egg.js 提供了内置的 RESRful API 实现方式,为了完整演示,示例自定义实现
  2. 非 get 请求需要处理 csrf token,防止 csrf 攻击,细节可以参考 egg.js 安全

    添加路由

    在示例中会使用到 user 相关的 5 个接口和 3 个页面,在 app/router.js 文件中添加路由

    1. // user api
    2. app.get('/api/user/list', 'api.user.list');
    3. app.get('/api/user/:id', 'api.user.get');
    4. app.post('/api/user', 'api.user.create');
    5. app.put('/api/user/:id', 'api.user.edit');
    6. app.delete('/api/user/:id', 'api.user.delete');
    7. // user page
    8. app.get('/user/create', 'user.create');
    9. app.get('/user/edit/:id', 'user.edit');
    10. app.get('/user/list', 'user.list');

    https://github.com/Samaritan89/egg-demo/blob/v2/app/router.js#L7

    创建 controller、service、view 文件

    根据路由创建对应的文件

  • app/controller/api/user.js
  • app/controller/user.js
  • app/service/user.js
  • app/view/user/create.hbs
  • app/view/user/edit.hbs
  • app/view/user/list.hbs

    创建用户功能实现

    1. 创建用户视图

    app/view/user/create.hbs 核心代码
    1. <form method="POST" action="/api/user?_csrf={{csrf}}" enctype="application/x-www-form-urlencoded">
    2. <div class="form-row">
    3. <label>用户名:</label>
    4. <input type="text" name="name" />
    5. </div>
    6. <div class="form-row">
    7. <label>用户配置:</label>
    8. <textarea name="config"></textarea>
    9. </div>
    10. <div class="form-row">
    11. <input type="submit" value="创建用户" />
    12. </div>
    13. </form>

    需要在 form action url 参数添加 _csrf 参数

完整代码:https://github.com/Samaritan89/egg-demo/blob/v2/app/view/user/create.hbs

2. 页面 controller

app/controller/user.js 添加 create 方法响应视图

  1. async create() {
  2. await this.ctx.render('user/create', {
  3. csrf: this.ctx.csrf,
  4. });
  5. }

完整代码:https://github.com/Samaritan89/egg-demo/blob/v2/app/controller/user.js#L6

3. 创建用户 api controller

app/controller/api/user.js 添加方法 create

  1. async create() {
  2. const { name, config } = this.ctx.request.body;
  3. const result = await this.service.user.insert({
  4. name,
  5. config,
  6. });
  7. this.ctx.body = {
  8. userId: result.insertId,
  9. };
  10. }

完整代码:https://github.com/Samaritan89/egg-demo/blob/v2/app/controller/api/user.js#L26

4. 创建用户 service

app/service/user.js 添加方法 insert

  1. async insert(user) {
  2. const result = await this.app.mysql.insert('user', user);
  3. return result;
  4. }

完整代码:https://github.com/Samaritan89/egg-demo/blob/v2/app/service/user.js#L33

这样就可以添加用户了,其它集中操作非常类似,不再重复介绍,感兴趣可以看看获取用户列表的 service,稍微复杂一些

  1. async list(pageSize, pageNo, orderBy = 'id', order = 'ASC') {
  2. let sql = 'select id, name, config from user ';
  3. sql += 'where deleted = 0 ';
  4. sql += 'order by ? ? ';
  5. sql += 'limit ?,?;';
  6. const offset = pageSize * (pageNo - 1);
  7. const users = await this.app.mysql.query(sql, [ orderBy, order, offset, pageSize ]);
  8. const totalNumRow = await this.app.mysql.query('select count(id) as totalNum from user where deleted = 0;');
  9. return {
  10. users,
  11. pages: {
  12. pageNo,
  13. pageSize,
  14. total: totalNumRow[0].totalNum,
  15. },
  16. };
  17. }

完整示例:https://github.com/Samaritan89/egg-demo/blob/v2/app/service/user.js#L11

事务

很多时候做一个业务操作需要对数据库中多个表做处理,但其中一个的操作出现错误就需要把所有操作返回,保证数据的准确性,这就是数据库中【事务】的概念
对于一个事务来讲,主要有 beginTransaction、commit 或 rollback,分别代表事务的开始,成功和失败回滚,

egg-mysql 提供了两种类型的事务:

手工操作

  1. const conn = await app.mysql.beginTransaction(); // 初始化事务
  2. try {
  3. await conn.insert(table, row1); // 第一步操作
  4. await conn.update(table, row2); // 第二步操作
  5. await conn.commit(); // 提交事务
  6. } catch (err) {
  7. // error, rollback
  8. await conn.rollback(); // 一定记得捕获异常后回滚事务!!
  9. throw err;
  10. }

自动控制

  1. const result = await app.mysql.beginTransactionScope(async conn => {
  2. // 不要自定 commit 或者回滚,出现错误时候回自动处理
  3. await conn.insert(table, row1);
  4. await conn.update(table, row2);
  5. return { success: true };
  6. }, ctx); // ctx 是当前请求的上下文,如果是在 service 文件中,可以从 `this.ctx` 获取到

在复杂场景合理使用事务能够让业务系统更加稳定、可靠

  1. MySQL 使用 138 张图带你 MySQL 入门