mongodb基础操作及进阶理解

从2012年初开始,公司的一些核心产品准备开始陆续迁移到MongoDB上,我们尝试着从一个小产品开始使用,陆续将其他产品迁入,到13年年底,公司产品在数据库选择上基本实现了NoSQL化,除了一些事务性要求较高(如支付)的模块继续停留在Mysql上,基本上现在大家都会偏向于使用MongoDB。就我个人而言,我觉得一些小项目(如后台管理),或者需求变化极快的项目(现今的大部分中小移动互联网产品),如果对于并发要求不高,没有特别强的事务性,业务相对简单,基本上就是一到两人完成的小应用,mongodb应该是是这类应用的首选数据库,我自己的体验的理由如下:

  • 相对于MySQL等关系型数据库,mongodb更为轻量,安装,使用,部署都轻便得多。

  • mongodb 的驱动写得极为成熟,天然的Bson数据结构,使得存取数据都以Map结构进行交互,数据接口非常方便,不需要额外进行数据转换,开发效率明显提升。较为明显的对比就是:如果使用MySQL,往往需要使用一个第三方ORM框架进行DB层的操作,以及Bean映射和数据转换,mongodb完全不需要ORM,原生驱动已经做得非常棒了。

  • 相对松散的数据库设计模式,使得它能更好的适应快速变化的需求。当然这一点并不是说用mongodb不需要进行严谨的数据库结构设计了,只是说在需求变更涉及到库表修改的时候,不像MySQL那么纠结要先去弄一下表结构,我才敢部署应用。mongodb基本上没有这个痛感。

  • mongodb 现在的最新稳定版是2.4.8,至此,它提供了相对完善的操作API,而且把Aggregation框架加入以后,原来一直头痛的各种统计操作也有了较好的解决方案,现在可以比较放心的说,MySQL能完成的几乎所有事情,mongodb都能完成。

  • mongodb的文档现在真的好的令人发指啊,应该可以算是业界文档的模范了。

这篇文章主要想介绍一下mongodb的一些基本常用的操作,顺便将一些工作中的处理和理解也提出来,希望能称得上是一篇进阶之作。

1. insert,插入数据

insert操作比较简单,mongodb提供了insert, save 方法进行数据插入操作。
insert就是普通插入,如果待插入的数据中未含有key:'_d',mongodb则会自动生成一个类型为ObjectId,key为'_id'的数据作为该条记录的主键,如果已经含有,则只校验一下'_id'是否存在于集合中,未存在则会插入成功,否则会返回一个错误。
sava 方法会根据待处理的数据中是否含有key:'_id'进行处理,没有包含则插入数据,包含则根据这个_id更新原有数据。
另外,insert方法还可以进行批量操作,只要将需要插入的数据按照数组格式组装传入即可。
基本语法如下:

//
db.collection.insert({key:value});
db.collection.insert([{key:value},{key:value}...]);
db.collection.save({key:value});

2. remove,删除数据

remove操作也很简单,只需要把删除条件传入即可。
基本语法:

db.collection.remove({key:value});

如果没有传入任何删除条件,则会删除整个集合。

3. update,更新数据

update稍微复杂一些,我们在开发中碰到的关于更新的操作大概有以下三种情况:

  • 普通更新操作(update.$set|$unset)。
  • 原子更新操作(update.$inc)。
  • 阻塞查询更新操作(findAndModify.$set|$inc)。
  • 数组相关更新操作($push|$pull|$addToSet|$pop 等)。

3.1 普通更新操作

首先来说一下update的基本语法:

db.collection.update( <query>, <update>, <upsert>, <multi> )

query:更新的查询条件.
update: 更新的数据.
upsert: 当查询条件没有找到数据时是否插入,默认false.
multi:是否更新多条,默认false.
这里需要强调一下的是对于选项【update】的处理,如果是更新全文档,则无需特别处理;如果只更新文档中的几个字段,则需要加"$set"进行处理,不然会将文档覆盖掉,在写数据处理脚本的时候要特别注意这些地方。这里提供一个对于【普通更新操作】的示例:

db.Student.update(
        {_id:ObjectId("52e8fce17ee72c8860511af6")},
        {"$set":{"name":"jay","status":1}},
        false,
        true
    )

