这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

MongoDB

文档数据库SpringBoot的系列教程,彻底扫平阻碍我们CURD的路障,适用于对Mongodb不了解或了解不够的小伙伴

1 - 1.基本环境搭建与使用

SpringBoot结合mongodb进行业务开发,也属于比较基本的需求了,本文为mongo系列的基本篇,主要就是环境搭建、工程的配置设置相关

I. 环境搭建

正式开始之前,第一步就是需要安装Mongo的环境了,因为环境的安装和我们spring的主题没有太大的关系,因此我们选择最简单的使用姿势:直接用docker来安装mongo来使用

下面的安装过程都是mac环境,其他操作系统可以直接安装mongodb,移步相关教程

1. docker 安装

可以直接到官网进行下载安装,但是对系统版本有要求,所以需要使用Docker ToolBox,实际试过之后,感觉不太好用,实际上是将docker安装到虚拟机中了,下面直接使用brew命令进行安装

安装命令

brew cask install docker

执行完毕之后,会多一个应用名为 docker, 双击运行,输入密码等即可

2. mongo 安装使用

直接使用官方的mongo镜像即可,然后绑定端口映射,就可以在宿主机中使用mongo

# 下载镜像
docker pull mongo
# 加载并运行镜像
docker run --name mongo -p 27017:27017 -d mongo --auth
# 进入容器
docker exec -it d9132f1e8b26 /bin/bash
# 为mongo创建登录用户和密码
mongo
use admin
db.createUser({user:"root",pwd:"root",roles:[{role:'root',db:'admin'}]})
exit

上面完毕之后,可以在宿主机进行连接测试,判断是否安装成功

II. SpringBoot工程配置

1. pom依赖

整个框架选择的是spring-boot,所有spring这一套相关的pom配置少不了,我们主要需要注意的包就是spring-boot-starter-data-mongodb

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    <java.version>1.8</java.version>
</properties>

<dependencies>
   <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.45</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

2. 配置文件

配置文件如下,主要就是连接mongo的url

spring.data.mongodb.uri=mongodb://root:root@localhost:27017/basic?authSource=admin&authMechanism=SCRAM-SHA-1

通过上面的实例,也知道格式如下:

mongodb://用户名:密码@host:port/dbNmae?参数

  • 当没有用户名和密码时,可以省略掉中间的 root:root@
  • 当需要认证时,请格外注意
    • mongodb新版的验证方式改成了SCRAM-SHA-1,所以参数中一定一定一定得加上
      • ?authSource=admin&authMechanism=SCRAM-SHA-1
    • 如果将mongodb的验证方式改成了MONGODB-CR, 则上面的可以不需要

3. 测试使用

写一个简单的测试类,看下mongodb是否连接成功,是否可以正常操作

@Slf4j
@Component
public class MongoTemplateHelper {

    @Getter
    @Setter
    private MongoTemplate mongoTemplate;

    public MongoTemplateHelper(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }



    /**
     * 保存记录
     *
     * @param params
     * @param collectionName
     */
    public void saveRecord(Map<String, Object> params, String collectionName) {
        mongoTemplate.save(params, collectionName);
    }

    /**
     * 精确查询方式
     *
     * @param query
     * @param collectionName
     */
    public void queryRecord(Map<String, Object> query, String collectionName) {
        Criteria criteria = null;
        for (Map.Entry<String, Object> entry : query.entrySet()) {
            if (criteria == null) {
                criteria = Criteria.where(entry.getKey()).is(entry.getValue());
            } else {
                criteria.and(entry.getKey()).is(entry.getValue());
            }
        }

        Query q = new Query(criteria);
        Map result = mongoTemplate.findOne(q, Map.class, collectionName);
        log.info("{}", result);
    }
}

上面提供了两个方法,新增和查询,简单的使用姿势如

@SpringBootApplication
public class Application {

    private static final String COLLECTION_NAME = "personal_info";


