新公司项目中使用的ORM框架为JPA框架,但是我们后端写的分页查询接口都各不相同。存在扩展性差、支持的查询类型单一、无法复用等问题。
所以我在写分页查询的进行了一些设计,将分页查询设计成了可拓展、功能复杂的一个公共分页查询方法。该公共方法所有使用JPA框架的项目都可以使用。
首先复用性高,首先想到使用反射或者泛型来实现。
复杂的查询类型,可以想到的精确查询、模糊查询、批量查询、段查询这些。
除了查询功能支持,还需要有分页相关的参数,然后还要能够支持排序功能。
所以再设计分页接口请求参数时需要考虑能够满足上面能够功能,最终设计出来的分页请求参数PageParam如下。
由于使用的是JPA框架,用过这个框架的同学都知道这个框架的查询都是通过实现JpaRepository<T, ID>接口来完成的。下面列举一下常用的查询手段,
1、通过Example.of()构造查询对象,这个只能进行精确查询。
2、通过方法命名形式进行查询,eg findAllByxxxxAndxxxxInAndxxxxIsTrue()。这个支持的查询很多但是对命名规范有要求且如果查询条件过多,方法名就很长很长了。
3、使用@Query完成较为复杂的查询,方法名不会很长。但是扩展性、复用性差,该查询条件就得改动查询方法。
4、Specification
具体构造实现请跳转构造查询条件
处理完成之后实际处理起来就比较简单了。如果还有什么疑问可以邮件私我,邮箱号在最下面。
/**
* 分页查询
* @param pageParam 查询条件
* @return
*/
@Override
public Page<XXXXVO> page(PageParam<XXXXVO> pageParam) {
XXXXVO vo = pageParam.getVo();
pageParam.getSorts().put("updateDate", JpaUtils.SORT_DESC);
if (null == vo) {
vo = new XXXXVO();
}
Pageable pageable = jpaUtils.getPageable(pageParam);
//vo转po
XXXXPO entity = DozerUtil.transfor(vo, XXXXPO.class);
//这个就是前面实现的构造查询条件方法
Specification<XXXXPO> spec = jpaUtils.getSpec(entity, pageParam);
//dao接口用过jpa的都清楚,实现了JpaRepository用来的接口
//如果你的dao没有这个方法,dao可以实现一个自己声明的接口(eg:BaseJpaRepository)实现JpaRepository,在里面加上入参为这两个的方法即可。
Page<XXXXPO> page = XXXXDao.findAll(spec, pageable);
List<XXXXPO> all = page.getContent();
return new PageImpl<>(DozerUtil.transforList(all, XXXXVO.class), page.getPageable(), page.getTotalElements());
}
/**
* 分页查询请求参数
* @author hehuibing442@163.com
* @version 2.0.0
* @date 2022/05/17 09:48
* @description
*/
@Data
public class PageParam<T> implements Serializable {
@ApiModelProperty("分页查询对象,主要用于精确查询")
private T vo;
@ApiModelProperty("页码,如果不传默认1")
@JsonProperty("page_index")
private Integer pageIndex =1;
@ApiModelProperty("页数,如果不传默认10")
@JsonProperty("page_size")
private Integer pageSize =10;
@ApiModelProperty("排序方式,只支持asc和desc. eg: \"create_date\":\"desc/asc\" ")
private Map<String,String> sorts =new HashMap<>();
@JsonProperty("search_date_map")
@ApiModelProperty("Date类型日期段查询,eg createDate:[startDate,endDate]")
private Map<String,List<String>> searchDateMap =new HashMap<>();
@JsonProperty("search_local_time_date_map")
@ApiModelProperty("LocalTimeDate类型日期段查询, eg createDate:[startDate,endDate]")
private Map<String,List<String>> searchLocalTimeDateMap =new HashMap<>();
@JsonProperty("search_map")
@ApiModelProperty("查询map, eg id:{in:1,2,3}")
private Map<String, SearchFilter> searchMap =new HashMap<>();
}
/**
* 搜索过滤对象
* @author hehuibing442@163.com
* @version 2.0.0
* @date 2022/05/17 11:23
* @description
*/
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class SearchFilter {
@ApiModelProperty("查询方式,目前支持:like,in")
private String opt;
@ApiModelProperty("查询值,多个值用英文逗号‘,’分割 ")
private String values;
}
//不需要权限控制的方法
public <T, R> Specification<T> getSpec(T entity, PageParam<R> pageParam) {
return this.getSpec(entity, pageParam, false);
}
/**
* 构造查询条件
* @param entity 实体Model类,拥有@Entity与@Table注解的实体, 非DTO、VO等
* @param pageParam 分页查询请求参数
* @param isOpenAuth 是否开启权限控制,属于扩展功能
* @param <T>
* @param <R>
* @return
*/
public <T,R> Specification<T> getSpec(T entity, PageParam<R> pageParam,boolean isOpenAuth){
if (null==pageParam||null==entity){
throw new BusinessException("查询体不能为空");
}
return (root,cq,cb)->{
//查询谓词集合
List<Predicate> predicates = new ArrayList<>();
//对象构造--就是获取实体类及其父类的所有声明字段
List<Field> fields = this.getFields(entity);
//字段map--方便检索Field,提升响应速度
Map<String, Field> fieldMap = fields.stream().collect(Collectors.toMap(Field::getName, v -> v));
//构造精确查询条件
for (Field field:fields){
String name = field.getName();
Class<?> type = field.getType();
if (!field.isAccessible()){
field.setAccessible(true);
}
Object fieldValue = this.getFieldValue(entity, field);
if (null==fieldValue|| ObjectUtils.isEmpty(fieldValue)){
continue;
}
//校验
if (!this.isSupportType(type)){
throw new BusinessException(type+"类型暂不支持!");
}
//将构造的查询条件加入谓词集合
predicates.add(cb.equal(root.get(this.underlineToHump(name)).as(field.getType()), fieldValue));
}
//下面这些均可抽取为一个方法。为了防止篇幅过长将分别列出
//是否开启权限控制
//排序
//模糊查询
//时间段查询
}
如果使用的基于角色RBAC或者基于属性ABAC这种方式实现的权限控制,在需要用到数据权限的时候就需要通过当前用户信息,判断当前用户所在角色组权限或者相关属性拿到当前用户针对当前查询数据的过滤条件。将得到的过滤条件比如XXXX部门、XXXX小组转、XXXX岗位换成对应的查询条件。中间可能涉及到多次转换,但最终一定可以转换成实体类里面的用于权限控制的公共字段。
我这里使用的是比较简单的权限控制,只根据用户名进行权限控制。可以参考,
//是否开启权限控制--只有属于继承了公共字段才可以生效
if (isOpenAuth && entity instanceof AbstractEntityPO) {
//可供扩展
String nickName = HttpRequestUtil.getNickNameOrThrow();
Field field = fieldMap.get(PageUtils.CREATE_USER);
if (null != field) {
predicates.add(cb.equal(root.get(this.underlineToHump(field.getName())).as(field.getType()), nickName));
}
}
//searchMap构造
Map<String, SearchFilter> searchMap = pageParam.getSearchMap();
//复杂查询构造
if (searchMap.size()>0){
searchMap.forEach((fieldName, searchFilter) -> {
if (null != searchFilter&& StringUtils.isNotEmpty(searchFilter.getValues())) {
//校验字段是否存在
this.checkFieldsExist(fieldMap,fieldName);
String opt = searchFilter.getOpt();
String optValues = searchFilter.getValues();
//in查询, IN,LIKE均为自定义的常量
if (IN.equals(opt)) {
String[] values = optValues.split(",");
CriteriaBuilder.In<String> in = cb.in(root.get(this.underlineToHump(fieldName)).as(String.class));
for (String value : values) {
in.value(value);
}
predicates.add(cb.and(in));
}
//like查询
if (LIKE.equals(opt)){
predicates.add(cb.like(root.get(this.underlineToHump(fieldName)).as(String.class),"%"+optValues+"%"));
}
}
});
};
//日期查询构造
//LocalDateTime构造
Map<String, List<String>> localDateTimeMap = pageParam.getSearchLocalTimeDateMap();
if (localDateTimeMap.size()>0){
localDateTimeMap.forEach((field,dates)->{
if (!CollectionUtils.isEmpty(dates)){
this.checkFieldsExist(fieldMap,field);
if(DATE_SEARCH_LIST_LENGTH!=dates.size()){
throw new BusinessException("构造日期查询条件失败!");
}
//这里就是将字符串格式的日期转为指定类型的日期
LocalDateTime startDate = PageUtils.parseLocalDateTime(dates.get(0));
LocalDateTime endDate = PageUtils.parseLocalDateTime(dates.get(1));
if (startDate.isAfter(endDate)){
throw new BusinessException("构造日期查询条件失败");
}
//构造日期段查询,除了用between还可以考虑联合使用ge与le实现
predicates.add(cb.between(root.get(this.underlineToHump(field)).as(LocalDateTime.class),startDate,endDate));
}
});
}
//排序参数构造
Map<String, String> sorts = pageParam.getSorts();
if (sorts.size() > 0) {
//排序集合
List<Order> sortOrders = sorts.entrySet().stream()
.map(entry -> {
String field = entry.getKey();
this.checkFieldsExist(fieldMap, field);
String sortType = entry.getValue();
if (SORT_DESC.equals(sortType)) {
return cb.desc(root.get(this.underlineToHump(field)));
}
return cb.asc(root.get(this.underlineToHump(field)));
})
.collect(Collectors.toList());
cq.orderBy(sortOrders);
}
/**
* 校验字段是否是存在的
*
* @param fieldMap
* @param fieldName
*/
private void checkFieldsExist(Map<String, Field> fieldMap, String fieldName) {
if (!fieldMap.containsKey(this.underlineToHump(fieldName))) {
throw new BusinessException("查询条件[" + fieldName + "]名称不合法");
}
}
/**
* 判读当前类是否是支持的类型
*
* @param type
* @return
*/
private boolean isSupportType(Class<?> type) {
if (String.class.equals(type)) {
return true;
}
if (LocalDateTime.class.equals(type)) {
return true;
}
if (BigDecimal.class.equals(type) || Double.class.equals(type) || Float.class.equals(type)) {
return true;
}
if (Boolean.class.equals(type)) {
return true;
}
if (Integer.class.equals(type) || int.class.equals(type)) {
return true;
}
if (Date.class.equals(type)) {
return true;
}
return false;
}
/**
* 根据传入的带下划线的字符串转化为驼峰格式
*
* @param str
* @return
* @author mrf
*/
private String underlineToHump(String str) {
//正则匹配下划线及后一个字符,删除下划线并将匹配的字符转成大写
Matcher matcher = UNDERLINE_PATTERN.matcher(str);
StringBuffer sb = new StringBuffer(str);
if (matcher.find()) {
sb = new StringBuffer();
//将当前匹配的子串替换成指定字符串,并且将替换后的子串及之前到上次匹配的子串之后的字符串添加到StringBuffer对象中
//正则之前的字符和被替换的字符
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
//把之后的字符串也添加到StringBuffer对象中
matcher.appendTail(sb);
} else {
//去除除字母之外的前面带的下划线
return sb.toString().replaceAll("_", "");
}
return underlineToHump(sb.toString());
}
我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时