# 数据持久化mysql

一般持久化数据的方法

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

# 使用文件系统存储数据

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

// db.json 初始内容为 {}
// index.js
const fs = require('fs')

// 获取某个key的值
function get(key) {
  fs.readFile('./db.json', (err, data) => {
    const json = data ? JSON.parse(data) : {}
    console.log(json[key])
  })
}

// 设置键值对值
function set(key, value) {
  fs.readFile('./db.json', (err, data) => {
    // 如果文件为空,则设置空对象ss
    const json = data ? JSON.parse(data) : {}
    json[key] = value
    fs.writeFile('./db.json', JSON.stringify(json), err => {
      err && console.log(err)
      console.log('写入成功')
    })
  })
}

// 命令行操作支持
const readline = require('readline')
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

rl.on('line', input => {
  const [op, key, value] = input.split(' ')

  if (op === 'get') {
    get(key)
  } else if(op === 'set') {
    set(key, value)
  } else if (op === 'quit') {
    rl.close()
  } else {
    console.log('没有该操作')
  }
})

rl.on('close', () => {
  console.log('程序结束')
  process.exit(0)
})

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

# mysql模块

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

const mysql = require('mysql')

// 连接配置
const cfg = {
  host: 'localhost',
  user: 'root',
  password: 'test',
  database: 'test'
}

// 创建连接对象
const conn = mysql.createConnection(cfg)

// 连接
conn.connect(err => {
  if (err) {
    console.log(err.message)
    throw err
  } else {
    console.log('连接成功!')
  }
})

// 执行mysql语句 conn.query()
const CREATE_SQL = `
  create table if not exists tb_test (
    id int not null auto_increment,
    message varchar(50) null,
    primary key (id)
  )
`
const INSERT_SQL = `insert into tb_test(message) values(?)`
const SELECT_SQL = `select * from tb_test`

// 创建表
conn.query(CREATE_SQL, (err, data) => {
  if (err) {
    throw err
  }
  // 没有报错,说明创建表成功, 没有特别的返回信息
  console.log(data)
})

// 插入数据
conn.query(INSERT_SQL, 'test', (err, data) => {
  if (err) {
    throw err
  } 
  console.log(data) // data.insertId 为插入数据的id
})

// 查询数据
conn.query(SELECT_SQL, (err, data) => {
  if (err) {
    throw err
  } 
  console.log(data)
  // console.log(data[0].id) // 1
})
// [ RowDataPacket { id: 1, message: 'test' },
//   RowDataPacket { id: 2, message: 'test' },
//   RowDataPacket { id: 3, message: 'test' },
//   RowDataPacket { id: 4, message: 'test' } ]

# mysql2模块(ES2017写法)

node-mysql2 github地址 (opens new window)

# 安装mysql2模块
npm install mysql2 --save

操作数据库demo

(async () => {
  try {
    const mysql = require('mysql2/promise')
    // 连接配置
    const cfg = {
      host: 'localhost',
      user: 'root',
      password: 'xx',
      database: 'test'
    }

    // 创建连接
    let connection = await mysql.createConnection(cfg)

    // 执行mysql语句 conn.execute()
    const CREATE_SQL = `
    create table if not exists tb_test (
      id int not null auto_increment,
      message varchar(50) null,
      primary key (id)
    )
    `
    const INSERT_SQL = `insert into tb_test(message) values(?)`
    const SELECT_SQL = `select * from tb_test`

    let ret = await connection.execute(CREATE_SQL)
    console.log(ret)

    ret = await connection.execute(INSERT_SQL, ['abc'])
    console.log(ret) // ret.insertId

    let [rows, fields] = await connection.execute(SELECT_SQL)
    console.log(rows)
  } catch(e) {
    console.log(e)
  }
})()

# Node.js ORM - Sequelize

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

# 使用ORM好处

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

# 增删查改demo

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

