前言
很久之前笔者用
nodejs开发了一套后台基于koa与mongoose的的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结合Sort、Criteria、Query以及分页Pageable类灵活地进行对mongodb数据库进查询
mongodb的查询方式很多种,下面只列了一些常用的,比如:
- =查询
- 模糊查询
- 大于小于范围查询
- in查询
- or查询
- 查询一条,查询全部
根据学生查询所有符合条件的数据,返回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);
}
}