一般持久化数据的方法
- 文件系统fs
- 数据库
- 关系型数据库 - mysql,Oracle
- 文档型数据库 - mongodb
- 键值对数据库 - redis
使用文件系统存储数据
node 命令行程序,操作文件数据:读取,吸入
// db.json 初始内容为 {}// index.jsconst 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) => {// 如果文件为空,则设置空对象ssconst json = data ? JSON.parse(data) : {}json[key] = valuefs.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地址
# 安装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.insertIdlet [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, configconst 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, updatedAtconst 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 }}, {// conifgtimestamps: 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.htmlret = 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-9502bdef4ee8name: { type: Sequelize.STRING(20), allowNull: false },price: { type: Sequelize.FLOAT, allowNull: false },stock: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 }}, {// conifgtimestamps: 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时会做校验。相关文档
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); });
- 更新与删除```javascript// 更新Fruit.findById(1).then(fruit => { // 方式1fruit.price = 4;fruit.save().then(()=>console.log('update!!!!'));});// 方式2Fruit.update({price:4}, {where:{id:1}}).then(r => {console.log(r);console.log('update!!!!')})// 删除// 方式1Fruit.findOne({ where: { id: 1 } }).then(r => r.destroy());// 方式2Fruit.destroy({ where: { id: 1 } }).then(r => console.log(r));
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方法来获取
https://api.example.com/v1/zooshttps://api.example.com/v1/animalshttps://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:返回一个空文档
参考