    public Application(MongoTemplateHelper mongoTemplateHelper) {
        Map<String, Object> records = new HashMap<>(4);
        records.put("name", "小灰灰Blog");
        records.put("github", "https://github.com/liuyueyi");
        records.put("time", LocalDateTime.now());

        mongoTemplateHelper.saveRecord(records, COLLECTION_NAME);

        Map<String, Object> query = new HashMap<>(4);
        query.put("name", "小灰灰Blog");
        mongoTemplateHelper.queryRecord(query, COLLECTION_NAME);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

}

然后开始执行,查看输出,结果演示如下

gif.gif

4. 说明

最后针对认证的问题,需要额外提一句,开始测试的时候,使用的配置如下

spring.data.mongodb.username=root
spring.data.mongodb.password=root
spring.data.mongodb.authentication-database=basic
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017

然而因为mongo采用的是SHA-1加密方式,所以始终验证不通过;然后查了一下,各种让改mongo的验证版本,改回去用CR的方式;但明显这种并不是一种好的解决方式,既然新的版本选择了新的加密方式,总有他的理由,所以应该改的还是spring的使用姿势;目前还没找到匹配上面这种配置方式的解决方案;

本文选择的是用url的方式指定加密方式来解决这个问题,当然研究下后面这种方式内部实现,应该就能知道前面的可以怎么解决,这点记下来,后续再开坑填

III. 其他

0. 项目

2 - 2.查询基本使用姿势

学习一个新的数据库,一般怎么下手呢?基本的CURD没跑了,当可以熟练的增、删、改、查一个数据库时,可以说对这个数据库算是入门了,如果需要更进一步的话,就需要了解下数据库的特性,比如索引、事物、锁、分布式支持等

本篇博文为mongodb的入门篇,将介绍一下基本的查询操作,在Spring中可以怎么玩

I. 基本使用

0. 环境准备

在正式开始之前,先准备好环境,搭建好工程,对于这一步的详细信息,可以参考博文: 181213-SpringBoot高级篇MongoDB之基本环境搭建与使用

接下来,在一个集合中,准备一下数据如下,我们的基本查询范围就是这些数据

data

1. 根据字段进行查询

最常见的查询场景,比如我们根据查询user=一灰灰blog的数据,这里主要会使用Query + Criteria 来完成

@Component
public class MongoReadWrapper {
    private static final String COLLECTION_NAME = "demo";

    @Autowired
    private MongoTemplate mongoTemplate;

    /**
     * 指定field查询
     */
    public void specialFieldQuery() {
        Query query = new Query(Criteria.where("user").is("一灰灰blog"));
        // 查询一条满足条件的数据
        Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME);
        System.out.println("query: " + query + " | specialFieldQueryOne: " + result);

        // 满足所有条件的数据
        List<Map> ans = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
        System.out.println("query: " + query + " | specialFieldQueryAll: " + ans);
    }
}

上面是一个实际的case,从中可以知道一般的查询方式为:

  • Criteria.where(xxx).is(xxx)来指定具体的查询条件
  • 封装Query对象 new Query(criteria)
  • 借助mongoTemplate执行查询 mongoTemplate.findOne(query, resultType, collectionName)

其中findOne表示只获取一条满足条件的数据;find则会将所有满足条件的返回;上面执行之后,输出结果如

query: Query: { "user" : "一灰灰blog" }, Fields: { }, Sort: { } | specialFieldQueryOne: {_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}
query: Query: { "user" : "一灰灰blog" }, Fields: { }, Sort: { } | specialFieldQueryAll: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]

2. and多条件查询

前面是只有一个条件满足,现在如果是要求同时满足多个条件,则利用org.springframework.data.mongodb.core.query.Criteria#and来斜街多个查询条件

/**
 * 多个查询条件同时满足
 */
public void andQuery() {
    Query query = new Query(Criteria.where("user").is("一灰灰blog").and("age").is(18));
    Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | andQuery: " + result);
}

输出结果如下

query: Query: { "user" : "一灰灰blog", "age" : 18 }, Fields: { }, Sort: { } | andQuery: {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}

3. or或查询

and对应的就是or,多个条件中只要一个满足即可,这个与and的使用有些区别, 借助org.springframework.data.mongodb.core.query.Criteria#orOperator来实现,传参为多个Criteria对象,其中每一个表示一种查询条件

/**
 * 或查询
 */
public void orQuery() {
    // 等同于 db.getCollection('demo').find({"user": "一灰灰blog", $or: [{ "age": 18}, { "sign": {$exists: true}}]})
    Query query = new Query(Criteria.where("user").is("一灰灰blog")
            .orOperator(Criteria.where("age").is(18), Criteria.where("sign").exists(true)));
    List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | orQuery: " + result);

    // 单独的or查询
    // 等同于Query: { "$or" : [{ "age" : 18 }, { "sign" : { "$exists" : true } }] }, Fields: { }, Sort: { }
    query = new Query(new Criteria().orOperator(Criteria.where("age").is(18), Criteria.where("sign").exists(true)));
    result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | orQuery: " + result);
}

执行后输出结果为

