Spring Boot整合MongoDB重构nodejs项目

2,272 阅读3分钟

前言

很久之前笔者用nodejs开发了一套后台基于koamongoose的的api接口,但效果不尽人意,代码多的时候整个项目变得比较臃肿,慢慢用eggjs进行重构,但基于javaScript弱类型的语言的因素,后面产生了一种用强类型语言重构的想法,参考了后台api成熟的方案, 以及spring生态,最后选择了java来进行重构,数据库基于mongodb,但网上的spring整合的基本以mysql为主关系型数据库,整合持久层MyBatis-Plus, spring-data-jpa的方案居多, Spring Data Mongodb的整合相对较少,甚至没找到例如MyBatis-Plus方面的代码生成器,所以写下此文章来记录一下自己的学习笔记。

关于mongodb

Mongodb是为快速开发互联网Web应用而构建的数据库系统,其数据模型和持久化策略就是为了构建高读/写吞吐量和高自动灾备伸缩性的系统。Spring Data Mongodb是Spring提供的一种以Spring Data风格来操作数据存储的方式,它可以避免编写大量的样板代码。

添加依赖

在Spring Boot中集成Mongodb非常简单,只需要加入Mongodb的Starter包即可,代码如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

然后在application.properties配置Mongodb的连接信息:

配置文件application.properties

#mongodb连接地址,集群用“;”隔开
#spring.mongo.mongoDatabaseAddress=10.110.112.165:27092;10.110.112.166:27092
#mongo域名
spring.data.mongodb.host=127.0.0.1 
#mongo端口
spring.data.mongodb.port=27017
#mongo数据名
spring.data.mongodb.database=education
#mongo用户
spring.mongo.username=admin
#mongo密码
spring.mongo.password=123456
#mongo最大连接数
spring.mongo.connectionsPerHost=50

添加数据

首先创建一个实体类,我们这边用学生来做实体类,定义如下字段:

@Data
@ApiModel(value="Student对象", description="学生")
@Document(collection = "student")
public class Student  {

    @Id
    private String id;

    

    @Field("name")
    private String name;
    private String birthday;
    private Integer sex;


    private  String contacts; // 联系人
    private  String phone; // 电话

    private  String province;
    private  String city;
    private  String region;
    private  String address;

    private  String teacherId;
    private   List<String> openId = new ArrayList<>();

    private  String desc;
    private  Integer status; // 状态:在读,毕业 1/2

    @ApiModelProperty(value = "创建时间", example = "2019-11-10T07:29:32.496+0000")
    private Date createDate;


    @ApiModelProperty(value = "更新时间", example = "2019-11-10T07:29:32.496+0000")
    private  Date updateDate;
}

实体类中的注解解释如下

注解解析
@Id主键标识, 用于标记id字段,没有标记此字段的实体也会自动生成id字段,但是我们无法通过实体来获取id。id建议使用ObjectId类型来创建
@Document用于标记此实体类是mongodb集合映射类,等同mysql中的表,collection值表示mongodb中集合的名称,不写默认为实体类名student
@DBRef用于指定与其他集合的级联关系,但是需要注意的是并不会自动创建级联集合
@Indexed用于标记为某一字段创建索引
@CompoundIndex用于创建复合索引
@TextIndexed:用于标记为某一字段创建全文索引
@Language指定documen语言
@Transient:被该注解标注的,将不会被录入到数据库中。只作为普通的javaBean属性
@Field:用于指定某一个字段映射到数据库中的名称,之所有有这样的注解,是为了能够让用户自定义字段名称,可以和实体类不一致,还有个好处就是可以用缩写,比如username我们可以配置成unane或者un,这样的好处是节省了存储空间,mongodb的存储方式是key value形式的,每个key就会重复存储,key其实就占了很大一份存储空间

操作数据

方式1:Sping Data方式的数据操作

添加StudentRepository接口用于操作Mongodb

// 继承MongoRepository接口可以获得常用的数据操作方法,可以使用衍生查询
// 在接口中直接指定查询方法名称便可查询,无需进行实现。
public interface StudentRepository extends MongoRepository<Student, String> {
    /**
        根据openid查询学生对应
    **/
    Student findFirstByOpenIdIn(String id);


    //使用@Query注解可以用Mongodb的JSON查询语句进行查询
    @Query("{'name': ?0}")
    Student findByLocalName(String name);

