07月11, 2018

mongo基础笔记

之前学到一些mongo的简单用法,但不是太全。这次系统地整理一下。

什么是MongoDB

  • MongoDB是一个基于分布式文件存储的开源数据库系统,相比MySQL那样的关系型数据库,它更显得 轻巧、灵活,非常适合在数据规模很大、事和性不强的场合下使用
  • MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

MongoDB安装

  • windows安装
  • linux安装
  • mac安装

具体安装步骤,大家网上查一下就OK了。需要注意的是,linux安装可能有32位和64位,所以会有不同的下载地址。

mongodb启动与连接

最简单的方式

mongod

如果想要后台启动,则需要加参数--fork

启动参数

如:

  • dbpath
  • logpath

示例:

mongod --dbpath=你指定的要存放数据库的目录
选项 含义
--port 指定服务端口号,默认端口27017
--logpath 指定MongoDB日志文件,注意是指定文件不是目录
--logappend 使用追加的方式写日志
--dbpath 指定数据库路径
--directoryperdb 设置每个数据库将被保存在一个单独的目录

连接

mongo

alt

参数

--host,默认是127.0.0.1,可以将这个IP改为远程的。

当然在远程的机器,需要找到mongodb.conf文件,将bind_ip = 127.0.0.1 改成 bind_ip = 0.0.0.0 。

alt

当然我远程机器的mongo版本比较低,这个问题不大,可能就是在语法上会有一些区别吧。

MongoDB基本概念

  • 数据库 MongoDB的单个实例可以容纳多个独立的数据库,比如一个学生管理系统就可以对应一个数据库实例
  • 集合 数据库是由集合组成的,一个集合用来表示一个实体,如学生集合
  • 文档 集合是由文档组成的,一个文档表示一条记录,如一位学生张三就是一个文档

alt

alt

MongoDB基本操作

数据库操作

使用数据库

use database_name   # database_name代表数据库的名字

注:如果此数据库存在,则切换到此数据库下,如果此数据库还不存在也可以切过来

查看所有数据库

show dbs

alt

需要注意的是,我们一开始use 一个不存在的数据库,再show dbs,那个不存在的数据库是显示不出来的,只有我们往该数据库插入数据时才OK:

db.students.insert({name:'zpu', age:32});

查看当前使用的数据库

db 
# db.getName()

删除数据库

db.dropDatabase()

集合操作

查看集合帮助

db.students.help()

alt

查看数据库下的集合

show collections

alt

创建集合

  • 创建一个空集合
db.createCollection(collection_Name)
  • 创建集合并插入一个文档
db.collection_Name.insert(document)

插入文档

  • insert
  • save
db.collection_name.insert( document)
db.collection_name.save(document)

区别在于insert相同的_id,会报错,save相同的_id,进行数据更新。

alt

alt

更新文档

语法:

db.collection.update(
   <query>,
   <updateObj>,
   {
     upsert: <boolean>,
     multi: <boolean>
   }
)
  • query 查询条件,指定要更新符合哪些条件的文档
  • update 更新后的对象或指定一些更新的操作符
    • $set直接指定更新后的值
    • $inc在原基础上累加
  • upsert 可选,这个参数的意思是,如果不存在符合条件的记录时是否插入updateObj. 默认是false,不插入。
  • multi 可选,mongodb 默认只更新找到的第一条记录,如果这个参数为true,就更新所有符合条件的记录。

基础使用

比如之前有一个文档是这样的:

{
    "_id": "5b4577f5f2572972f4b4bf03",
    "name": "zpu"
}

写法一:

db.students.update({_id: "5b4577f5f2572972f4b4bf03"}, {age: 5})

写法二:

db.students.update({_id: "5b4577f5f2572972f4b4bf03"}, {$set: {"name": "zpu"}})

大家可以比较一下两个的区别,前者是对整个文档的覆盖,而后者可以对里面的某个进行修改。