query: Query: { "user" : "一灰灰blog", "$or" : [{ "age" : 18 }, { "sign" : { "$exists" : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]
query: Query: { "$or" : [{ "age" : 18 }, { "sign" : { "$exists" : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3b0538e3ac8e8d2d392390, user=二灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]

4. in查询

标准的in查询case

/**
 * in查询
 */
public void inQuery() {
    // 相当于:
    Query query = new Query(Criteria.where("age").in(Arrays.asList(18, 20, 30)));
    List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | inQuery: " + result);
}

输出

query: Query: { "age" : { "$in" : [18, 20, 30] } }, Fields: { }, Sort: { } | inQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]

5. 数值比较

数值的比较大小,主要使用的是 get, gt, lt, let

/**
 * 数字类型,比较查询 >
 */
public void compareBigQuery() {
    // age > 18
    Query query = new Query(Criteria.where("age").gt(18));
    List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | compareBigQuery: " + result);

    // age >= 18
    query = new Query(Criteria.where("age").gte(18));
    result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | compareBigQuery: " + result);
}

/**
 * 数字类型,比较查询 <
 */
public void compareSmallQuery() {
    // age < 20
    Query query = new Query(Criteria.where("age").lt(20));
    List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | compareSmallQuery: " + result);

    // age <= 20
    query = new Query(Criteria.where("age").lte(20));
    result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | compareSmallQuery: " + result);
}

输出

query: Query: { "age" : { "$gt" : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]
query: Query: { "age" : { "$gte" : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]
query: Query: { "age" : { "$lt" : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}]
query: Query: { "age" : { "$lte" : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]

6. 正则查询

牛逼高大上的功能

/**
 * 正则查询
 */
public void regexQuery() {
    Query query = new Query(Criteria.where("user").regex("^一灰灰blog"));
    List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | regexQuery: " + result);
}

输出

query: Query: { "user" : { "$regex" : "^一灰灰blog", "$options" : "" } }, Fields: { }, Sort: { } | regexQuery: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afacde3ac8e8d2d392389, user=一灰灰blog2, desc=帅气逼人的码农界老秀2}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afafbe3ac8e8d2d39238b, user=一灰灰blog4, desc=帅气逼人的码农界老秀4}, {_id=5c3afb0ae3ac8e8d2d39238c, user=一灰灰blog5, desc=帅气逼人的码农界老秀5}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]

7. 查询总数

统计常用,这个主要利用的是mongoTemplate.count方法

/**
 * 查询总数
 */
public void countQuery() {
    Query query = new Query(Criteria.where("user").is("一灰灰blog"));
    long cnt = mongoTemplate.count(query, COLLECTION_NAME);
    System.out.println("query: " + query + " | cnt " + cnt);
}

输出

query: Query: { "user" : "一灰灰blog" }, Fields: { }, Sort: { } | cnt 5

8. 分组查询

这个对应的是mysql中的group查询,但是在mongodb中,更多的是通过聚合查询,可以完成很多类似的操作,下面借助聚合,来看一下分组计算总数怎么玩

/*
 * 分组查询
 */
public void groupQuery() {
    // 根据用户名进行分组统计,每个用户名对应的数量
    // aggregate([ { "$group" : { "_id" : "user" , "userCount" : { "$sum" : 1}}}] )
    Aggregation aggregation = Aggregation.newAggregation(Aggregation.group("user").count().as("userCount"));
    AggregationResults<Map> ans = mongoTemplate.aggregate(aggregation, COLLECTION_NAME, Map.class);
    System.out.println("query: " + aggregation + " | groupQuery " + ans.getMappedResults());
}

注意下,这里用Aggregation而不是前面的QueryCriteria,输出如下

query: { "aggregate" : "__collection__", "pipeline" : [{ "$group" : { "_id" : "$user", "userCount" : { "$sum" : 1 } } }] } | groupQuery [{_id=一灰灰blog, userCount=5}, {_id=一灰灰blog2, userCount=1}, {_id=一灰灰blog4, userCount=1}, {_id=二灰灰blog, userCount=1}, {_id=一灰灰blog5, userCount=1}]

9. 排序

sort,比较常见的了,在mongodb中有个有意思的地方在于某个字段,document中并不一定存在,这是会怎样呢?

/**
 * 排序查询
 */
public void sortQuery() {
    // sort查询条件,需要用with来衔接
    Query query = Query.query(Criteria.where("user").is("一灰灰blog")).with(Sort.by("age"));
    List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | sortQuery " + result);
}

输出结果如下,对于没有这个字段的document也被查出来了

query: Query: { "user" : "一灰灰blog" }, Fields: { }, Sort: { "age" : 1 } | sortQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]

10. 分页

数据量多的时候,分页查询比较常见,用得多就是limit和skip了

/**
 * 分页查询
 */
public void pageQuery() {
    // limit限定查询2条
    Query query = Query.query(Criteria.where("user").is("一灰灰blog")).with(Sort.by("age")).limit(2);
    List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | limitPageQuery " + result);


    // skip()方法来跳过指定数量的数据
    query = Query.query(Criteria.where("user").is("一灰灰blog")).with(Sort.by("age")).skip(2);
    result = mongoTemplate.find(query, Map.class, COLLECTION_NAME);
    System.out.println("query: " + query + " | skipPageQuery " + result);
}

输出结果表明,limit用来限制查询多少条数据,skip则表示跳过前面多少条数据

query: Query: { "user" : "一灰灰blog" }, Fields: { }, Sort: { "age" : 1 } | limitPageQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}]
query: Query: { "user" : "一灰灰blog" }, Fields: { }, Sort: { "age" : 1 } | skipPageQuery [{_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]

11. 小结

上面给出的一些常见的查询姿势,当然并不全面,比如我们如果需要查询document中的部分字段怎么办?比如document内部结果比较复杂,有内嵌的对象或者数组时,嵌套查询可以怎么玩?索引什么的又可以怎么利用起来,从而优化查询效率?如何通过传说中自动生成的_id来获取文档创建的时间戳?

先留着这些疑问,后面再补上

II. 其他

0. 项目

3 - 3.如何新增文档

本篇博文为mongodb的curd中一篇,前面介绍简单的查询使用,这一篇重点则放在插入数据;

I. 基本使用

首先是准备好基本环境,可以参考博文

1. 新增一条数据

MongoDB一个基本数据称为document,和mysql不一样,没有强制约束哪些字段,可以随意的插入,下面是一个简单的插入演示

private static final String COLLECTION_NAME = "demo";

@Autowired
private MongoTemplate mongoTemplate;

/**
 * 新增一条记录
 */
public void insert() {
    JSONObject object = new JSONObject();
    object.put("name", "一灰灰blog");
    object.put("desc", "欢迎关注一灰灰Blog");
    object.put("age", 28);

    // 插入一条document
    mongoTemplate.insert(object, COLLECTION_NAME);


    JSONObject ans = mongoTemplate
            .findOne(new Query(Criteria.where("name").is("一灰灰blog").and("age").is(28)), JSONObject.class,
                    COLLECTION_NAME);
    System.out.println(ans);
}

使用的关键地方为一行: mongoTemplate.insert(object, COLLECTION_NAME);

  • 第一个参数为待插入的document
  • 第二个参数为collection name (相当于mysql的table)

执行后输出结果为如下

{"name":"一灰灰blog","_id":{"counter":12472353,"date":1548333180000,"machineIdentifier":14006254,"processIdentifier":17244,"time":1548333180000,"timeSecond":1548333180,"timestamp":1548333180},"age":28,"desc":"欢迎关注一灰灰Blog"}

2. 批量插入

一次插入多条记录,传集合进去即可

/**
 * 批量插入
 */
public void insertMany() {
    List<Map<String, Object>> records = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        Map<String, Object> record = new HashMap<>(4);
        record.put("wechart", "一灰灰blog");
        record.put("blog", Arrays.asList("http://spring.hhui.top", "http://blog.hhui.top"));
        record.put("nums", 210);
        record.put("t_id", i);
        records.add(record);
    }

    // 批量插入文档
    mongoTemplate.insert(records, COLLECTION_NAME);

    // 查询插入的内容
    List<Map> result =
            mongoTemplate.find(new Query(Criteria.where("wechart").is("一灰灰blog")), Map.class, COLLECTION_NAME);
    System.out.println("Query Insert Records: " + result);
}

返回结果如下:

Query Insert Records: [{t_id=0, wechart=一灰灰blog, _id=5c49b07cd5b7ee435cbe5022, blog=[http://spring.hhui.top, http://blog.hhui.top], nums=210}, {t_id=1, wechart=一灰灰blog, _id=5c49b07cd5b7ee435cbe5023, blog=[http://spring.hhui.top, http://blog.hhui.top], nums=210}, {t_id=2, wechart=一灰灰blog, _id=5c49b07cd5b7ee435cbe5024, blog=[http://spring.hhui.top, http://blog.hhui.top], nums=210}]

3. upsert,不存在才插入

我们希望在插入之前,判断数据是否存在,如果不存在则插入;如果存在则更新;此时就可以采用upsert来使用,一般三个参数

mongoTemplate.upsert(Query query, Update update, String collectionName)

第一个为查询条件,第二个为需要更新的字段,最后一个指定对应的collection,一个简单的实例如下

/**
 * 数据不存在,通过 upsert 新插入一条数据
 *
 * set 表示修改key对应的value
 * addToSet 表示在数组中新增一条
 */
public void upsertNoMatch() {
    // addToSet 表示将数据塞入document的一个数组成员中
    UpdateResult upResult = mongoTemplate.upsert(new Query(Criteria.where("name").is("一灰灰blog").and("age").is(100)),
            new Update().set("age", 120).addToSet("add", "额外增加"), COLLECTION_NAME);
    System.out.println("nomatch upsert return: " + upResult);

    List<JSONObject> re = mongoTemplate
            .find(new Query(Criteria.where("name").is("一灰灰blog").and("age").is(120)), JSONObject.class,
                    COLLECTION_NAME);
    System.out.println("after upsert return should not be null: " + re);
    System.out.println("------------------------------------------");
}

输出结果如下:

nomatch upsert return: AcknowledgedUpdateResult{matchedCount=0, modifiedCount=0, upsertedId=BsonObjectId{value=5c49b07ce6652f7e1add1ea2}}
after upsert return should not be null: [{"add":["额外增加"],"name":"一灰灰blog","_id":{"counter":14491298,"date":1548333180000,"machineIdentifier":15099183,"processIdentifier":32282,"time":1548333180000,"timeSecond":1548333180,"timestamp":1548333180},"age":120}]
------------------------------------------

4. upsert,存在则更新

前面的demo是演示不存在,那么存在数据呢?

/**
 * 只有一条数据匹配,upsert 即表示更新
 */
public void upsertOneMatch() {
    // 数据存在,使用更新
    UpdateResult result = mongoTemplate.upsert(new Query(Criteria.where("name").is("一灰灰blog").and("age").is(120)),
            new Update().set("age", 100), COLLECTION_NAME);
    System.out.println("one match upsert return: " + result);

    List<JSONObject> ans = mongoTemplate
            .find(new Query(Criteria.where("name").is("一灰灰blog").and("age").is(100)), JSONObject.class,
                    COLLECTION_NAME);
    System.out.println("after update return should be one: " + ans);
    System.out.println("------------------------------------------");
}

输出结果如下,注意下面的输出数据的 _id,正视前面插入的那条数据,两个数据唯一的不同,就是age被修改了

one match upsert return: AcknowledgedUpdateResult{matchedCount=1, modifiedCount=1, upsertedId=null}
after update return should be null: [{"add":["额外增加"],"name":"一灰灰blog","_id":{"counter":14491298,"date":1548333180000,"machineIdentifier":15099183,"processIdentifier":32282,"time":1548333180000,"timeSecond":1548333180,"timestamp":1548333180},"age":100}]

5. upsert,多条满足时

如果query条件命中多条数据,怎么办?会修改几条数据呢?

/**
 * 两条数据匹配时,upsert 将只会更新一条数据
 */
public void upsertTwoMatch() {
    // 多条数据满足条件时,只会修改一条数据
    System.out.println("------------------------------------------");
    List<JSONObject> re = mongoTemplate
            .find(new Query(Criteria.where("name").is("一灰灰blog").and("age").in(Arrays.asList(28, 100))),
                    JSONObject.class, COLLECTION_NAME);
    System.out.println("original record: " + re);

    UpdateResult result = mongoTemplate
            .upsert(new Query(Criteria.where("name").is("一灰灰blog").and("age").in(Arrays.asList(28, 100))),
                    new Update().set("age", 120), COLLECTION_NAME);
    System.out.println("two match upsert return: " + result);

    re = mongoTemplate.find(new Query(Criteria.where("name").is("一灰灰blog").and("age").is(120)), JSONObject.class,
            COLLECTION_NAME);
    System.out.println("after upsert return size should be 1: " + re);
    System.out.println("------------------------------------------");
}

根据实际输出进行查看,发现只有一条数据被修改;另外一条保持不变,结果如下

------------------------------------------
original record: [{"name":"一灰灰blog","_id":{"counter":12472353,"date":1548333180000,"machineIdentifier":14006254,"processIdentifier":17244,"time":1548333180000,"timeSecond":1548333180,"timestamp":1548333180},"age":28,"desc":"欢迎关注一灰灰Blog"}, {"add":["额外增加"],"name":"一灰灰blog","_id":{"counter":14491298,"date":1548333180000,"machineIdentifier":15099183,"processIdentifier":32282,"time":1548333180000,"timeSecond":1548333180,"timestamp":1548333180},"age":100}]
two match upsert return: AcknowledgedUpdateResult{matchedCount=1, modifiedCount=1, upsertedId=null}
after upsert return size should be 1: [{"name":"一灰灰blog","_id":{"counter":12472353,"date":1548333180000,"machineIdentifier":14006254,"processIdentifier":17244,"time":1548333180000,"timeSecond":1548333180,"timestamp":1548333180},"age":120,"desc":"欢迎关注一灰灰Blog"}]
------------------------------------------

II. 其他

0. 项目

相关博文

4 - 4.修改基本使用姿势

本篇依然是MongoDB curd中的一篇,主要介绍document的更新,主要内容如下

  • 常见类型成员的修改
  • 数组类型成员的增删改
  • document类型成员的增删改

I. 基本使用

首先是准备好基本环境,可以参考博文

在开始之前,先封装一个输出方法,用于打印修改后的record对象

private void queryAndPrint(Query query, String tag) {
    System.out.println("------------- after " + tag + " age -------------");
    Map record = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME);
    System.out.println("query records: " + record);
    System.out.println("-------------  end " + tag + " age --------------\n");
}

1. 基本类型修改

mongodb支持我们常见的各种基本类型,而MongoTemplate也封装了不少对应的修改方法,最基础的修改,主要是借助Update来实现

常见的使用姿势如:

a. 基本使用姿势

public void basicUpdate() {
    /*
     * {
     *     "_id" : ObjectId("5c49b07ce6652f7e1add1ea2"),
     *     "age" : 100,
     *     "name" : "一灰灰blog",
     *     "desc" : "Java Developer",
     *     "add" : [
     *         "额外增加"
     *     ],
     *     "date" : ISODate("2019-01-28T08:00:08.373Z"),
     *     "doc" : {
     *         "key" : "小目标",
     *         "value" : "升职加薪,迎娶白富美"
     *     }
     * }
     */

    // 1. 直接修改值的内容
    Query query = new Query(Criteria.where("_id").is("5c49b07ce6652f7e1add1ea2"));

    Update update = new Update().set("desc", "Java & Python Developer");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "set");
}

输出结果为:

------------- after set age -------------
query records: {_id=5c49b07ce6652f7e1add1ea2, age=100, name=一灰灰blog, desc=Java & Python Developer, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}}
-------------  end set age --------------

b. 数字增加/减少

数字类型修改,使用 org.springframework.data.mongodb.core.query.Update#inc

// 数字修改,实现添加or减少
Update numUp = new Update().inc("age", 20L);
mongoTemplate.updateFirst(query, numUp, COLLECTION_NAME);
queryAndPrint(query, "inc");

输出结果为:

------------- after inc age -------------
query records: {_id=5c49b07ce6652f7e1add1ea2, age=120, name=一灰灰blog, desc=Java & Python Developer, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}}
-------------  end inc age --------------

c. 数字比较修改

数字简单比较之后修改,如org.springframework.data.mongodb.core.query.Update#max

// 数字比较修改
Update cmpUp = new Update().max("age", 88);
mongoTemplate.updateFirst(query, cmpUp, COLLECTION_NAME);
queryAndPrint(query, "cmp");

输出结果

------------- after cmp age -------------
query records: {_id=5c49b07ce6652f7e1add1ea2, age=120, name=一灰灰blog, desc=Java & Python Developer, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}}
-------------  end cmp age --------------