    //使用@Aggregation注解可以用Mongodb的聚合查询语句进行查询
    @Aggregation({"{ '$project': { _id : '$status' } }", "{$group: {_id: null, count: {$sum: $_id}}}"})
    Integer findAlls();

}

添加StudentService接口

public interface StudentService  {

   Student selectOne(String id);

}

添加StudentService接口实现类StudentServiceImpl

@Service
@Slf4j
public class StudentServiceImpl  implements StudentService {
    @Autowired
    private StudentRepository StudentRepository;
    
    @Override
    public Student selectOne(String name) {
        return this.StudentRepository.findByLocalName(name);
    }

}

添加StudentController定义接口

@RestController
@Api(description="学生管理")
@RequestMapping("/student")
public class StudentController extends ApiController {

    @Resource
    StudentService studentService;


    /**
     * 通过名字查询单条数据
     *
     * @param name 名字
     * @return 单条数据
     */
    @GetMapping("/findName")
    @ApiOperation(value = "根据ID查询学生")
    public R selectOne( @ApiParam(name = "name", value = "学生name", required = true) String name) {

        return R.ok(this.studentService.selectOne(name));
    }


}

方式2:MongoTemplate

添加数据

@Autowired
private MongoTemplate mongoTemplate;

// 添加单个
Student stu = new Student();
// 省略数据的设置.....
mongoTemplate.save(student);


 //批量添加
List<Student> stus = new ArrayList<>(10);
// 省略数据的设置.....
mongoTemplate.insert(stus, Student.class);

删除操作

//删除name为Tom的数据
Query query = Query.query(Criteria.where("name").is("Tom"));
mongoTemplate.remove(query, Student.class);
mongoTemplate.remove(query, "student");

//删除集合,可传实体类,也可以传名称
mongoTemplate.dropCollection(Student.class);
mongoTemplate.dropCollection("student");

//删除数据库
mongoTemplate.getDb().dropDatabase();

//查询出符合条件的第一个结果,并将符合条件的数据删除,只会删除第一条
query = Query.query(Criteria.where("name").is("Tom"));
Student student = mongoTemplate.findAndRemove(query, Student.class);

//查询出符合条件的所有结果,并将符合条件的所有数据删除
query = Query.query(Criteria.where("name").is("Tom"));
List<Student> students = mongoTemplate.findAllAndRemove(query, Student.class);

修改操作

//修改第一条name为name的数据中的contacts和visitCount
Query query = Query.query(Criteria.where("name").is("Tom"));
Update update = Update.update("contacts", "MongoTemplate").set("age", 10);
mongoTemplate.updateFirst(query, update, Student.class);

//修改全部符合条件的
mongoTemplate.updateMulti(query, update, Student.class);


//特殊更新,更新name为Tom的数据,如果没有name为Tom的数据则以此条件创建一条新的数据
//当没有符合条件的文档,就以这个条件和更新文档为基础创建一个新的文档,如果找到匹配的文档就正常的更新。
mongoTemplate.upsert(query, update, Student.class);

//更新条件不变,更新字段改成了一个我们集合中不存在的,用set方法如果更新的key不存在则创建一个新的key
Query query = Query.query(Criteria.where("name").is("Tom"));
Update update = Update.update("contacts", "MongoTemplate").set("money", 100);
mongoTemplate.updateMulti(query, update, Student.class);

//update的inc方法用于做累加操作,将money在之前的基础上加上100
Query query = Query.query(Criteria.where("name").is("Tom"));
update = Update.update("contacts", "MongoTemplate").inc("money", 100);
mongoTemplate.updateMulti(query, update, Student.class);

//update的rename方法用于修改key的名称
Query query = Query.query(Criteria.where("name").is("Tom"));
update = Update.update("contacts", "MongoTemplate").rename("visitCount", "vc");
mongoTemplate.updateMulti(query, update, Student.class);

//update的unset方法用于删除key
Query query = Query.query(Criteria.where("name").is("Tom"));
update = Update.update("contacts", "MongoTemplate").unset("vc");
mongoTemplate.updateMulti(query, update, Student.class);

//update的pull方法用于删除tags数组中的java
Query query = Query.query(Criteria.where("name").is("Tom"));
update = Update.update("contacts", "MongoTemplate").pull("tags", "java");
mongoTemplate.updateMulti(query, update, Student.class);


查询操作

查询,无论是关系型数据库还是mongodb这种nosql,都是使用比较多的,大部分操作都是读的操作。使用MongoTemplate结合SortCriteriaQuery以及分页Pageable类灵活地进行对mongodb数据库进查询

