一般持久化数据的方法

  • 文件系统fs
  • 数据库
    • 关系型数据库 - mysql,Oracle
    • 文档型数据库 - mongodb
    • 键值对数据库 - redis

使用文件系统存储数据

node 命令行程序,操作文件数据:读取,吸入

  1. // db.json 初始内容为 {}
  2. // index.js
  3. const fs = require('fs')
  4. // 获取某个key的值
  5. function get(key) {
  6. fs.readFile('./db.json', (err, data) => {
  7. const json = data ? JSON.parse(data) : {}
  8. console.log(json[key])
  9. })
  10. }
  11. // 设置键值对值
  12. function set(key, value) {
  13. fs.readFile('./db.json', (err, data) => {
  14. // 如果文件为空,则设置空对象ss
  15. const json = data ? JSON.parse(data) : {}
  16. json[key] = value
  17. fs.writeFile('./db.json', JSON.stringify(json), err => {
  18. err && console.log(err)
  19. console.log('写入成功')
  20. })
  21. })
  22. }
  23. // 命令行操作支持
  24. const readline = require('readline')
  25. const rl = readline.createInterface({
  26. input: process.stdin,
  27. output: process.stdout
  28. })
  29. rl.on('line', input => {
  30. const [op, key, value] = input.split(' ')
  31. if (op === 'get') {
  32. get(key)
  33. } else if(op === 'set') {
  34. set(key, value)
  35. } else if (op === 'quit') {
  36. rl.close()
  37. } else {
  38. console.log('没有该操作')
  39. }
  40. })
  41. rl.on('close', () => {
  42. console.log('程序结束')
  43. process.exit(0)
  44. })

nodemon 运行后,在命令行里 set a 1,就可以向文件写入值,然后 get a就可以得到值

mysql模块

使用mysql模块连接并操作数据表

  1. const mysql = require('mysql')
  2. // 连接配置
  3. const cfg = {
  4. host: 'localhost',
  5. user: 'root',
  6. password: 'test',
  7. database: 'test'
  8. }
  9. // 创建连接对象
  10. const conn = mysql.createConnection(cfg)
  11. // 连接
  12. conn.connect(err => {
  13. if (err) {
  14. console.log(err.message)
  15. throw err
  16. } else {
  17. console.log('连接成功!')
  18. }
  19. })
  20. // 执行mysql语句 conn.query()
  21. const CREATE_SQL = `
  22. create table if not exists tb_test (
  23. id int not null auto_increment,
  24. message varchar(50) null,
  25. primary key (id)
  26. )
  27. `
  28. const INSERT_SQL = `insert into tb_test(message) values(?)`
  29. const SELECT_SQL = `select * from tb_test`
  30. // 创建表
  31. conn.query(CREATE_SQL, (err, data) => {
  32. if (err) {
  33. throw err
  34. }
  35. // 没有报错,说明创建表成功, 没有特别的返回信息
  36. console.log(data)
  37. })
  38. // 插入数据
  39. conn.query(INSERT_SQL, 'test', (err, data) => {
  40. if (err) {
  41. throw err
  42. }
  43. console.log(data) // data.insertId 为插入数据的id
  44. })
  45. // 查询数据
  46. conn.query(SELECT_SQL, (err, data) => {
  47. if (err) {
  48. throw err
  49. }
  50. console.log(data)
  51. // console.log(data[0].id) // 1
  52. })
  53. // [ RowDataPacket { id: 1, message: 'test' },
  54. // RowDataPacket { id: 2, message: 'test' },
  55. // RowDataPacket { id: 3, message: 'test' },
  56. // RowDataPacket { id: 4, message: 'test' } ]

mysql2模块(ES2017写法)

node-mysql2 github地址

  1. # 安装mysql2模块
  2. npm install mysql2 --save

操作数据库demo

  1. (async () => {
  2. try {
  3. const mysql = require('mysql2/promise')
  4. // 连接配置
  5. const cfg = {
  6. host: 'localhost',
  7. user: 'root',
  8. password: 'xx',
  9. database: 'test'
  10. }
  11. // 创建连接
  12. let connection = await mysql.createConnection(cfg)
  13. // 执行mysql语句 conn.execute()
  14. const CREATE_SQL = `
  15. create table if not exists tb_test (
  16. id int not null auto_increment,
  17. message varchar(50) null,
  18. primary key (id)
  19. )
  20. `
  21. const INSERT_SQL = `insert into tb_test(message) values(?)`
  22. const SELECT_SQL = `select * from tb_test`
  23. let ret = await connection.execute(CREATE_SQL)
  24. console.log(ret)
  25. ret = await connection.execute(INSERT_SQL, ['abc'])
  26. console.log(ret) // ret.insertId
  27. let [rows, fields] = await connection.execute(SELECT_SQL)
  28. console.log(rows)
  29. } catch(e) {
  30. console.log(e)
  31. }
  32. })()

Node.js ORM - Sequelize