除了$set之外,还有:

  • $inc,如: {$inc: {"age": "1"},年龄+1
  • $unset 删除指定的键,如{$unset: {name: ""}},删除name字段
  • $push 向数组中添加元素,如{$push:{"hobbys":"smoking"}}
  • $ne 匹配不等于(≠)指定值的文档,{field: {$ne: value} }
  • $addToSet 功能与$push相同,区别在于,$addToSet把数组看作成一个Set,如果数组中存在相同的元素,不会插入。
  • $each 可以结合上面$addToSet一起使用,如db.users.update({"name":"zhang"},{"$addToSet":{"emails":{"$each":["zhang@fwvga.com","zhang@163.com","zhang@qq.com"]}}})
  • $pop 与$push相对应,删除数组里的元素,db.users.update({"name":"zhang"},{"$pop":{"emails":{key:1}}}); key=1,从尾删除,key=-1,从头删除

上面提到数组,如果我们想针对某个索引,让它的值变化:

alt

dring改为aaa,写法如下:

db.students.update({_id: "5b4577f5f2572972f4b4bf03"}, {$set: {"hobbys.1": "aaa"}})

runCommand

var modify = {
    findAndModify:'student',
    query:{name:'张三'},
    update:{$set:{age:100}},
    fields:{name:'1'},
    sort:true,
    new:true //返回修改后的值
}

var result = db.runCommand(modify);
printjson(result);

数据库命令操作 runCommand

文档的删除

语法

db.collection.remove(
   <query>,
   {
     justOne: <boolean>
   }
)
  • query :(可选)删除的文档的条件。
  • justOne : (可选)如果设为 true 或 1,则只删除匹配到的多个文档中的第一个

查询文档

一般查是比较复杂的,我们需要学会以下几个东西:

  • 查找全部: db.collection_name.find()
  • 查询指定列:如db.students.find({}, {"age": 1})
  • 查找一条:db..collection_name.findOne()
  • 指定条件查询

指定条件查询,这个比较多,下面枚举一些场景:

查询age是5或者11的数据

db.students.find({age: {$in: [5, 11]}})

$in相反的是$nin

查询age大于3的数据

db.students.find({age: {$gt: 3}})

gte表示大于等于

另外一种写法:

db.students.find({age: {$not: {$lte: 3}}})

查询age小于3的数据

db.students.find({age: {$lt: 3}})

lte表示小于等于

查询age大于3小于11的数据

db.students.find({age: {$gt: 3, $lte: 11}})

查询age = 3 或者 age = 11 的数据

db.students.find({$or:[{age: 3},{age: 11}]})

使用ID进行查询

这里有一个特殊的,比如说我们在insert或者save的时候,没给_id,则进去的数据是长这样的:

alt

所以查询的时候,要这么来查:

db.students.find({_id: ObjectId("5b46cc71f2572972f4b4bf04")})

而不是字符串的那一串。

数组查询

//按所有元素匹配
//let result = db.student.find({friends:[ "Lufy", "Nami2", "Wusopu", "A" ]});
//匹配一条
//let result = db.student.find({friends:"A"});
//$all 数组值中满足所有指定的匹配条件,不考虑多出的元素以及元素顺序问题
//let result = db.student.find({friends:{$all:['A',"Lufy"]}});
//$in
//let result = db.student.find({friends:{$in:['A',"Lufy"]}});
//$size 返回元素个数总值等于指定值的文档
//let result = db.student.find({friends:{$size:4}});
//$slice 用于返回指定位置的数组元素值的子集(是数值元素值得一部分,不是所有的数组元素值)
//let result = db.student.find({friends:{$size:5}},{name:1,friends:{$slice:1}});
//let result = db.student.find({friends:{$size:5}},{name:1,friends:{$slice:-1}});

$where

$where操作符功能强大而且灵活,他可以使用任意的JavaScript作为查询的一部分,包含JavaScript表达式的字符串或者JavaScript函数。


//JavaScrip字符串形式
db.fruit.find( { $where: "this.banana == this.peach" } )
db.fruit.find( { $where: "obj.banana == obj.peach" } )

//JavaScript函数形式
db.fruit.find( { $where: function() { return (this.banana == this.peach) } } )
db.fruit.find( { $where: function() { return obj.banana == obj.peach; } } )

我们尽量避免使用where査询,因为它们在速度上要比常规査询慢很多。每个文档都要从BSON转换成JavaScript对象,然后通过where的表达式来运行;同样还不能利用索引。

查询结果集的条数

db.students.find().count()

正则匹配

用来处理类似模糊查询的功能。

# 查询某个字段的值当中是否以另一个值开头
db.students.find({name:/^zhang/})

分页查询

通过limitskip来实现。

# 查询在4-6之间的数据
db.students.find().skip(3).limit(3);

sort排序

# 查询出并升序排序 {age:1} age表示按那个字段排序 1表示升序
db.students.find().sort({age:1})

备份与恢复

备份语法:

mongodump
    -- host  
    -- port 
    -- out 
    -- collection 
    -- db 
    -- username
    -- password

恢复语法:

mongorestore
    --host
    --port
    --username
    --password
     [directory or filename to restore from]

权限

db.createUser({
    user:'zpu',
    pwd:'123456',
    roles:[
        {
          role:'readWrite',
          db:'school'
        },
        'read'
    ]
});

上面的代码表示创建一个zpu的用户,数据库school有读写的操作,其他数据库只有只读的权限。

当然mongod的启动方式也要变化:

mongod --auth

客户端mongo要加上用户名密码:

mongo -u zpu -p 123456 127.0.0.1:27017/school

什么是Mongoose

  • Mongoose是MongoDB的一个对象模型工具
  • 同时它也是针对MongoDB操作的一个对象模型库,封装了MongoDB对文档的的一些增删改查等常用方法
  • 让NodeJS操作Mongodb数据库变得更加灵活简单
  • Mongoose因为封装了MongoDB对文档操作的常用方法,可以高效处理mongodb,还提供了类似Schema的功能,如hook、plugin、virtual、populate等机制。
  • 官网 mongoosejs

使用 mongoose

安装mongoose

npm install mongoose -S

使用mongoose

语法如下:

const mongoose = require("mongoose");
mongoose. createConnection("mongodb://user:pass@ip:port/database");
  • user 用户名
  • pass 密码
  • ip IP地址
  • port 端口号
  • database 数据库
const mongoose = require("mongoose");
const connection = mongoose.createConnection("mongodb://127.0.0.1/school");

connection.on("open", () => {
    console.log("连接成功");
})

如果端口号是默认的,那么在URL中不传也是OK的。

Schema

Schema是数据库集合的模型骨架,定义了集合中的字段的名称和类型以及默认值等信息

Schema.Type

NodeJS中的基本数据类型都属于 Schema.Type 另外Mongoose还定义了自己的类型 基本属性类型有:

  • 字符串(String)
  • 日期型(Date)
  • 数值型(Number)
  • 布尔型(Boolean)
  • null
  • 数组([])
  • 内嵌文档

定义Schema

var testSchema = new Schema({
    name:String, //姓名
    binary:Buffer,//二进制
    living:Boolean,//是否活着
    birthday:Date,//生日
    age:Number,//年龄
    _id:Schema.Types.ObjectId,  //主键
    _fk:Schema.Types.ObjectId,  //外键
    array:[],//数组
    arrOfString:[String],//字符串数组
    arrOfNumber:[Number],//数字数组
    arrOfDate:[Date],//日期数组
    arrOfBuffer:[Buffer],//Buffer数组
    arrOfBoolean:[Boolean],//布尔值数组
    arrOfObjectId:[Schema.Types.ObjectId]//对象ID数组
    nested:{ //内嵌文档
      name:String,
    }
});

简单使用

const mongoose = require("mongoose");
const connection = mongoose.createConnection("mongodb://127.0.0.1:27017/school", {
    useNewUrlParser: true
});

const StudentSchema = new mongoose.Schema({
    name: String,
    age: Number
})
// 通过connection关联集合, 传入的就是模型的名称,Student=>小写student=>复数students
const Student = connection.model("Student", StudentSchema); 
// _id是默认返回,如果不要显示加上("_id":0)
Student.find({}, {_id: 0}, function(err, docs) {
    console.log(docs);
})

alt

ObjectId

存储在mongodb集合中的每个文档都有一个默认的主键_id 这个主键名称是固定的,它可以是mongodb支持的任何数据类型,默认是ObjectId 该类型的值由系统自己生成,从某种意义上几乎不会重复 ObjectId是一个12字节的 BSON 类型字符串。按照字节顺序,依次代表:

  • 4字节:UNIX时间戳
  • 3字节:表示运行MongoDB的机器
  • 2字节:表示生成此_id的进程
  • 3字节:由一个随机数开始的计数器生成的值

每一个文档都有一个特殊的键_id,这个键在文档所属的集合中是唯一的。

基础操作

查询

语法:

Model.find(查询条件,callback);

Model保存

语法:

Model.create(文档数据, callback))