d. 乘法

乘法运算, 主要使用 org.springframework.data.mongodb.core.query.Update#multiply

// 乘法
Update mulUp = new Update().multiply("age", 3);
mongoTemplate.updateFirst(query, mulUp, COLLECTION_NAME);
queryAndPrint(query, "multiply");

输出结果

------------- after multiply age -------------
query records: {_id=5c49b07ce6652f7e1add1ea2, age=360.0, name=一灰灰blog, desc=Java & Python Developer, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}}
-------------  end multiply age --------------

e. 日期修改

日期修改, 如 org.springframework.data.mongodb.core.query.Update#currentDate

// 日期修改
Update dateUp = new Update().currentDate("date");
mongoTemplate.updateFirst(query, dateUp, COLLECTION_NAME);
queryAndPrint(query, "date");

输出结果

------------- after date age -------------
query records: {_id=5c49b07ce6652f7e1add1ea2, age=360.0, name=一灰灰blog, desc=Java & Python Developer, add=[额外增加], date=Mon Feb 18 19:34:56 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}}
-------------  end date age --------------

2. field修改

不同于mysql的列表是固定的,mongodb的field可以增加、删除和重命名,下面分别看下三种case如何使用

/**
 * 修改字段名,新增字段,删除字段
 */
public void fieldUpdate() {
    /**
     * {
     *     "_id" : ObjectId("5c6a7ada10ffc647d301dd62"),
     *     "age" : 28.0,
     *     "name" : "一灰灰blog",
     *     "desc" : "Java Developer",
     *     "add" : [
     *         "额外增加"
     *     ],
     *     "date" : ISODate("2019-01-28T08:00:08.373Z"),
     *     "doc" : {
     *         "key" : "小目标",
     *         "value" : "升职加薪,迎娶白富美"
     *     }
     * }
     */
    Query query = new Query(Criteria.where("_id").is("5c6a7ada10ffc647d301dd62"));
    renameFiled(query);

    addField(query);
    delField(query);
}