sequelize 基于Promise的ORM(Object Relation Mapping),支持多种数据库、事务、关联等

使用ORM好处

  1. 支持多种数据库 ‘mysql’ | ‘mariadb’ | ‘postgres’ | ‘mssql’ | ‘sqlite’
  2. 可以不用手写sql语句
  3. 以面向对象、模块化的方式来操作数据库,不用自己手动创建操作数据库的类
  1. # 安装
  2. npm install mysql2 sequelize -s

增删查改demo

使用面向对象的方式,不写sql来对数据库进行增删查改

  1. // index.js
  2. (async () => {
  3. try {
  4. const Sequelize = require('sequelize')
  5. // 建立连接
  6. // 参数分别为: database, username, password, config
  7. const sequelize = new Sequelize('test', 'root', '1234567Abc,.', {
  8. host: 'localhost',
  9. dialect: 'mysql' // 'mysql' | 'mariadb' | 'postgres' | 'mssql' 之一
  10. })
  11. // 方法2: 传递连接 URI
  12. // const sequelize = new Sequelize('postgres://user:pass@example.com:5432/dbname');
  13. // 测试连接,使用 .authenticate() 函数来测试连接
  14. await sequelize.authenticate() // 如果连接异常,会走catch的逻辑
  15. // 创建表模型
  16. // public define(modelName: string, attributes: Object, options: Object): Model
  17. // 使用下面的模型创建表时,会默认加上3个字段 主键id, createdAt, updatedAt
  18. const Fruit = sequelize.define('fruit', {
  19. name: { type: Sequelize.STRING(20), allowNull: false },
  20. price: { type: Sequelize.FLOAT, allowNull: false },
  21. stock: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }
  22. }, {
  23. // conifg
  24. timestamps: false, // 默认值为true,如果为true会加上createdAt, updatedAt字段
  25. // freezeTableName: true // 默认为false, 默认情况下会为表名添加一个s,即 fruits,设置为true可以阻止这一默认行为
  26. })
  27. // 创建表:将模型同步到数据库
  28. let ret = await Fruit.sync() // 如果表不存在则同步,否则不处理
  29. // let ret = await Fruit.sync({force: true}) // 创建之前,先删除原来的表
  30. console.log(ret) // 返回 fruit
  31. // 插入数据(增)
  32. ret = await Fruit.create({
  33. name: "香蕉",
  34. price: 3.5,
  35. // test: 1 会根据模型来,就算这加了额外的数据,也不会插入到表
  36. })
  37. console.log(ret.toJSON()) // 对象里面包含插入的数据行,包含id
  38. // 查询所有数据(查)
  39. ret = await Fruit.findAll()
  40. console.log(ret.length)
  41. console.log(JSON.stringify(ret)) // 如果不stringify,打印的都是对象
  42. // 更新 (改)
  43. // ret = await Fruit.update({ price: 8 }) // 如果没有where会报错
  44. // 文档 https://demopark.github.io/sequelize-docs-Zh-CN/querying.html
  45. ret = await Fruit.update({ price: 8 }, {
  46. where: {
  47. price: { [Sequelize.Op.lt]: 2, [Sequelize.Op.gt]: 0 } // price > 0 and price < 2
  48. }
  49. })
  50. console.log(ret[0]) // 修改影响行数
  51. // 删除
  52. ret = await Fruit.destroy({
  53. where: {
  54. price: { [Sequelize.Op.lt]: 4, [Sequelize.Op.gt]: 0 } // price > 0 and price < 4
  55. }
  56. })
  57. console.log(ret) // 删除行数
  58. } catch(e) {
  59. console.log('error message: ', e.message)
  60. }
  61. })()

自增id的缺点与UUID

一般数据比较大时,会分表,如果自增id,相对不同表的自增,id会乱,UUID就派上用场了。使用随机生成的UUID格式

  1. const Fruit = sequelize.define('fruit', {
  2. // 指定id属性
  3. id: {
  4. type: Sequelize.DataTypes.UUID,
  5. defaultValue: Sequelize.DataTypes.UUIDV1,
  6. primaryKey: true
  7. },
  8. // UUID: dba3d770-36c0-11ea-bb43-9502bdef4ee8
  9. name: { type: Sequelize.STRING(20), allowNull: false },
  10. price: { type: Sequelize.FLOAT, allowNull: false },
  11. stock: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }
  12. }, {
  13. // conifg
  14. timestamps: false, // 默认值为true,如果为true会加上createdAt, updatedAt字段
  15. // freezeTableName: true // 默认为false, 默认情况下会为表名添加一个s,即 fruits,设置为true可以阻止这一默认行为
  16. tableName: 'tb_fruit' // 指定表名
  17. })
  18. let ret = await Fruit.sync({force: true}) // 创建之前,先删除原来的表,会删除原来的互数据

其他