3.2 原子更新操作

mongodb对于自增长的处理是通过$inc来实现的,自增长的过程是原子性的。示例如下:

db.Student.update(
        {_id:ObjectId("52e8fce17ee72c8860511af6")},
        {"$inc":{"age":3}},
        false,
        true
    )

上面这段代码将Student中的一条记录的age字段自增长了3。
如果在一个update操作中,我既有更新部分数据的需求,又希望对某个字段进行自增长操作,还希望删除某个字段,这里的处理就很简单了:

db.Student.update(
        {_id:ObjectId("52e8fce17ee72c8860511af6")},
        {
            "$set":{"name":"jay","status":1}
            ,"$inc":{"age":3},
            ,"$unset":{"sex":1}
        },
        false,
        true
    )

3.3 阻塞查询更新操作

这里需要提一下mongodb的锁机制了。

3.3.1 MongoDB 使用的锁

MongoDB 使用的是“readers-writer”锁, 可以支持并发但有很大的局限性,当一个读锁存在,许多读操作可以使用这把锁,然而, 当一个写锁的存在,一个单一的写操作会 exclusively 持有该锁,同时其它读,写操作不能使用共享这个锁;举个例子,假设一个集合里有 10 个文档,多个 update 操作不能并发在这个集合上,即使是更新不同的文档。

3.3.2锁的粒度

在2.2版本以前,mongod只有全局锁;在2.2版本开始,大部分读写操作只锁一个库,相对之前版本,这个粒度已经下降,例如如果一个 mongod 实例上有5个库,如果只对一个库中的一个集合执行写操作,那么在写操作过程中,这个库被锁;而其它5个库不影响。相比RDBMS来说,这个粒度已经算很大了!

可以看出,mongodb这种锁机制设计得不是很合理,数据到了一定数量级比较容易出现性能问题,所以要特别注意【更新】和【查询】操作。

我现在的需求是,要在mongodb中获取自增长的Integer类型的主键。利用findAndModify以及mongodb的锁机制可以实现这一需求。findAndModify既是read的操作,又是write的操作,在执行findAndModify时,mongodb会对集合进行writer加锁,其他线程不能进行write操作,操作完毕以后,它同时返回操作后的最新结果,保证read的准确性。这样就保证了每一次只能执行write and read in document的事情。
我们在实践中的设计是这么做的:

  • 设计一个Collection,集合名为AutoIds.插入一条数据:{_id:1}.
  • 实现生成自增长并返回主键逻辑,这里用的是java驱动:
 public Integer getNextId(String fieldName) {
        DBCollection autoIdsColl = db.getAutoIdsCollection();
        // _id=1, 确定预先插入的唯一一条记录
        DBObject query = new BasicDBObject("_id", 1);
        // 过滤一下查询的 field
        DBObject fields ={_id:1, fieldName:1};
        // 排序
        DBObject sort = new BasicDBObject("_id", 1);
        // 定义每次自增长幅度为1
        update = new BasicDBObject("$inc", new BasicDBObject(fieldName, 1));
        // 更新并返回
        DBObject obj = autoIdsColl.findAndModify(query, fields, sort, false, update, true, true);
        // 返回此次更新的Id值
        Integer id = (Integer) obj.get(fieldName);
        return id;
    }
  • 由上一步可知,AutoIds只有一条记录,理论上可以无限横向扩展,为多个表维护ID,只需要传递不同的ID的key作为getNextId的参数即可。

相对于关系型数据库,mongodb需要绕这么一大圈确实有点说不过去,而且由于锁机制的欠缺,性能还差了一大截,不过在实际业务中,mongodb自带的ObjectId作为主键其实能解决大部分问题,所以也还算能接受。

3.4 数组更新操作

数组相关的更新操作在大部分情况下和普通更新操作没有啥特别大的区别,无非就是加了几个操作符。但是也有一些棘手的操作,由于不常用,每次弄的时候总是要回过头来翻文档,所以我这里单独提一下。

3.4.1 添加一个子项到数组中
db.Student.update(
        {_id:ObjectId("52e8fce17ee72c8860511af6")},
        {
            "$push":{"courses":{"name":"Math","code":"001"}}
        },
        false,
        true
    )