a. 重命名

利用org.springframework.data.mongodb.core.query.Update#rename来实现重命名,需要注意的是,当修改的docuemnt没有这个成员时,相当于没有任务操作

private void renameFiled(Query query) {
    Update update = new Update().rename("desc", "skill");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);

    queryAndPrint(query, "rename");

    // 如果字段不存在,相当于没有更新
    update = new Update().rename("desc", "s-skill");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "rename Not exists!");
}

输出结果如下,后面一个语句相当于没有执行

------------- after rename age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end rename age --------------

------------- after rename Not exists! age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end rename Not exists! age --------------

b. 新增成员

新增也是直接利用的Update#set方法,当存在时,修改;不存在时,添加

  • 另外提一下setOnInsert, 如果要更新的文档存在那么$setOnInsert操作符不做任何处理;
private void addField(Query query) {
    // 新增一个字段
    // 直接使用set即可
    Update update = new Update().set("new-skill", "Python");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);

    queryAndPrint(query, "addField");

    // 当更新一个不存在的文档时,可以使用setOnInsert
    // 如果要更新的文档存在那么$setOnInsert操作符不做任何处理;
}

输出结果如下:

------------- after addField age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer, new-skill=Python}
-------------  end addField age --------------

c. 删除成员

删除document中的某个成员,借助org.springframework.data.mongodb.core.query.Update#unset, 正好与添加对上