mongodb的查询方式很多种,下面只列了一些常用的,比如:

  1. =查询
  2. 模糊查询
  3. 大于小于范围查询
  4. in查询
  5. or查询
  6. 查询一条,查询全部

根据学生查询所有符合条件的数据,返回List



Query Query query = Query.query(Criteria.where("name").is("Tom"));
List<Student> students = mongoTemplate.find(query, Student.class);



//分页查询
Sort sort = new Sort(Sort.Direction.DESC, "name");
Pageable pageable = PageRequest.of(3, 20, sort);
List<Student> StudentPageableList = mongoTemplate.find(Query.query(Criteria.where("name").is(name)).with(pageable), Student.class);



//只查询符合条件的第一条数据,返回student对象
Query query = Query.query(Criteria.where("name").is("Tom"));
Student student = mongoTemplate.findOne(query, Student.class);


//基于sort排序使用findOne查询最新一条记录
Student student = mongoTemplate.findOne(Query.query(Criteria.where("name").is(name)).with(sort), Student.class);


//模糊查询
List<Student> StudentList = mongoTemplate.find(Query.query(Criteria.where("name").is(name).regex(name)).with(sort), Student.class);

//总数
 long conut = mongoTemplate.count(Query.query(Criteria.where("name").is(name)), Student.class);

//查询集合中所有数据,不加条件
student = mongoTemplate.findAll(Student.class);


// 查询符合条件的数量

Query query = Query.query(Criteria.where("name").is("Tom"));
long count = mongoTemplate.count(query, Student.class);


// 根据主键ID查询

student = mongoTemplate.findById(new ObjectId("87c6e1601e4735b2c3064db7"), Student.class);

// in查询

List<String> names = Arrays.asList("jack", "Tom");
query = Query.query(Criteria.where("name").in(names));
students = mongoTemplate.find(query, Student.class);


//ne(!=)查询

query = Query.query(Criteria.where("name").ne("Tom"));
students = mongoTemplate.find(query, Student.class);


// lt(<)查询年龄小于10的学生

query = Query.query(Criteria.where("age").lt(10));
students = mongoTemplate.find(query, Student.class);


//范围查询,大于5小于10

query = Query.query(Criteria.where("age").gt(5).lt(10));
students = mongoTemplate.find(query, Student.class);


// 模糊查询,name中包含a的数据

query = Query.query(Criteria.where("name").regex("a"));
students = mongoTemplate.find(query, Student.class);


// 数组查询,查询openid里数量为3的数据

query = Query.query(Criteria.where("openid").size(3));
students = mongoTemplate.find(query, Student.class);


// or查询,查询name=Tom的或者age=10的数据

query = Query.query(Criteria.where("").orOperator(
    Criteria.where("name").is("Tom"),
    Criteria.where("age").is(10)));
students = mongoTemplate.find(query, Student.class);

聚合查询

// 根据创建时间统计学生的个数
List<AggregationOperation> operations = new ArrayList();
operations.add(
        Aggregation.project( "name"). // 保留的字段
                andExpression("createDate")
                .dateAsFormattedString("%Y-%m").as("date")
                .andExpression("createDate").extractMonth().as("key")
                .andExpression("id").as("stuId")

);

operations.add(
        Aggregation.group("date")
                .first("key").as("key")
                .push("name").as("student")
                .push("stuId").as("ids")
                .count().as("count")
);



AggregationResults<Map> aggregate = this.mongoTemplate.aggregate(Aggregation.newAggregation(operations), Student.class, Map.class);

优化

对于复杂的聚合查询语句可以自定义查询类

例如:链表查询的,需要定义变量的

db.getCollection('course').aggregate([{
    $unwind: '$studentIds',
    },
    {
    $lookup: {
        from: 'student',
        let: { stuId: { $toObjectId: '$studentIds' } },
        pipeline: [
        {
            $match: {
            $expr: { $eq: [ '$_id', '$$stuId' ] },
            },
        },
        {
            $project: {
            isSendTemplate: 1,
            openId: 1,
            stu_name: '$name',
            stu_id: '$_id',
            },
        },
        ],
        as: 'student',
    },
    }])

自定义AggregationOperation

import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;

public class CustomAggregationOperation implements AggregationOperation {

  private String jsonOperation;

  public CustomAggregationOperation(String jsonOperation) {
    this.jsonOperation = jsonOperation;
  }

