数据持久化

nodejs中实现持久化的多种方法。
文件系统 fs
数据库

数据库类型

文件系统数据库

const fs = require('fs');
function get(key) {
    fs.readFile('./db.json', (err, data) => {
        const json = JSON.parse(data);
        console.log(json[key]);
    })
}
function set(key, value) {
    fs.readFileSync('./db.json', (err, data) => {
        const json = data ? JSON.parse(data) : {};
        json[key] = value; // 设置值
        // 重新写入文件
        fs.writeFile('./db.json', JSON.stringify(json), err => {
            if (err) {
                console.log(err);
            } else {
                console.log('写入成功');
            }
        })
    })
}
// 命令行接口部分
const readline = require('readline');
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});
rl.on('line', function(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('没有该操作');
    }
});

fl.on('close', function() {
    console.log("程序结束");
    process.exit(0);
})

mysql

原生驱动

// npm i mysql --save;
const mysql = require('mysql');
const cfg = {
    host: 'localhost',
    user: 'root',
    password: 'admin',
    database: 'yddb',
}
// 操作语句 conn.query();
// 创建连接
const conn = mysql.createConnection(cfg);
// 连接
conn.connect(err => {
    if (err) {
        throw err;
    } else {
        console.log('连接成功');
    }
})
// 创建表
const CREATE_SQL = `CREATE TABLE IF NOT EXISTS test (
    id INT NOT NULL AUTO_INCREMENT,
    message VARCHAR(45) NULL,
    PRIMARY KEY (id)
)`

const INSERT_SQL = `INSERT INTO test(message) VALUES(?)`;
const SELECT_SQL = `SELECT * FROM test`;

const.query(CREATE_SQL, err => {
    if (err) {
        throw err;
    }
    // 插入数据
    conn.query(INSERT_SQL, 'hello, world', (err, result) => {
        if (err) {
            throw err;
        }
        console.log(result);
        conn.query(SELECT_SQL, (err, results) => {
            console.log(results);
            conn.end(); // 若query语句有嵌套,则end需在此执行
        })
    })
})

ORM sequelize

基于promise的ORM(Object Relation Mapping) 支持多种数据库,事务,关联等
基本使用:

// 安装 npm i sequelize mysql2 -S
const Sequelize = require('sequelize');
// 建立连接
const sequelize = new Sequelize('yd', 'root', 'admin', {
    host: 'localhost',
    dialect: 'mysql',
    operatorsAliases: false
});
// 定义模型
const Fruit = sequelize.define('Fruit', {
    name: { type: Sequelize.STRING(20), allowNull: false },
    price: { type: Sequelize.FLOAT, allowNull: false },
    stock: { type: Sequelize.INTEGRE, defaultValue: 0 }
});
// 同步数据库,force: true 则会删除已存在的表
Fruit.sync().then(() => {
    // 添加测试数据
    return Fruit.create({
        name: '香蕉',
        price: 3.5
    });
}).then(() => {
    // 查询
    Fruit.findAll().then(fruits => {
        console.log(JSON.stringify(fruits));
    })
})
Fruit.sync({ force: true })'
const Fruit = sequelize.define('Fruit', {}, {
    timestamps: false
})

freeTableName: true 或 tableName: 'xxx'

设置前者则以modelName作为表名; 设置后者则按其值作为表明