private void delField(Query query) {
    // 删除字段,如果不存在,则不操作
    Update update = new Update().unset("new-skill");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);

    queryAndPrint(query, "delField");
}

输出结果如下

------------- after delField age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end delField age --------------

3. 数组操作

在MongoDB的document中,有两个有意思的类型,一个是数组,一个是document(即可以嵌套),这里则主要介绍下如何操作数组中的成员

/**
 * 更新文档中字段为数组成员的值
 */
public void updateInnerArray() {
    /**
     * {
     *     "_id" : ObjectId("5c6a7ada10ffc647d301dd62"),
     *     "age" : 28.0,
     *     "name" : "一灰灰blog",
     *     "skill" : "Java Developer",
     *     "add" : [
     *         "额外增加"
     *     ],
     *     "date" : ISODate("2019-01-28T08:00:08.373Z"),
     *     "doc" : {
     *         "key" : "小目标",
     *         "value" : "升职加薪,迎娶白富美"
     *     }
     * }
     */
    Query query = new Query(Criteria.where("_id").is("5c6a7ada10ffc647d301dd62"));
    this.addData2Array(query);
    this.batchAddData2Array(query);
    this.delArrayData(query);
    this.updateArrayData(query);
}

a. 添加到数组中

在数组中新增一个数据,提供了两种方式,一个是org.springframework.data.mongodb.core.query.Update#addToSet(java.lang.String, java.lang.Object),一个是org.springframework.data.mongodb.core.query.Update#push(java.lang.String, java.lang.Object);两个的区别在于前者不能插入重复数据,后者可以