关于sequelize,大多是api调用,api可以直接看文档,有机会整理下完整的文档

  • 在define模型时,除了type外,还可以加validate校验, 在update、create、save时会做校验。相关文档
  • Getters & Setters:可用于定义伪属性或映射到数据库字段的保护属性javascript name: { type: Sequelize.STRING, allowNull: false, get() { const fname = this.getDataValue("name"); const price = this.getDataValue("price"); const stock = this.getDataValue("stock"); return `${fname}(价格:¥${price} 库存:${stock}kg)`; } }

  • 查询相关```javascript // 通过属性查询 Fruit.findOne({ where: { name: “香蕉” } }).then(fruit => { // fruit是首个匹配项,若没有则为null console.log(fruit.get()); });

// 指定查询字段 Fruit.findOne({ attributes: [‘name’] }).then(fruit => { // fruit是首个匹配项,若没有则为null console.log(fruit.get()); });

// 获取数据和总条数 Fruit.findAndCountAll().then(result => { console.log(result.count); console.log(result.rows.length); });

// 分页 Fruit.findAll({ offset: 0, limit: 2, })

// 排序 Fruit.findAll({ order: [[‘price’, ‘DESC’]], })

// 聚合 Fruit.max(“price”).then(max => { console.log(“max”, max); }); Fruit.sum(“price”).then(sum => { console.log(“sum”, sum); });

  1. - 更新与删除```javascript
  2. // 更新
  3. Fruit.findById(1).then(fruit => { // 方式1
  4. fruit.price = 4;
  5. fruit.save().then(()=>console.log('update!!!!'));
  6. });
  7. // 方式2
  8. Fruit.update({price:4}, {where:{id:1}}).then(r => {
  9. console.log(r);
  10. console.log('update!!!!')
  11. })
  12. // 删除
  13. // 方式1
  14. Fruit.findOne({ where: { id: 1 } }).then(r => r.destroy());
  15. // 方式2
  16. Fruit.destroy({ where: { id: 1 } }).then(r => console.log(r));

ERD 实体关系图

参考:什么是实体关系图(ERD)? 转

关系型数据库在规划设计表时,都会先画ER图,ER指的是entity related。实体一般对应一个表。表与表之间的或多或少存在关联。一般主键、外键就是用来描述关联的。比如:制造商表与产品表,一条产品数据会包含制造商id,这就是关联。

想几个问题:

  • 当删除某个制造商时,产品表里,对应制造商的产品数据怎么办?客户订单表里,包含这些产品的订单数据怎么处理?
  • 如果我们在用户表里引用了部门名称,部门表里部门名称修改了,用户表里怎么处理?

当修改和删除操作时,与之关联的可能都会有影响。怎么处理好这些关联呢,这就需要画ER图,合理的规划表结构,外键约束等。sequelize 关联文档

TODO: 待学完 数据库系统原理 相关知识再深入理解这一块

Restful API

Rest是Representational State Transfer的缩写,”表现层状态转换”

  • representation 表现 我们把”资源”具体呈现出来的形式,叫做它的”表现层”(Representation)。 每一个URL描述了一种资源
  • State transfer 状态转化 如果客户端想要操作服务器,必须通过某种手段,让服务器端发生”状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是”表现层状态转化”。 客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

RESTful架构理解:
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

域名与版本

应该尽量将API部署在专用域名之下。https://api.example.com ,如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。https://api.example.com/api

版本号建议放入URL,https://api.example.com/v1/ 也可以放到请求头里

URI不要使用动词

因为”资源”表示一种实体,所以应该是名词,URI不应该有动词 比如 /api/getUserInfo 应该为 /api/user 使用GET方法来获取

  1. https://api.example.com/v1/zoos
  2. https://api.example.com/v1/animals
  3. https://api.example.com/v1/employees
  • GET(SELECT):从服务器取出资源(一项或多项)。 比如获取用户信息
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。
  • HEAD:获取资源的元数据。
  • OPTIONS:获取信息,关于资源的哪些属性是客户端可以改变的。

例子

  1. GET /zoos:列出所有动物园
  2. POST /zoos:新建一个动物园
  3. GET /zoos/ID:获取某个指定动物园的信息
  4. PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  5. PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  6. DELETE /zoos/ID:删除某个动物园
  7. GET /zoos/ID/animals:列出某个指定动物园的所有动物
  8. DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

状态码(仅供参考)

完整状态码参考: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

  1. # 幂等 Idempotent [aɪ'dɛmpətənt] 意思是一次执行或多次执行的结构都是一样的
  2. 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
  3. 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
  4. 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
  5. 204 NO CONTENT - [DELETE]:用户删除数据成功。
  6. 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
  7. 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
  8. 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
  9. 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
  10. 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
  11. 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
  12. 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
  13. 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

返回结果(仅供参考)

  1. GET /collection:返回资源对象的列表(数组)
  2. GET /collection/resource:返回单个资源对象
  3. POST /collection:返回新生成的资源对象
  4. PUT /collection/resource:返回完整的资源对象
  5. PATCH /collection/resource:返回完整的资源对象
  6. DELETE /collection/resource:返回一个空文档

参考