MongoDB 简介

MongoDB 是非关系型数据库中,最接近关系型数据库的,文档型数据库。它支持的查询功能非常强大。
MongoDB 是为快速开发互联网Web应用而设计的数据库系统。他的数据模型是面向文档的,这种文档是一种类似于JSON的结构,准确来说是一种支持二进制的BSON(Binary JSON)结构。

非关系性数据库

非关系性数据库 也被称为 NoSQL(Not only sql),主要有四大类:键值存储数据库、列存储数据库、文档型数据库、图形数据库。之前介绍的Redis属于键值存储数据库。

关系与非关系型数据库

关系型数据库的优点:
1 支持事务处理,事务特性:原子性、一致性、隔离性、持久性。
2 数据结构清晰,便于理解,可读性高。
3 使用方便,有标准的sql语法。

关系型数据库的缺点:
1 读写性能相对较差,为保证事务的一致性,需要一定的开销。在高并发下表现的尤为突出。
2 表结构固定,不易于表后期的扩展,所以前期对表的设计要求较高。

非关系型数据库的优点:
1 读写性能高,没有保障数据的一致性。
2 表结构灵活,表结构并不是固定的,通过key-value存储数据,value又可以存储其他格式的数据。

两者的优缺点其实是向反的,一件事物不会凭空出现,都是在原有的基础上做了补充和优化,两者的侧重点各有不同。就像MySQL保障了数据的一致性,却影响了读写的性能。MongoDB放弃数据的强一致性,保障了读写的效率。在合适的场景使用合适的数据库,是需要我们考虑的。
1 对于需要高度事务特性的系统,比如和钱有关的,银行系统,金融系统。我们要考虑使用关系型数据库,确保数据的一致性和持久性。
2 对于那些数据并不是很重要,访问量又很大的系统,比如电商平台的商品信息。我们可以使用非关系型数据库来做缓存,充分提高了系统查询的性能。

最佳实践指南

查询建议

1 查询所有数据,建议使用分页查询。
2 查询key建议用引号,对象的属性可以省略引号,内嵌的对象属性不能省略。比如下面的name可以省略,但address.province则不能。
3 尽量少用$or, $in 查询,效率很低。

插入建议:
1 插入数据不能破坏原有的数据结构,造成不必要的麻烦。
2 批量插入数据,尽量一次执行多个文档,而不是多个文档执行多次方法。

更新建议:
1 更新数据不能破坏原有的数据结构。
2 正确使用修改器完成更新操作。

SpringBoot MongoDB 整合

如果你觉得Spring整合MongoDB略显麻烦,那SpringBoot整合MongoDB就是你的福音。SpringBoot旨在零配置,只需简单的两个步骤即可。
第一步:在pom.xml文件中添加spring-boot-starter-data-mongodb

<dependency>    <!-- 添加对mongodb的支持 -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

第二步:在application.properties文件中配置MongoDB数据库链接地址。
链接MongoDB数据库地址规则:spring.data.mongodb.uri=mongodb://account:password@ip:port/database 其中 account和password方便是链接数据库的账号和密码。而database是需要链接的数据库地址

# 没有账号密码可以简写
spring.data.mongodb.uri=mongodb://localhost:27017/itdragonstu

Spring Data MongoDB 编程

Spring Data给我们提供了MongoTemplate类,极大的方便了我们的工作,但是若每个实体类都基于MongoTemplate重写一套CRUD的实现类,似乎显得有些笨重。于是我们可以将其简单的封装一下。步骤如下

第一步:创建用户实体类,其数据库表名就是类名首字母小写。
第二步:封装MongoTemplate类,实现增删改查,分页,排序,主键自增等常用功能。
第三步:创建封装类的Bean管理类,针对不同的实体类,需要配置不同的bean。
第四步:创建测试类,测试:注册,更新,分页,排序,查询用户功能。

创建用户实体类

用户实体类有五个字段,除了主键ID,其他四个分别代表四个常用的类型(字符串,数字,对象,集合)。为了简化开发,实体类建议不实用@Document注解重命名User在MongoDB数据库中的表名。
省略get/set方法和toString方法

import java.io.Serializable;
import java.util.ArrayList;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
/**
 * 用户实体类
 * @author itdragon
 */
//@Document(collection = "itdragon_user")  如果为了代码的通用性,建议不要使用
public class User implements Serializable{
    
    private static final long serialVersionUID = 1L;
    @Id
    private Long id;
    private String name;
    private Integer age;
    private Address address;
    private ArrayList ability;
}

public class Address{
    private Long id;
    private String province;
    private String city;
}

封装MongoTemplate类