private void addData2Array(Query query) {
    // 新加一个元素到数组,如果已经存在,则不会加入
    String insert = "新添加>>" + System.currentTimeMillis();
    Update update = new Update().addToSet("add", insert);
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "add2List");

    // push 新增元素,允许出现重复的数据
    update = new Update().push("add", 10);
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "push2List");
}

输出结果

------------- after add2List age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加, 新添加>>1550489696892], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end add2List age --------------

------------- after push2List age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加, 新添加>>1550489696892, 10], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end push2List age --------------

b. 批量添加

一次添加多个,借助addToSeteach来实现

private void batchAddData2Array(Query query) {
    // 批量插入数据到数组中, 注意不会将重复的数据丢入mongo数组中
    Update update = new Update().addToSet("add").each("2", "2", "3");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "batchAdd2List");
}

输出结果:

------------- after batchAdd2List age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加, 新添加>>1550489696892, 10, 2, 3], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end batchAdd2List age --------------

c. 删除

借助pull来精确删除某个值

private void delArrayData(Query query) {
    // 删除数组中元素
    Update update = new Update().pull("add", "2");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "delArrayData");
}

输出如下,注意对比,2没有了

------------- after delArrayData age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加, 新添加>>1550489696892, 10, 3], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end delArrayData age --------------