// 定义为属性的一部分
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}`
    }
}
// 定义为模型选项
{
    getterMethods: {
        amount() {
            return this.getDataValue('stock') + 'kg';
        }
    },
    setterMethods: {
        amount(val) {
            const idx = val.indexOf('kg');
            const v = val.slice(0, idx);
            this.setDataValue('stock', v);
        }
    }
}

// 通过模型实例触发setterMethods
Fruit.findAll().then(fruits => {
    console.log(JSON.stringify(fruits));
    // 修改amount,触发setterMethods
    fruits[0].amount = '150kg';
    fruits[0].save();
})

可以通过检验功能验证模型字段格式,内容,检验会在create, update 和 save 时自动运行

price: {
    validate: {
        isFloat: { msg: '价格字段请输入数字'},
        min: { args: [0], mes: '价格字段必须大于0'}
    }
},
stock: {
    validate: {
        isNumeric: { msg: '库存字段请输入数字'}
    }
}
// 添加类级别方法
Fruit.classify = function(name) {
    const tropicFruits = ['香蕉', '芒果', '椰子']; // 热带水果
    return tropicFruits.includes(name) ? '热带水果' : ''其它水果
}
// 添加实例级别方法
Fruit.prototype.totalPrice = function(count) {
    return (this.price * count).toFixed(2);
}
Fruit.findAll().then(fruits => {
    const [ f1 ] = fruits;
    console.log(f1.totalPrice(5));
})
// 通过id查询
Fruit.findById(1).then(fruit => {
    // fruit 是一个Fruit 实例,若没有则为null
    console.log(fruit.get());
})
// 通过属性查询
Fruit.findOne({ where: { name: '香蕉' } }).then(fruit => {
    // fruit 是首歌匹配项, 若没有则为null
    console.log(fruit.get());
});
// 获取数据和总条数
Fruit.findAndCountAll().then(result => {
    console.log(result.count);
    console.log(result.rows.length);
});
// 查询操作符
const Op = Sequelize.Op;
Fruit.findAll({
    // where: { price: {[Op.lt]: 4 }, stock: { [Op.gte]: 100 }}
    where: { price: { [Op.lt]: 4, [Op.gt]: 2}}
}).then(fruits => {
    console.log(fruits.length);
});
// 或语句
Fruit.findAll({
    where: { price: { [Op.or]: [{ [Op.gt]: 3}, { [Op.lt: 2]}]}}
}).then(fruits => {
    console.log(fruits[0].get());
})
// 分页
Fruit.findAll({
    offset: 0,
    limit: 2
})
// 排序
Fruit.findAll({
    order: [['price', 'desc']],
})
// 聚合
setTimeout(() => {
    Fruit.max('price').then(max => {
        console.log(max);
    });
    Fruit.sum('price').then(sum => {
        console.log(sum);
    })
}, 500)
Fruit.findById(1).then(fruit => {
    // 方式1
    fruit.price = 4;
    fruit.save().then(() => { console.log('update')});
})
// 方式2
Friut.update({ price: 4}, { where: {id : 1}}).then(r => {
    console.log(r);
    console.log('update');
})
// 方式1
Fruit.findOne({ where: {id: 1}}).then(r => r.destroy());
// 方式2
Fruit.destroy({ where: {id: 1}}).then(r => console.log(r));
// 1:N 关系
const Player = sequelize.define('player', { name: Sequelize.STRING });
const Team = sequelize.define('team', { name: Sequelize.STRING });
// 会添加teamId 到player表作为外键
Player.belongsTo(Team); // 1端建立关系
Team.hasMany(Player); // N端建立关系
// 同步
sequelize.sync({ force: true }).then(async () => {
    await Team.create({ name: '火箭'});
    await Player.bulkCreate([{name: '哈登', teamId: 1}, { name: '保罗', teamId: q}])
    // 1 端关联查询
    const  player = await Player.findAll({includes: [ Team ]});
    console.log(JSON.stringify(players, null, '\t'));
    // N端关联查询
    const team = await Team.findOne({ where: {name: '火箭'}, includes: [Player]});
    console.log(JSON.stringify(team, null, '\t'));
})
// 多对多关系
const Fruit = sequelize.define('fruit', { name: Sequelize.STRING });
const Category = sequelize.define('category', { name: Sequelize.STRING });
Fruit.FruitCategory = Fruit.belongsToMany(Category, {
    through: 'FruitCategory'
})
// 插入测试数据
await Fruit.create(
    {
        name: '香蕉',
        categories: [{id: 1, name: '热带'}, {id: 2, name: '温带'}]
    },
    {
        includes: [Fruit.FruitCategory]
    }
);
// 多对多联合查询
const fruit = await Fruit.findOne({
    where: {name: '香蕉'}, // 通过through指定条件,字段等
    include: [{mode}: Category, through: { attributes: ['id', 'name']}]
})

mongodb

创建dbpath文件夹

启动 mongod --dbpath=/data

测试

// 查询所有数据库
show dbs
// 切换、创建数据库,当创建一个集合(table)的时候,会自动创建当前数据库
use test
// 得到当前db的所有聚集集合
db.getCollectionNames()
// 查询
db.fruits.find()
// 插入一条数据
db.fruits.save({name: '苹果', price: 5})
// 条件查询
db.fruits.find({price: 5})
db.fruits.find({ price: {$lte: 10}})

mongodb 原生驱动
npm install mongodb --save
链接mongodb

// 客户端
const MongoClient = require('mongodb').MongoClient;
// 链接URL
const url = 'mongodb://1270.0.1:27017';
// 数据库名
const dbName = 'test';
(asycn function() {
    // 创建客户端
    const client = new MongoClient(url, { useNewUrlParser: true });
    try {
        // 链接数据库,返回promise
        await client.connect();
        console.log('链接成功');
        // 获取数据库
        const db = client.db(dbName);
        // 获取集合
        const fruitsColl = db.collection('fruits');
        // 插入文档,返回Promise<CommandResult>
        let r = await fruitsColl.insertOne({ name: '芒果', price: 20.0 });
        console.log('插入成功', r.result);
        // 查询文档
        r = await fruitsColl.findOne();
        console.log('查询结果', r);
        // 更细文档,返回Promise<CommandResult>
        r = await fruitsColl.updateOne({ name: '芒果'}, { $set: { name: '苹果'} });
        console.log('更新成功', r.result);
        // 删除文档
        r = await fruitsColl.deleteOne({ name: '苹果'});
        console.log('删除成功', r.result);
    } catch (err) {
        console.log(err);
    }
    client.close();
})();
// 比较 $eq, $gte, $in等
await fruitsColl.find({ price: {$gt: 10}})
// 逻辑$and, $nor, $or
// price > 10 或 price < 5
await fruitsColl.find({$or: [{price: { $gt: 10}}, { price: {$lt: 5}}]})
// price 不大于10 且 price 不小于5
await fruitsColl.find({ $nor: [price: {$gt: 10}, {price: {$lt: 5}}]})
// 元素$exits, $type
await fruitsColl.insertOne({ name: '芒果', price: 20.0, stack: true});
await fruitsColl.find({ stack: {$exists: true }});
// 模拟$regex, $text, $expr
await fruitsColl.find({ name: {$regex: /芒/}});
await fruitsColl.createIndex({ name: 'text'}) // 验证文本抖索须首先对字段加索引
await fruitsColl.find({$text: {$search: '芒果'}}) // 按词搜索,单独字查询不出结果
// 数组$all, $elemMatch, $sequelize
fruitsColl.insertOne({..., tags: ['热带', '甜']}) // 插入带标签数据
// $all: 查询指定字段包含所有指定内容的文档
await fruitsColl.find({ tags: {$all: ['热带', '甜']}})
// $elemMatch: 指定字段数组中至少有一个元素满足所有查询规则
fruitsColl.insertOne({ hisPrice: [20, 25, 30]) // 数据准备
fruitsColl.find({ hisPrice: { $elemMatch: {$gt: 24, $lt: 26}}}) // 历史价位有没有出现在24 - 26之间
// 地理空间 $geoIntersects, $geoWithin, $near, $nearSphere
// 创建 stations集合
const stations = db.collection('stations');
// 添加测试数据,执行一次即可
await stations.insertMany([
    { name: '天安门东', loc: [116.407851, 39.91408]},
    { name: '天安门西', loc: [116.398056, 39.913723]},
    { name: '王府井', loc: [116.417809, 39.91435]}
]);
await stations.createIndex({ loc: '2dsphere'});
r = await stations.find({
    loc: {
        $nearSphere: {
            $geometry: {
                type: 'Point',
                coordinates: [116.403846, 39.915525]
            },
            $maxDistance: 1000
        }
    }
}).toArray();
console.log('天安门附近地铁站', r);

// 字段相关: $set, $unset, $setOnInsert, $rename, $inc, $min, $max, $mul
// 更新多个字段

await fruitsColl.updateOne(
    { name: '芒果'},
    { $set: { price: 19.8, category: '热带水果'}},
)
// 更新内嵌字段
{ $set: { ..., area: { city: '三亚'}}}
// 数组相关 $, $[]. $addToSet, $pull, $pop, $push, $pushAll
// $push 用于新增
insertOne({ tags: ['热带', '甜']}) // 添加tags数组字段
fruitsColl.updateMany({name: '芒果'}, { $push: { tags: '上火'}})
// $push, $pushAll用于删除符合条件项,$pop删除首项 -1 或尾项1
fruitsColl.updateMany({name: '芒果'}, { $pop: { tags: 1}});
fruitsColl.updateMany({name: '芒果'}, { $pop: { tags: 1}});
// $, $[] 用于修改
fruitsColl.updateMany({name: '芒果', tags: '甜'}, { $set: {'tags.$': '香甜'}})
// 修改器,常结合数组操作符使用: $each, $position, $slice, $sort
$push: { tages: { $each: ['上火', '真香'], $slice: -3 }}

Collection

// 聚合管道阶段: $group, $count, $sort, $skip, $limit, $project 等
// 分页查询
r = await fruitsColl.aggregate([ {$sort: { price: -1}}, {$skip: 0}, {$limit: 2}]).toArray();
// 投射: 值选择name, price并排除_id
fruitsColl.aggregate([..., {$project: {name: 1, price: 1, _id: 0}}]).toArray();
// 集合管道操作符,$add, $avg, $sum等
// 按name字段分组,统计组内price总和
fruitsColl.aggregate([ { $group: {_id: '$name', total: {$sum: '$price'}}}]).toArray();

常用聚合管道阶段操作均有对应的单个方法,通过Cursor调用
await fruitsColl.find().count();
await fruitsColl.find().sort({price: -1}).skip(0).limit(2).project({name: 1, price: 1}).toArray();

ODM - Mongoose

概述: 优雅的NodeJs对象文档模型object document model, Mongoose有两个特点

// npm install mongoose -S
const mongoose = require('mongoose');
// 链接
mongoose.connect('mongodb://127.0.0.1:27017/test', { useNewUrlParser: true });
const conn = mongoose.connection;
conn.on('error', () => {
    console.log('链接数据库失败');
})
conn.once('open', async () => {
    // 定义个Schema - Table
    const Schema = mongoose.Schema({
        category: String,
        name: String
    })
    // 编译一个Model, 他对应数据库中复数,小写的collection
    const Model = mongoose.model('fruit', Schema);
    try {
        // 创建 create 返回Promise
        let r = await Model.create({
            category: '温带水果',
            name: '苹果',
            price: 5
        })
        console.log('插入数据', r);
        // 查询,find返回Query, 他实现了then和catch,可以当promise使用
        // 如果需要返回Promise,调用exec()
        r = await Model.find({name: '苹果'});
        console.log('查询结果', r);
        // 更新, updateOne 返回Query
        r = await Model.updateOne({ name: '苹果'}, { $set: { name: '芒果'}});
        console.log('更新结果', r);
        // 删除,deleteOne 返回Query
        r = await Model.deleteOne({ name: '苹果'});
        console.log('删除结果', r);
    } cache (error) {
        console.log(error);

    }
})

Mongoose 中各盖面和关系数据库,文档数据库对应关系

Oracle MongoDB Mongoose
数据库实例(database instance) MongoDB实例 Mongoose
模式(schema) 数据库(database) mongoose
表(table) 集合(collection) 模板(Schema) + 模型(Model)
行(row) 文档(document) 实例(instance)
rowid _id _id
Join DBRef DBRef

字段定义

const blogSchema = mongoose.Schema({
    title: { type: String, require: [true, '标题为必填项']}, // 定义检验规则
    author: String,
    body: String,
    comments: [{body: String, date: Date }], // 定义对象数组
    date: { type: Date, default: Date.now }, // 指定默认值
    hidden: Boolean,
    meta: {
        votes: Number, // 定义对象
        favs: Number
    }
})
// 定义多个索引
blogSchema.index({ title: 1, author: 1, date: -1});
const BlogModel = mongoose.model('blog', blogSchema);
const blog = new BlogModel({
    title: 'nodejs持久化',
    author: 'jerry',
    body: '....'
})
const r = await blog.save();
console.log('新增blog', r);

可选字段类型:
String
Number
Date
Buffer
Boolean,
Mixed
ObjectId
Array
避免创建索引警告

mongoose.connect('mongodb://127.0.0.1:27017/test', {
    useCreateIndex: true
})
// 定义实例方法
blogSchema.methods.findByAuthor = function() {
    return this.model('blog').find({ author: this.author }).exec();
}
// 获得模型实例
const BlogModel = mongoose.model('blog', blogSchema);
const blog = new BlogModel({...});
// 调用实例方法
r = await blog.findByAuthor();
console.log('findByAuthor', r);

实例方法还需要定义实例,用起来较为繁琐,可以使用静态方法

blogSchema.statics.findByAuthor = function(author) {
    return this.model('blog').find({ author }).exec();
}
r = await BlogModel.findByAuthor('jerry');
console.log('findByAuthor', r);
blogSchema.virtual('commentsCount').get(function() {
    return this.comments,length;
})
let r = await blog.findOne({ author: 'jerry' });
console.log('blog留言数', r.commentsCount);

购物车相关接口实现

const mongoose = reuqire('mongoose');
const schema = mongoose.Schema({
    name: String, 
    password: String, 
    cart: []
});
schema.statics.getCart = function(_id) {
    return this.model('user').findById(_id).exec();
}
schema.statics.setCart = function(_id, cart) {
    return this.model('user').findByIdAndUpdate(_id, { $set: { cart }}).exec();
}
const model = mongoose.model('user', schema);
// 测试数据
model.updateOne(
    {_id: 'sfsfsdfsada'},
    { name: 'jerry', cart: [{ pname: 'iphone', price: 666, count: 1}]},
    { upset: true },
    (err, r) => {
        console.log('测试数据');
        console.log(err, r);
    }
)
module.exports = model;

API编写,./cart.js

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
// 数据库相关
require('./mongoos');
const UserModel = require('./models/user');
// mock session
const session = { sid: { userId: 'fhsjfsjdhakda' }};
app.use(bodyParser.json());
// 查询购物车数据
app.get('/api/cart', async (req, res) => {
    const data = await UserModel.getCart(session.sid.userId);
    res.send({ ok: 1, data });
})
// 设置购物车数据
app.post('/api/cart', async (req, res) => {
    await UserModel.setCart(session.sid.userId, req.body.cart);
    res.send({ok: 1});
})
app.listen(3000);

Redis

  1. 功能
  1. 工具
  1. 基本用法
const redis = require('redis');
const client = redis.createClient(6379, '127.0.0.1');
// 设置
client.set('hello', 'this is a value', redis.print);
// 读取
client.get('hello', function(err, v) {
    console.log('redis get hello:', v);
});
// 写入json
client.set('json', JSON.stringify({ name: 'jerry', age: 22}), redis.print);
client.get('json', function(err, v) {
    console.log('redis get json:', JSON.parse(v));
});
// 先清除数据
client.del('testLists');
client.rpush('testLists', 'a');
client.rpush('testLists', 'b');
client.rpush('testLists', 'c');
client.rpush('testLists', 'd');
client.rpush('testLists', 'e');
client.lpop('testLists', function(err, v) {
    console.log('client.lpop', v);
});
client.lrange('testLists', 0, -1, function(err, lists) {
    console.log('client.lrange:', lists);
})
client.sadd('testSet', 1);
client.sadd('testSet', 'a');
client.sadd('testSet', 'bb');
// 不可重复
client.sadd('testSet', 'bb');
client.smembers('testSet', function(err, v){
    console.log('client.smembers', v);
})
client.subscribe('testPublish');
client.on('message', function(channel, msg) {
    console.log('client.on message, channel:', channel, msg);
})
const client2 = redis.createClient(6379, '127.0.0.1');
client2.publish('testPublish', 'message from publisher');