# 数据持久化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好处
- 支持多种数据库 'mysql' | 'mariadb' | 'postgres' | 'mssql' | 'sqlite'
- 可以不用手写sql语句
- 以面向对象、模块化的方式来操作数据库,不用自己手动创建操作数据库的类 ...
# 安装
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 实体关系图
关系型数据库在规划设计表时,都会先画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:返回一个空文档
参考
# 连接池
← 网络编程 数据持久化mongodb →