引言:
常见的数据存储库有很多,例如常见的Mysql、Redis、PostgreSql。但在当前敏捷开发的时代,MongoDB不需要设计数据库的结构,省去了设计步骤。在扩展时,支持水平扩展,直接添加新的服务器存储节点,使得系统能够轻松适应大规模的数据和高并发的访问。同样支持索引结构。读了下面的文章,有直截了当的例子,以及实战案例,大家可以直接Ctrl C+V,移植到在自己的代码中。另外例子中富含注释,大家只要花一些时间读一读,都会理解并且快速上手使用。那么下面就由我给大家讲讲。
MongoDB的形式:
MongoDB是基于Json数据结构的非关系型数据库,每一个Json文档有一个唯一的主键。
一、在SpringBoot项目中整合MongoDB,首先需要引入Pom文件依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
二、在yaml配置文件中进行配置,Spring会自动扫描配置文件,并且利用。
spring:
data:
mongodb:
uri: mongodb://username:password@ip:port/dataBaseName
# username、password、ip、port、dataBaseName名称需要写自己的
三、使用上,我们定义一个Service类,里面封装好Mongo操作,在这里将这个Mongo Service开放给大家,方便大家使用:
//利用泛型传参,有效的使代码结构清晰,代码能够重复利用。另外,这里面的增、删、改、查操作都是在添加事务的情况下。
@Service
public class BaseService<T, ID> {
private MongoTemplate mongoTemplate;
private MongoClient client;
@Resource
public void setClient(MongoClient client) {
this.client = client;
}
@Resource
public void setMongoTemplate(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
/**
* MongoDB事务进行数据一致性的管理。涉及失败回滚操作
* @param entity 添加数据
* @param <S> 数据类型
* @return 添加的数据
*/
public <S extends T> S insert(S entity) {
ClientSession clientSession = client.startSession();
try {
//开始事务
clientSession.startTransaction();
return mongoTemplate.save(entity);
} catch (Exception e) {
//回滚
clientSession.abortTransaction();
throw new BadRequestException(e.getMessage());
}finally {
// 无论是否发生异常,都要结束会话。释放资源
clientSession.close();
}
}
/**
* 主键删除
*
* @param s 删除数据
* @param id 主键id
* @param <S> 删除数据类型
* @return 删除结果
*/
public <S extends T> DeleteResult deleteById(S s, ID id) {
ClientSession clientSession = client.startSession();
try {
clientSession.startTransaction();
Query query = Query.query(Criteria.where("_id").is(id));
return mongoTemplate.remove(query, s.getClass());
} catch (Exception e) {
clientSession.abortTransaction();
throw new BadRequestException(e.getMessage());
}
}
/**
* 批量删除
*
* @param s 删除数据
* @param ids 主键id
* @param <S> 删除数据类型
* @return 删除结果
*/
public <S extends T> DeleteResult deleteByIds(S s, List<ID> ids) {
ClientSession clientSession = client.startSession();
try {
clientSession.startTransaction();
Query query = Query.query(Criteria.where("_id").in(ids));
return mongoTemplate.remove(query, s.getClass());
} catch (Exception e) {
clientSession.abortTransaction();
throw new BadRequestException(e.getMessage());
}
}
/**
* 修改
*
* @param s 修改数据
* @param id 主键id
* @param <S> 修改数据类型
* @return 修改结果
*/
public <S extends T> UpdateResult update(S s, String id) {
ClientSession clientSession = client.startSession();
try {
clientSession.startTransaction();
Query query = Query.query(Criteria.where("_id").is(id));
Update update = new Update();
Field[] fields = s.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//获取属性
String name = field.getName();
//获取属性值
Object value = field.get(s);
if (ObjectUtils.allNotNull(value)) {
update.set(name, value);
}
}
return mongoTemplate.updateFirst(query, update, s.getClass());
} catch (Exception e) {
clientSession.abortTransaction();
throw new BadRequestException(e.getMessage());
}
}
/**
* 根据条件批量修改
*
* @param s 修改数据
* @param query 修改条件
* @param update 修改内容
* @param <S> 修改数据类型
* @return 修改结果
*/
public <S extends T> UpdateResult updateByQuery(S s, Query query, Update update) {
ClientSession clientSession = client.startSession();
try {
clientSession.startTransaction();
return mongoTemplate.updateMulti(query, update, s.getClass());
} catch (Exception e) {
clientSession.abortTransaction();
throw new BadRequestException(e.getMessage());
}
}
/**
* 分页查询
*
* @param dto 查询条件
* @param s 结果类型
* @param orderName 排序字段
* @param <S> 类型
* @return 分页数据
*/
public <S extends T> PageVO pagination(PageRequestDTO dto, S s, String orderName, String orderRules) {
return DataToolUtil.pagination(dto, s.getClass(), mongoTemplate, orderName, orderRules);
}
/**
* 根据表id查询数据详情
*
* @param id 表id
* @param s 数据类型
* @param <S> 返回数据类型
* @return 详情信息
*/
public <S extends T> S getInfoById(ID id, S s) {
return (S) mongoTemplate.findById(id, s.getClass());
}
/**
* 根据多个表Id查询信息
*
* @param ids 多个表id
* @param s 数据类型
* @param <S>返回数据类型
* @return 详情集合
*/
public <S extends T> List<S> getInfoByIds(List<ID> ids, S s) {
Query query = Query.query(Criteria.where("_id").in(ids));
return (List<S>) mongoTemplate.find(query, s.getClass());
}
/**
* 条件查询
*
* @param query 查询条件
* @param s 返回结果
* @param <S> 结果类型
* @return 列表
*/
public <S extends T> List<S> listByQuery(Query query, S s) {
if (null != query) {
return (List<S>) mongoTemplate.find(query, s.getClass());
}
return (List<S>) mongoTemplate.findAll(s.getClass());
}
}
四、然后在要使用Mongo的业务类继承这个Service类,像这样:
@Service
@Slf4j
public class ****ServiceImpl extends BaseService<****Entity, String> implements ****Service {
/**
* 业务代码
*/
}
类似于使用Mybatis-Plus,要使用MP中Service层级的方法,需要继承ServiceImpl<Mapper, Entity>,像这样:
@Service
@Slf4j
public class ****ServiceImpl extends ServiceImpl<****Mapper, ****Entity> implements *****Service {
/**
* 业务代码
*/
}
五、具体在业务类中如何使用,我在这里举一些例子来进行说明,帮助大家最大程度的理解,并可以快速上手使用。
Query与Criteria的关联:
Criteria用来构建查询条件,类似于Mybatis-Plus中的wrapper。而Query是用来组合一个或者多个Criteria的。
在构建多条件查询时,使用下面的语句进行合并,合并有两种情况,一个是criteria是根据where构建,另一种是由and构建:
Query query = new Query();
Criteria criteria1 = Criteria.where("_id").is(id);
query.addCriteria(criteria1);//where形式
Criteria criteria2 = new Criteria();
criteria2.and("_id").is(id);//and形式
query.addCriteria(criteria2);
where是单一条件查询,而and可以构建包含多个条件的查询。
在使用时与Mybatis-Plus一样需要对实体类进行操作,使用@Id注解指定主键Id,使用@Collation注解来对实体类指定名字。
像这样:
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.annotation.Collation;
import java.io.Serializable;
@Data
@Collation("taskLog")
public class TaskLog implements Serializable {
@Id
private String logId;
/*
* 其他属性
*/
}
增:
@Service
@Slf4j
public class TaskLogServiceImpl extends BaseService<TaskLog, String> implements TaskLogService {
@Autowired
private MongoTemplate mongoTemplate;
//增加一个
@Override
public void save(TaskLog tasklog) {
this.insert(tasklog);//实际调用在mongoTemplate的save方法
}
//批量增加
public void insertMany(List<TaskLog> tasklogs) {
mongoTemplate.insert(tasklogs, TaskLog.class);
}
}
/**
* mongoTemplate中,insert与save的区别:
* insert()方法:如果tasklog中主键id已经存在了,会抛出异常,表示插入了重复的主键。如果该主键不存在,则会正常生成。
* 用于生成新的数据
*
* save()方法:如果主键id已经存在,则会覆盖掉原来的数据。
* 用于更新数据
*/
删:
@Service
@Slf4j
public class TaskLogServiceImpl extends BaseService<TaskLog, String> implements TaskLogService {
//根据主键id删除一个
@Override
public void delete(String logId) {
this.deleteById(new TaskLog(), logId);
}
//根据主键id批量删除
public void deleteMany(List<String> logIds) {
this.deleteByIds(new TaskLog(), logIds);
}
}
/**
* 实际使用的是mongoTemplate的remove()方法。
* Query query = Query.query(Criteria.where("_id").is(id));
* mongoTemplate.remove(query, s.getClass());
*
* Query query = Query.query(Criteria.where("_id").in(ids));
* mongoTemplate.remove(query, s.getClass());
*/
改:
@Service
@Slf4j
public class TaskLogServiceImpl extends BaseService<TaskLog, String> implements TaskLogService {
//根据主键id修改
@Override
public void update(String logId) {
this.update(data, data.getId());
}
}
/**
* 实际使用的是mongoTemplate中的updateFirst()方法
* Query query = Query.query(Criteria.where("_id").is(id));
* Update update = new Update();
* Field[] fields = s.getClass().getDeclaredFields();
* for (Field field : fields) {
* field.setAccessible(true);
* //获取属性
* String name = field.getName();
* //获取属性值
* Object value = field.get(s);
* if (ObjectUtils.allNotNull(value)) {
* update.set(name, value);
* }
* }
* 利用反射覆盖对应实体类中的每一个字段以及字段值。
* mongoTemplate.updateFirst(query, update, s.getClass());
*/
简单查:
@Service
@Slf4j
public class AnalysisStudyServiceImpl extends BaseService<AnalysisStudy, String> implements AnalysisStudyService {
private MongoTemplate mongoTemplate;
@Resource
public void setMongoTemplate(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
/**
* 条件查询
*/
@Override
public void find(String patientId, String hospitalId) {
Query query = Query.query(Criteria.where("patientId").is(patientId).and("hospitalId").is(hospitalId));
List<AnalysisStudy> analysisStudyList = this.listByQuery(query, new AnalysisStudy());
}
//底层使用的是mongoTemplate的find()方法,查询满足query条件的数据。
//如果构建的query为null,listByQuery()会调用findAll()方法,查询所有的数据
}
分页查询(重点):
@Service
@Slf4j
public class AnalysisStudyServiceImpl extends BaseService<AnalysisStudy, String> implements AnalysisStudyService {
@Override
public PageVO<AnalysisStudy> queryPage(PageRequestDTO pageRequest) {
return this.pagination(pageRequest, new AnalysisStudy(), "studyDateTime", "asc");
//第一个参数:查询条件
//第二个参数:结果类型
//第三个参数:排序字段
//第四个参数:排序类型,升序ASC或者降序DESC
}
}
//BaseService中封装的方法
public <S extends T> PageVO pagination(PageRequestDTO dto, S s, String orderName, String orderRules) {
return DataToolUtil.pagination(dto, s.getClass(), mongoTemplate, orderName, orderRules);
}
//分页查询的使用封装成了一个工具类
@Slf4j
public class DataToolUtil {
//防止工具类被实例化
private DataToolUtil() {
throw new IllegalStateException("Utility class");
}
/**
* 在mongodb中分页查询,通过 PageVO 对象封装了结果数量和查询结果
*
* @param dto 分页条件
* @param tClass 返回数据类型
* @param mongoTemplate 查询模板
* @param <T> 返回数据
* @param orderName 排序列名
* @param orderRules 排序规则 asc 正序 desc 倒序
* @return 分页结果
*/
public static <T> PageVO<T> pagination(PageRequestDTO dto, Class<T> tClass,
MongoTemplate mongoTemplate,
String orderName, String orderRules) {
Query query = handParam(dto);
if (!ObjectUtils.allNotNull(dto.getCurrentPageNum())) {//没有请求的页码,默认是第一页
dto.setCurrentPageNum(0);
}
if (!ObjectUtils.allNotNull(dto.getSize())) {//没有每页展示的数量,默认是展示10个
dto.setSize(10);
}
//用给定的查询对象query来计算文档数量
long count = mongoTemplate.count(query, tClass);
/**
* dto.getCurrentPageNum():用户请求的页码
* dto.getSize():每页展示的数量
* offset用来计算偏移量
* 例如用户请求的是第2页,(2-1)=1。每页展示10个,1*10=10.则从第11个开始展示。
*/
int offset = (dto.getCurrentPageNum() - 1) * dto.getSize();
if (StringUtils.isNotEmpty(orderName) && StringUtils.isNotEmpty(orderRules)) {
String asc = "asc";//升序排序
String desc = "desc";//降序排序
if (asc.equals(orderRules)) {
//排序逻辑
query.with(Sort.by(Sort.Order.asc(orderName)));
}
if (desc.equals(orderRules)) {
//排序逻辑
query.with(Sort.by(Sort.Order.desc(orderName)));
}
}
// 分页逻辑
/**
* 举例:跳过10个,每页获取10个,展示11~20的数据。
*/
query.skip(offset).limit(dto.getSize());
log.info("querySql>>>>" + query);
//find()用于获取符合query查询条件的文档列表.tClass是目标实体类
List<T> list = mongoTemplate.find(query, tClass);
//创建一个用于封装分页查询结果的对象
PageVO<T> pageDTO = new PageVO<>();
//给PageVO对象设置查询到的满足条件的数量。
pageDTO.setTotalNum(count);
//将查询到的结果设置到PageVO对象中。
pageDTO.setResults(list);
return pageDTO;
}
/**
* 根据传入的查询条件进行 MongoDB 数据库的动态查询
* @param dto
* @return
*/
private static Query handParam(PageRequestDTO dto) {
Query query = new Query();//创建一个MongoDB查询对象
List<ParamDTO> params = dto.getParams();//获取传入参数的查询条件列表
Criteria criteria = new Criteria();//创建MongoDB的Criteria对象,用于构建查询条件
params.forEach(param -> {//遍历查询条件列表
String operator = param.getOperator();
switch (operator) {
//该语句构建了一个等于(eq)的查询条件,使用 is 方法指定字段等于给定的值。
case "eq" -> criteria.and(param.getFiled()).is(param.getFiledValue());
//regex表示使用正则表达式来进行查询,Pattern.compile()来构造一个正则表达式。这个正则表达式是‘^.*value.*$’
//'^'和‘$’是正则表达式的起始和结束标识。‘.*表示匹配字符0次或者多次’
//Pattern.CASE_INSENSITIVE表示不区分大小写
case "like" -> criteria.and(param.getFiled()).regex(Pattern.compile("^.*" + param.getFiledValue()
+ ".*$", Pattern.CASE_INSENSITIVE));
case "le" -> criteria.and(param.getFiled()).lt(param.getFiledValue());
case "ge" -> criteria.and(param.getFiled()).gt(param.getFiledValue());
//转换成String类型的字符串列表
case "in" -> {
List<String> values = JSON.parseArray(param.getFiledValue().toString(), String.class);
criteria.and(param.getFiled()).in(values);
}
case "between" -> {
if (param.getFiledValue() instanceof JSONArray) {
List<Object> list = JSON.parseArray(param.getFiledValue().toString(), Object.class);
if (CollUtil.isNotEmpty(list)) {
//gte:大于等于
//lte:小于等于
criteria.and(param.getFiled()).gte(list.get(0)).lte(list.get(1));
}
}
}
default -> {//没匹配任何一个case语句,默认执行default语句。
}
}
});
query.addCriteria(criteria);//将构建好的 Criteria 对象添加到查询对象中。
return query;
}
}
@Data
public class PageVO<T> {
private Long totalNum;//结果总数
private List<T>results;//查询结果
}
@Data
@Schema(description = "分页查询条件")
public class PageRequestDTO {
@Schema(description = "当前页数")
private Integer currentPageNum;
@Schema(description = "展示行数")
private Integer size;
@Schema(description = "查询条件")
private List<ParamDTO> params;
}