// index.js
(async () => {
  try {
    const Sequelize = require('sequelize')

    // 建立连接
    // 参数分别为: database, username, password, config
    const sequelize = new Sequelize('test', 'root', '1234567Abc,.', {
      host: 'localhost',
      dialect: 'mysql' // 'mysql' | 'mariadb' | 'postgres' | 'mssql' 之一 
    })
    // 方法2: 传递连接 URI
    // const sequelize = new Sequelize('postgres://user:pass@example.com:5432/dbname');

    // 测试连接,使用 .authenticate() 函数来测试连接
    await sequelize.authenticate() // 如果连接异常,会走catch的逻辑

    // 创建表模型
    // public define(modelName: string, attributes: Object, options: Object): Model
    // 使用下面的模型创建表时,会默认加上3个字段 主键id, createdAt, updatedAt
    const Fruit = sequelize.define('fruit', {
      name: { type: Sequelize.STRING(20), allowNull: false },
      price: { type: Sequelize.FLOAT, allowNull: false },
      stock: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }
    }, {
      // conifg
      timestamps: false, // 默认值为true,如果为true会加上createdAt, updatedAt字段
      // freezeTableName: true // 默认为false, 默认情况下会为表名添加一个s,即 fruits,设置为true可以阻止这一默认行为
    })
    // 创建表:将模型同步到数据库
    let ret = await Fruit.sync() // 如果表不存在则同步,否则不处理
    // let ret = await Fruit.sync({force: true}) // 创建之前,先删除原来的表
    console.log(ret) // 返回 fruit

    // 插入数据(增)
    ret = await Fruit.create({
      name: "香蕉",
      price: 3.5,
      // test: 1 会根据模型来,就算这加了额外的数据,也不会插入到表
    })
    console.log(ret.toJSON()) // 对象里面包含插入的数据行,包含id

    // 查询所有数据(查)
    ret = await Fruit.findAll()
    console.log(ret.length)
    console.log(JSON.stringify(ret)) // 如果不stringify,打印的都是对象

    // 更新 (改)
    // ret = await Fruit.update({ price: 8 }) // 如果没有where会报错
    // 文档 https://demopark.github.io/sequelize-docs-Zh-CN/querying.html
    ret = await Fruit.update({ price: 8 }, {  
      where: {
        price: { [Sequelize.Op.lt]: 2, [Sequelize.Op.gt]: 0 }  // price > 0 and price < 2
      }
    })
    console.log(ret[0]) // 修改影响行数

    // 删除
    ret = await Fruit.destroy({  
      where: {
        price: { [Sequelize.Op.lt]: 4, [Sequelize.Op.gt]: 0 }  // price > 0 and price < 4
      }
    })
    console.log(ret) // 删除行数

  } catch(e) {
    console.log('error message: ', e.message)
  }
})()

# 自增id的缺点与UUID

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

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

# 其他

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

  • 在define模型时,除了type外,还可以加validate校验, 在update、create、save时会做校验。相关文档 (opens new window)
  • Getters & Setters:可用于定义伪属性或映射到数据库字段的保护属性
    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)`;
       } 
    }
    
  • 查询相关
    // 通过属性查询
    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);
    });
    
  • 更新与删除
    // 更新
    Fruit.findById(1).then(fruit => { // 方式1
        fruit.price = 4;
        fruit.save().then(()=>console.log('update!!!!'));
    });
    // 方式2
    Fruit.update({price:4}, {where:{id:1}}).then(r => {
        console.log(r);
        console.log('update!!!!')
    })
    
    // 删除
    // 方式一
    Fruit.findOne({ where: { id: 1 } }).then(r => r.destroy());
    // 方式2
    Fruit.destroy({ where: { id: 1 } }).then(r => console.log(r));
    

# ERD 实体关系图

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

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

想几个问题:

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

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

# 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方法来获取

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

例子

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

# 状态码(仅供参考)

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

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

# 返回结果(仅供参考)

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

参考

# 连接池

上次更新: 2020/10/29 22:59:19