SpringData提供的MongoTemplate类,极大的方便我们操作MongoDB数据库。可是它的很多方法都涉及到了Class,和CollectionName。针对不同的实体类,我们需要重复写不同的方法。这里,我们进一步封装,实现代码的高可用。
实现的思路大致:将Class作为一个参数,在初始化MongoTemplate的封装类时赋值。这里有一个约束条件是:CollectionName是Class类名的首字母小写。

import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;

@Repository
@SuppressWarnings({"unchecked", "rawtypes"})
public class ITDragonMongoHelper {
    
    @Autowired(required = false)
    private MongoTemplate mongoTemplate; 
    private Class entityClass;      // 实体类
    private String collectionName;  // 数据库表名
    private String orderAscField;   // 升序字段
    private String orderDescField;  // 降序字段
    
    private static final String ID = "id";
    private static final String MONGODB_ID = "_id";
    
    public ITDragonMongoHelper() {
    }

    public ITDragonMongoHelper(Class entityClass) {
        this.entityClass = entityClass;
        this.collectionName = _getCollectionName();
    }

    public ITDragonMongoHelper(Class entityClass, String collectionName) {
        this.entityClass = entityClass;
        this.collectionName = collectionName;
    }
    
    /**
     * @Title save
     * @Description 通过Map创建实体类
     * @param object Map,无需自带ID
     * @return
     */
    public Boolean save(Map<String, Object> requestArgs) {
        try {
            Object object = getEntityClass().newInstance();
            if (null == requestArgs.get(ID)) {
                requestArgs.put(ID, getNextId());
            }
            BeanUtils.populate(object, requestArgs);
            saveOrUpdate(object);
        } catch (Exception e) {
            e.printStackTrace();
            return Boolean.valueOf(false);
        }
        return Boolean.valueOf(true);
    }
    
    /**
     * @Title save
     * @Description 通过对象创建实体类
     * @param object 实体类,需自带ID
     * @return
     */
    public Boolean saveOrUpdate(Object object) {
        try {
            this.mongoTemplate.save(object, this.collectionName);
        } catch (Exception e) {
            e.printStackTrace();
            return Boolean.valueOf(false);
        }
        return Boolean.valueOf(true);
    }
    
    /**
     * @Title update
     * @Description 通过Map更新实体类具体字段,可以减少更新出错字段,执行的销率更高,需严格要求数据结构的正确性
     * @param requestArgs Map,需自带ID, 形如:{id: idValue, name: nameValue, ....}
     * @return
     */
    public Boolean update(Map<String, Object> requestArgs) {
        Object id = requestArgs.get(ID);
        if (null == id) {
            return Boolean.valueOf(false);
        }
        try {
            Update updateObj = new Update();
            requestArgs.remove(ID);
            for (String key : requestArgs.keySet()) {
                updateObj.set(key, requestArgs.get(key));
            }
            findAndModify(Criteria.where(ID).is(id), updateObj);
        } catch (Exception e) {
            e.printStackTrace();
            return Boolean.valueOf(false);
        }
        return Boolean.valueOf(true);
    }
    
    /**
     * @Title find
     * @Description 根据查询条件返回所有数据,不推荐
     * @param criteria 查询条件
     * @return
     */
    public List find(Criteria criteria) {
        Query query = new Query(criteria);
        _sort(query);
        return this.mongoTemplate.find(query, this.entityClass, this.collectionName);
    }
    
    /**
     * @Title find
     * @Description 根据查询条件返回指定数量数据
     * @param criteria 查询条件
     * @param pageSize 查询数量
     * @return
     */
    public List find(Criteria criteria, Integer pageSize) {
        Query query = new Query(criteria).limit(pageSize.intValue());
        _sort(query);
        return this.mongoTemplate.find(query, this.entityClass, this.collectionName);
    }

    /**
     * @Title find
     * @Description 根据查询条件分页返回指定数量数据
     * @param criteria 查询条件
     * @param pageSize 查询数量
     * @param pageNumber 当前页数
     * @return
     */
    public List find(Criteria criteria, Integer pageSize, Integer pageNumber) {
        Query query = new Query(criteria).skip((pageNumber.intValue() - 1) * pageSize.intValue()).limit(pageSize.intValue());
        _sort(query);
        return this.mongoTemplate.find(query, this.entityClass, this.collectionName);
    }
    
    public Object findAndModify(Criteria criteria, Update update) {
        // 第一个参数是查询条件,第二个参数是需要更新的字段,第三个参数是需要更新的对象,第四个参数是MongoDB数据库中的表名
        return this.mongoTemplate.findAndModify(new Query(criteria), update, this.entityClass, this.collectionName);
    }
    