d. 修改

修改,首先的问题是要定位,确定删除数组中某个下标的元素,这里借助了一个有意思的站位

  • 定位删除的数组元素方法: arrayKey.index
    • arrayKey 是数组在docment中的名
    • index 表示要删除的索引

一个实例如下

private void updateArrayData(Query query) {
    // 使用set,field.index 来更新数组中的值
    // 更新数组中的元素,如果元素存在,则直接更新;如果数组个数小于待更新的索引位置,则前面补null
    Update update = new Update().set("add.1", "updateField");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "updateListData");

    update = new Update().set("add.10", "nullBefore");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "updateListData");
}

输出结果,注意后面的,如果数组个数小于待更新的索引位置,则前面补null

------------- after updateListData age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加, updateField, 10, 3], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end updateListData age --------------

------------- after updateListData age -------------
query records: {_id=5c6a7ada10ffc647d301dd62, age=28.0, name=一灰灰blog, add=[额外增加, updateField, 10, 3, null, null, null, null, null, null, nullBefore], date=Mon Jan 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end updateListData age --------------

4. document操作

内嵌文档,可以所是MongoDB的一个特色了,我们则来看下如何进行操作

/**
 * 更新文档中字段为document类型的值
 */
public void updateInnerDoc() {
    /**
     * {
     *     "_id" : ObjectId("5c6a956b10ffc647d301dd63"),
     *     "age" : 18.0,
     *     "name" : "一灰灰blog",
     *     "date" : ISODate("2019-02-28T08:00:08.373Z"),
     *     "doc" : {
     *         "key" : "小目标",
     *         "value" : "升职加薪,迎娶白富美"
     *     },
     *     "skill" : "Java Developer"
     * }
     */

    Query query = new Query(Criteria.where("_id").is("5c6a956b10ffc647d301dd63"));
    this.addFieldToDoc(query);
    this.updateFieldOfDoc(query);
    this.delFieldOfDoc(query);
}

a. 添加

借助前面的站位思想,就很好实现了,定位元素的方式采用

  • docName.fieldName
    • docName 为内嵌文档在docunent中的fieldName
    • fieldName 为内嵌文档内部需要修改的fieldName
private void addFieldToDoc(Query query) {
    // 内嵌doc新增field
    Update update = new Update().set("doc.title", "好好学习,天天向上!");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "addFieldToDoc");
}

输出如下

------------- after addFieldToDoc age -------------
query records: {_id=5c6a956b10ffc647d301dd63, age=18.0, name=一灰灰blog, date=Thu Feb 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美, title=好好学习,天天向上!}, skill=Java Developer}
-------------  end addFieldToDoc age --------------

c. 修改

private void updateFieldOfDoc(Query query) {
    // 内嵌doc修改field
    Update update = new Update().set("doc.title", "新的标题:一灰灰Blog!");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "updateFieldOfDoc");
}

输出如下

------------- after updateFieldOfDoc age -------------
query records: {_id=5c6a956b10ffc647d301dd63, age=18.0, name=一灰灰blog, date=Thu Feb 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美, title=新的标题:一灰灰Blog!}, skill=Java Developer}
-------------  end updateFieldOfDoc age --------------

d. 删除

private void delFieldOfDoc(Query query) {
    // 删除内嵌doc中的field
    Update update = new Update().unset("doc.title");
    mongoTemplate.updateFirst(query, update, COLLECTION_NAME);
    queryAndPrint(query, "delFieldOfDoc");
}

输出如下

------------- after delFieldOfDoc age -------------
query records: {_id=5c6a956b10ffc647d301dd63, age=18.0, name=一灰灰blog, date=Thu Feb 28 16:00:08 CST 2019, doc={key=小目标, value=升职加薪,迎娶白富美}, skill=Java Developer}
-------------  end delFieldOfDoc age --------------

II. 其他

0. 项目