实体保存

语法:

Entity.save(callback))

example:

let student = new Student({ name: 'zzl', age: 12 });
student.save((err, doc) => {
    console.log(err);
    console.log(doc);
});

alt

更新

语法:

Model.update(查询条件,更新对象,callback);

这里的更新和mongodb不太一样,即使传入了整个文档,也不会直接覆盖原文档,也是按字段覆盖

删除

语法:

Model.remove(查询条件,callback);

remove默认会删除所有匹配的文档,如果只想删除匹配的第一条的话,则要加一个选项justOne

Student.remove({ age: 3 }, { justOne: true }, function (err, result) {
    console.log(err);
    console.log(result);
});

查询的一些补充

除了find这个方法之外,还有下面两个也经常用到

  • findOne
  • findById

两个都是返回单个文档,但是第二个只接收文档的_id作为参数。

之前没有在mongodb中提$exists,它可以判断某些关键字段是否存在来进行条件查询,示例如下:

Model.find({name: {$exists: true}},function(error,docs){
      //查询所有存在name属性的文档
});

Model.find({email: {$exists: false}},function(error,docs){
      //查询所有不存在email属性的文档
});

高阶查询

find(Conditions,fields,options,callback)

像涉及复杂的limitskipsort,都可以写到options中去。

当然也有更简单的写法,如下:

Model.find({})
  .sort({createAt:-1})
  .skip((pageNum-1)*pageSize)
  .limit(pageSize)
  .exec(function(err,docs){
     console.log(docs);
  });

一些说明

只要学会mongodb的增删改查,基本上在使用mongoose上问题不大。

populate

在使用mongoose进行nodejs开发时,有很多场景都需要通过外键与另一张表建立关联,populate可以很方便的实现。

比如说我们有一张分数表,关联之前的学生表:

const Score = connection.model('Score', new mongoose.Schema({
    stuid: {
        type: mongoose.Schema.Types.ObjectId,//对象ID类型
        ref: 'Student' //引用 我是一个外键,我引用的是哪个集合的主键
    },
    grade: Number
}));

Student.create({ name: 'wangwu', age: 22 }).then((doc) => {
    return Score.create({ stuid: doc._id, grade: "80" })
}).then((doc) => {
    console.log(doc);
})

Score.findById("5b48426fc770af4db485423c")
    .populate("stuid", "name")
    .exec((err, doc) => {
    console.log(doc);
})

alt

mongoose进阶

扩展mongoose模型

业务分层

service(多个模型)->dao单个模型->model 模型定义

service(多个模型)->dao单个模型->model (模型定义+扩展方法)

statics 对类进行扩展

// 找年纪最大的
StudentSchema.statics.findOldest = function (callback) {
    return this.find().sort({ age: -1 }).limit(1).exec(callback);
}

const Student = connection.model("Student", StudentSchema); 

Student.findOldest(function(err, doc) {
    console.log(doc);
})

methods 对实例进行扩展

StudentSchema.methods.exist = function (callback) {
    let query = { name: this.name, age: this.age };
    return this.model('Student').findOne(query, callback);
}

const Student = connection.model("Student", StudentSchema); 

let person = new Student({ name: "zpu", age: 3 });
person.exist(function (err, doc) {
    console.log(err, doc);
});

virutal虚拟属性

  • virtual是虚拟属性的意思,即原来Schema定义里是不存在该属性,后来通过virutal方法赋予的属性。
  • Schema中定义的属性是要保存到数据库里的,而virtual属性基于已有属性做的二次定义。

比如我们有这样的一个Schema:

let PersonSchema = new mongoose.Schema({
    first_name: String, //姓  张
    last_name: String,   //名 三
    phone: String         //电话 010-6255889
});

然后我们要获取完整的name以及电话(不需要010-)。

PersonSchema.virtual('full_name').get(function () {
    return this.first_name + this.last_name;
});
PersonSchema.virtual('phonenumber').get(function () {
    return this.phone.split('-')[1];
});

默认情况下,document 对象的toJSON和toObject方法里面,它将virtual 属性排除了。

所以为了find的时候,能拿到上面的虚拟属性,还需要这样设置一下:

StudentSchema.set('toJSON', { virtuals: true });
StudentSchema.set('toObject', { virtuals: true });

hook

在用户注册保存的时候,需要先把密码通过salt生成hash密码,并再次赋给password

PersonSchema.pre('save', function (next) {
    this.password = crypto.createHmac('sha256', 'zpu').update(this.password).digest('hex');
    next();
});

PersonSchema.statics.login = function (username, password, callback) {
    password = crypto.createHmac('sha256', 'zpu').update(password).digest('hex');
    return this.findOne({ username, password }, callback);
}

// 使用时
Person.login('zpu', '123456', function (err, doc) {
    console.log(err, doc);
});

schema 插件

Schemas是可插拔的,也就是说,它们提供在应用预先打包能力来扩展他们的功能。

module.exports = function (schema) {
    schema.add({
        lastModify: Date
    });
    schema.pre('save', function (next) {
        this.lastModify = new Date();
        next();
    });
}

使用

let plugin = require('./plugin');
StudentSchema..plugin(plugin);

MongoDB 聚合

MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似sql语句中的 count(*)。 aggregate() 方法 MongoDB中聚合的方法使用aggregate()。

语法:

db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION)

分组

先往表里面放一些数据:

> db.article.insert({uid:1,content:'1'});
> db.article.insert({uid:2,content:'2'});
> db.article.insert({uid:1,content:'3'});

db.article.aggregate([{$group:{_id:'$uid',total:{$sum:1}}}]);

alt

聚合的表达式

表达式 描述 实例
$sum 计算总和。 db.mycol.aggregate([{$group : {_id : "$uid", num_tutorial : {$sum : "$visit"}}}])
$avg 计算平均值 db.mycol.aggregate([{$group : {_id : "$uid", num_tutorial : {$avg : "$visit"}}}])
$min 获取集合中所有文档对应值得最小值。 db.mycol.aggregate([{$group : {_id : "$uid", num_tutorial : {$min : "$visit"}}}])
$max 获取集合中所有文档对应值得最大值 db.mycol.aggregate([{$group : {_id : "$uid", num_tutorial : {$max : "$visit"}}}])
$push 在结果文档中插入值到一个数组中。 db.mycol.aggregate([{$group : {_id : "$uid", url : {$push: "$url"}}}])
$addToSet 在结果文档中插入值到一个数组中,但不创建副本。 db.mycol.aggregate([{$group : {_id : "$uid", url : {$addToSet : "$url"}}}])
$first 根据资源文档的排序获取第一个文档数据。 db.mycol.aggregate([{$group : {_id : "$uid", first_url : {$first : "$url"}}}])
$last 根据资源文档的排序获取最后一个文档数据 db.mycol.aggregate([{$group : {_id : "$uid", last_url : {$last : "$url"}}}])
> db.article.insert({uid:1,content:'3',url:'url1'});
> db.article.aggregate([{$group : {_id : "$uid", url : {$push: "$url"}}}])

alt

管道的概念

管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。 MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。

  • $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
  • $match:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。
  • $limit:用来限制MongoDB聚合管道返回的文档数。
  • $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。
  • $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
  • $group:将集合中的文档分组,可用于统计结果。
  • $sort:将输入文档排序后输出。

过滤显示字段

db.article.aggregate(
    { $project : {
        _id:0,
        content : 1 ,
    }}
 );

过滤文档

db.article.aggregate( [
    { $match : { visit : { $gt : 10, $lte : 200 } } },
    { $group: { _id: '$uid', count: { $sum: 1 } } }
 ] );

跳过指定数量

db.article.aggregate( [
    { $match : { visit : { $gt : 10, $lte : 200 } } },
    { $group: { _id: '$uid', count: { $sum: 1 } } },
    { $skip : 1 }
 ] );

Mongoose中使用

Student.aggregate([
    { $match : { age : { $gt : 18, $lte : 100 } } },
    { $skip : 1  }
]).then((doc) => {
    console.log(doc);
})

alt

结语

在没接触MongoDB之前,只知道它是NoSQL,而我那时候的理解是:not sql,其实是错误的,而是:not only sql。

它和mysql相比,性能更高一些,但容易丢数据。mysql数据库相对更稳定一些,但性能要低一些。

本文链接:www.my-fe.pub/post/mongo-basic-note.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。