    /**
     * @Title findById
     * @Description 通过ID查询数据
     * @param id 实体类ID
     * @return
     */
    public Object findById(Object id) {
        return this.mongoTemplate.findById(id, this.entityClass, this.collectionName);
    }
    
    /**
     * @Title findOne
     * @Description 通过查询条件返回一条数据
     * @param id 实体类ID
     * @return
     */
    public Object findOne(Criteria criteria) {
        Query query = new Query(criteria).limit(1);
        _sort(query);
        return this.mongoTemplate.findOne(query, this.entityClass, this.collectionName);
    }
    
    // id自增长
    public String getNextId() {
        return getNextId(getCollectionName());
    }

    public String getNextId(String seq_name) {
        String sequence_collection = "seq";
        String sequence_field = "seq";
        DBCollection seq = this.mongoTemplate.getCollection(sequence_collection);
        DBObject query = new BasicDBObject();
        query.put(MONGODB_ID, seq_name);
        DBObject change = new BasicDBObject(sequence_field, Integer.valueOf(1));
        DBObject update = new BasicDBObject("$inc", change);
        DBObject res = seq.findAndModify(query, new BasicDBObject(), new BasicDBObject(), false, update, true, true);
        return res.get(sequence_field).toString();
    }
    
    private void _sort(Query query) {
        if (null != this.orderAscField) {
            String[] fields = this.orderAscField.split(",");
            for (String field : fields) {
                if (ID.equals(field)) {
                    field = MONGODB_ID;
                }
                query.with(new Sort(Sort.Direction.ASC, new String[] { field }));
            }
        } else {
            if (null == this.orderDescField) {
                return;
            }
            String[] fields = this.orderDescField.split(",");
            for (String field : fields) {
                if (ID.equals(field)) {
                    field = MONGODB_ID;
                }
                query.with(new Sort(Sort.Direction.DESC, new String[] { field }));
            }
        }
    }
    
    // 获取Mongodb数据库中的表名,若表名不是实体类首字母小写,则会影响后续操作
    private String _getCollectionName() {
        String className = this.entityClass.getName();
        Integer lastIndex = Integer.valueOf(className.lastIndexOf("."));
        className = className.substring(lastIndex.intValue() + 1);
        return StringUtils.uncapitalize(className);
    }
    
    public Class getEntityClass() {
        return entityClass;
    }
    public void setEntityClass(Class entityClass) {
        this.entityClass = entityClass;
    }
    public String getCollectionName() {
        return collectionName;
    }
    public void setCollectionName(String collectionName) {
        this.collectionName = collectionName;
    }
    public String getOrderAscField() {
        return orderAscField;
    }
    public void setOrderAscField(String orderAscField) {
        this.orderAscField = orderAscField;
    }
    public String getOrderDescField() {
        return orderDescField;
    }
    public void setOrderDescField(String orderDescField) {
        this.orderDescField = orderDescField;
    }
}

创建封装类的Bean管理类

这里用Bean注解修饰的方法名和测试类中ITDragonMongoHelper 的变量名要保持一致。这样才能具体知道是哪个实体类的数据操作。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.itdragon.pojo.User;
import com.itdragon.repository.ITDragonMongoHelper;

/**
 * ITDragonMongoHelper的bean配置管理类 
 * @author itdragon
 */
@Configuration
public class MongodbBeansConfig {
    
    @Bean // 该方法名很重要
    public ITDragonMongoHelper userMongoHelper() {
        return new ITDragonMongoHelper(User.class);
    }

}

MongoDB的测试类