  @Override
  public org.bson.Document toDocument(AggregationOperationContext aggregationOperationContext) {
    return aggregationOperationContext.getMappedObject(org.bson.Document.parse(jsonOperation));
  }
}


@Service
public class LookupAggregation {

  @Autowired
  MongoTemplate mongoTemplate;

  public void LookupAggregationExample() {

    AggregationOperation unwind = Aggregation.unwind("studentIds");

    String query1 = "{$lookup: {from: 'student', let: { stuId: { $toObjectId: '$studentIds' } },"
        + "pipeline: [{$match: {$expr: { $eq: [ '$_id', '$$stuId' ] },},}, "
        + "{$project: {isSendTemplate: 1,openId: 1,stu_name: '$name',stu_id: '$_id',},},], "
        + "as: 'student',}, }";

    TypedAggregation<Course> aggregation = Aggregation.newAggregation(
        Course.class,
        unwind,
        new CustomAggregationOperation(query1)
    );

    AggregationResults<Course> results =
        mongoTemplate.aggregate(aggregation, Course.class);
    System.out.println(results.getMappedResults());
  }
}

封装通用crud的BaseServiceImpl

public class BaseServiceImpl<T extends BaseEntity>  implements BaseService<T > {
    @Autowired
    MongoTemplate mongoTemplate;

    @Autowired
    MongoRepository<T, String> mongoRepository;

    /**
     * 创建一个 Class 的对象来获取泛型的 Class
     */
    private Class<T> clazz ;

    public Class<T> getClazz() {
        if (clazz == null) {
            clazz = ((Class<T>) (((ParameterizedType) (this.getClass().getGenericSuperclass())).getActualTypeArguments()[0]));
        }
        return clazz;
    }

    @Override
    public PageData<T> findQueryPage(Query query) {

        JSONObject queryObj = query.getQuery();


        BasicQuery basicQuery = new BasicQuery(queryObj.toString());


        //计算总数
        long total = this.mongoTemplate.count(basicQuery, getClazz());

        // 添加分页
        PageRequest pageable = query.toPageRequest();
        basicQuery.with(pageable);

        List<T> list = this.mongoTemplate.find(basicQuery, getClazz());


        PageData result = new PageData(total, list);

        return result;
    }


    @Override
    public T selectOne(String id) {
        Optional<T> byId = this.mongoRepository.findById(id);

        return byId.orElse(null);
    }

    @Override
    public T insert(T t) {
        t.setCreateDate(new Date());
        t.setUpdateDate(new Date());
        T save = this.mongoRepository.save(t);

        return save;
    }

    @Override
    public T update(T t) {

        Class<? extends BaseEntity> entryClazz = t.getClass();
        Document annotation = entryClazz.getAnnotation(Document.class);
        String collection = annotation.collection();

        org.springframework.data.mongodb.core.query.Query query = new org.springframework.data.mongodb.core.query.Query(Criteria.where("_id").is(t.getId()));

        Update update = new Update();

        Field[] declaredFields = entryClazz.getDeclaredFields();
        Boolean isUpdate = false;
        for (Field targetField : declaredFields) {
            if (Modifier.isStatic(targetField.getModifiers())) { // 跳过静态方法
                continue;
            }

            targetField.setAccessible(true);


            try {
                Object obj = targetField.get(t);
                String name = targetField.getName();

                if (StringUtils.isEmpty(obj)) {
                    continue;
                }

                if (name.equalsIgnoreCase("createDate") || name.equalsIgnoreCase("updateDate")) {
                    continue;
                }

                // 判断是否数组为空
                if (obj.getClass().isArray()) {
                    List list = CollectionUtils.arrayToList(obj);
                    if (list.isEmpty()) {
                        continue;
                    }
                } else if (obj instanceof  List) { // 判断list是否为空
                    List list = (ArrayList) obj;
                    if (list.isEmpty()) {
                        continue;
                    }
                }

                isUpdate = true;
                update.set(name, obj);

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

        }


        if (isUpdate) {
            update.set("updateDate", new Date());
            UpdateResult updateResult = mongoTemplate.updateFirst(query, update, entryClazz);
            long modifiedCount = updateResult.getModifiedCount();
            System.out.println("更新数量" + modifiedCount);
        }

        return null;
    }

    @Override
    public void removeById(String id) {
        this.mongoRepository.deleteById(id);
    }

    @Override
    public void removeByIds(List<T> all) {
        this.mongoRepository.deleteAll(all);
    }
}

参考文档