3.4.2 添加多个子项到数组中
db.Student.update(
        {_id:ObjectId("52e8fce17ee72c8860511af6")},
        {
            "$addToSet":{"courses":
                           {"$each":[ {"name":"Math","code":"001"}
                                      ,{"name":"English","code":"002"}
                                    ]
                            }
                        }
        },
        false,
        true
    )

这里的$addToSet会保证带插入的数组中相同子项只会存在一个,重复的子项也只会插入一次。如果业务需求没有这么严谨,也可以用$push代替。

3.4.2 移除指定子项
db.Student.update(
        {_id:ObjectId("52e8fce17ee72c8860511af6")},
        {
            "$pull":{"courses":{"name":"Math","code":"001"}}
        },
        false,
        true
    )

3.4.2 更新数组子项中的某个field

这里要借用占位符 $ 来完成。 先看示例:

db.Student.update(
        {
            _id:ObjectId("52e8fce17ee72c8860511af6")
            ,"courses.code":"001"
        },
        {
            "$set":{"courses.$.name":"MATH"}
        },
        false,
        true
    )

这个语句稍微解释一下:
a) 对于更新的查询条件,务必加 【"courses.code":"001"】这一项,这样才能定位到数组中的具体项。这里我之前有一个疑惑,就是加不加【"courses.code":"001"】都能查到同一条记录,为啥一定要加呢,主要是为了定位数组中的子项。

b) 有了 a)的解释,【"$set":{"courses.$.name":"MATH"}】中的 "$" 的意思就很好理解了,它就是用来定位数组子项当前项的,这两个写法缺一不可。

占位符$的使用在涉及到数组子项的查询也需要用到,后面的章节会说。

4. query 查询

查询操作其实比较简单了,mongodb提供了大量的操作符来做这个事情。之前我也说了mongodb的文档做得非常好,所以一些普通查询操作,直接翻文档吧,里面有语法,实例,非常棒。 链接
这里我就不准备把文档翻译一遍了,我写一下在使用过程中一些必要但是稍微绕了一下的处理。

4.1 优雅实现 between...and

db.Student.find({
    "time":
        {
            "$gt":start,
            "$lt":end
        }
})

这个结构对我的启发就是:我个人认为 $and 基本上是多余的。
之前用$and实现的方式:

db.Student.find({
    "$and":[
        {"time":{"$gt":start}}
        ,{"time":{"$lt":end}}
    ]
})

这样一对比,后者真的笨重而且多余。所以仔细想想,似乎所有的查询条件都不需要通过$and这样通过数组来实现呀,Map结构本来就支持多键存放的嘛。

4.2 ‘like’ 的新样子

db.Student.find({
    "name":
    {
        "$regex":"/abc[dD]{1}/"
    }
})

正则表达式来实现like的功能,而且更为强大,唯一需要考虑的就是效率问题。这里顺带也把全文搜索也牵出来了,范围太大了,以后单独讲。

4.3 数组子项的查询,中规中矩的$elemMatch,还是有更方便的写法?

示例:

db.Student.find({
      "courses":{
            "$elemMatch":{"code":"001"}
      }
    });

偶然发现还有一个超级简单的写法:

db.Student.find({
      "courses.code":"001"
    });

这里很容易引起混淆,到底Student的数据结构是怎么样的?【courses】这个字段类型是Map子文档(map)还是数组子文档(List)呢? 实际上只要它是二者中的任何一种,都可以用上面的写法查询出来。

4.4 根据数组子项查询,希望只返回查询到的数组子项,应该怎么写?

db.students.find( 
            {_id:ObjectId("6718703038737487484498")
              , "courses.code": "001" 
            },
            { "courses.$": 1 })

这里find方法使用了第二个参数,【courses.$】又看到了熟悉的占位符了,这里的作用还是一样,就是定位到query参数中查询到的子项,并只返回这个子项。

其实查询操作还有很多地方没有说到,例如基于位置的查询,全文搜索等。但是只要了解了本文所说的篇幅,日常开发中应该大部分也够了。
查询操作避不开的话题就是效率问题,我会单独写一篇这方面的文章,从索引,锁机制等探讨一下在mongodb中查询和更新等操作需要注意的问题。

综上,基本的操作都说了一下,我觉得还是多翻文档,用多了自然就熟了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容