主要测试MongoDB保存数据,更新字符串,更新数值,更新对象(文档),更新集合,分页查询几个常用方法。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.test.context.junit4.SpringRunner;
import com.itdragon.StartApplication;
import com.itdragon.pojo.Address;
import com.itdragon.pojo.User;
import com.itdragon.repository.ITDragonMongoHelper;
/**
 * @RunWith 它是一个运行器
 * @RunWith(SpringRunner.class) 表示让测试运行于Spring测试环境,不用启动spring容器即可使用Spring环境
 * @SpringBootTest(classes=StartApplication.class)  表示将StartApplication.class纳入到测试环境中,若不加这个则提示bean找不到。
 * @author itdragon
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes=StartApplication.class)
public class SpringbootStudyApplicationTests {
    @Autowired
    private ITDragonMongoHelper userMongoHelper; // 命名规则:需和MongodbBeansConfig配置Bean的方法名一致
    @Test
    public void createUser() {
        System.out.println("^^^^^^^^^^^^^^^^^^^^^^createUser");
        for (int i = 0; i < 25; i++) {  // 插入25条数据
            User user = new User();
            user.setId(Long.valueOf(userMongoHelper.getNextId(User.class.getName())));
            user.setAge(25 + i);
            user.setName("itdragon-" + i);
            Address address = new Address();
            address.setId(Long.valueOf(userMongoHelper.getNextId(Address.class.getName()))); 
            address.setProvince("湖北省");
            address.setCity("武汉市");
            user.setAddress(address);
            ArrayList<String> ability = new ArrayList<>();
            ability.add("Java");
            user.setAbility(ability);
            userMongoHelper.saveOrUpdate(user);
            System.out.println("user : " + user.toString());
        }
    }
    @Test
    public void updateUserName() {
        System.out.println("^^^^^^^^^^^^^^^^^^^^^^updateUserName");
        Map<String, Object> updateMap = new HashMap<>();
        // 查询name为itdragon-1的数据,将name修改为ITDragonBlog
        User user = (User) userMongoHelper.findOne(Criteria.where("name").is("itdragon-1"));
        if (null == user) {
            System.out.println("^^^^^^^^^^^^^^^^^^^^^^User non-existent");
            return ;
        }
        updateMap.put("id", user.getId());
        updateMap.put("name", "ITDragonBlog");
        userMongoHelper.update(updateMap);
    }
    @Test
    public void updateUserAddress() {
        System.out.println("^^^^^^^^^^^^^^^^^^^^^^updateUserAddress");
        Map<String, Object> updateMap = new HashMap<>();
        User user = (User) userMongoHelper.findOne(Criteria.where("name").is("itdragon-3"));
        if (null == user) {
            System.out.println("^^^^^^^^^^^^^^^^^^^^^^User non-existent");
            return ;
        }
        Address address = new Address();
        address.setId(Long.valueOf(userMongoHelper.getNextId(Address.class.getName()))); 
        address.setProvince("湖南省");
        address.setCity("长沙");
        updateMap.put("id", user.getId());
        updateMap.put("address", address);
        userMongoHelper.update(updateMap);
    }
    @Test
    public void updateUserAbility() {
        System.out.println("^^^^^^^^^^^^^^^^^^^^^^updateUserAbility");
        Map<String, Object> updateMap = new HashMap<>();
        User user = (User) userMongoHelper.findOne(Criteria.where("name").is("itdragon-4"));
        if (null == user) {
            System.out.println("^^^^^^^^^^^^^^^^^^^^^^User non-existent");;
            return ;
        }
        ArrayList<String> abilitys = user.getAbility();
        abilitys.add("APP");
        updateMap.put("id", user.getId());
        updateMap.put("ability", abilitys);
        userMongoHelper.update(updateMap);
    }
    @Test
    public void findUserPage() {
        System.out.println("^^^^^^^^^^^^^^^^^^^^^^findUserPage");
        userMongoHelper.setOrderAscField("age"); // 排序
        Integer pageSize = 5; // 每页页数
        Integer pageNumber = 1; // 当前页
        List<User> users = userMongoHelper.find(Criteria.where("age").gt(25), pageSize, pageNumber); // 查询age大于25的数据
        for (User user : users) {
            System.out.println("user : " + user.toString());
        }
    }
}

MongoDB开发注意事项

MongoDB对表结构要求不严,方便了我们的开发,同时也提高了犯错率,特别是公司来了新同事,这颗地雷随时都会爆炸。
第一点: MongoDB通过key获取value的值。而这个value可以是内嵌的其他文档。因为没有主外键的概念,使用起来非常方便。若嵌套的文档太深,在更新数据是,需要注意不能覆盖原来的值。比如User表中的ability是一个集合,若传一个字符串,依然可以更新成功,但已经破坏了数据结构。这是很多新手容易犯的错。

第二点: 内嵌的文档属性名最好不要重名。举个例子,如果User表中的address对象,也有一个name的属性。那么在后续写代码的过程中,极容易混淆。导致数据更新异常。

第三点: 表的设计尽量做到扁平化,单表设计能有效提高数据库的查询销率。

第四点: 使用Mongoose约束数据结构,当数据结构不一致时操作失败。

前两点足以让一些老辈程序员抓狂,让新来的程序员懵圈。这也是很多开发人员喜欢又讨厌MongoDB的原因。

总结

1 MongoDB是最接近关系型数据的非关系型数据库中的文档型数据库。

2 MongoDB支持非常丰富的查询语句,功能强大,但容易犯错。

3 MongoDB表结构的设计需谨慎,尽量减少嵌套层数,各嵌套的文档属性名尽量避免相同。

参考:1. 

           2. https://docs.mongodb.com/