MongoDB日期存储与查询、@Query、嵌套字段查询实战总结

news2024/11/18 20:17:45

缘由

MongoDB数据库如下:
在这里插入图片描述
如上截图,使用MongoDB客户端工具DataGrip,在filter过滤框输入{ 'profiles.alias': '逆天子', 'profiles.channel': '' },即可实现昵称和渠道多个嵌套字段过滤查询。

现有业务需求:用Java代码来查询指定渠道和创建日期在指定时间区间范围内的数据。

注意到creationDate是一个一级字段(方便理解),profiles字段和creationDate属于同一级,是一个数组,而profiles.channel是一个嵌套字段。

Java应用程序查询指定渠道(通过@Query注解profiles.channel)和指定日期的数据,Dao层(或叫Repository层)接口Interface代码如下:

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;

@Repository
public interface AccountRepository extends MongoRepository<Account, String> {
	@Query("{ 'profiles.channel': ?0 }")
	List<Account> findByProfileChannelAndCreationDateBetween(String channel, Date start, Date end);
}

单元测试代码如下:

@Test
public void testFindByProfileChannelAndCreationDateBetween() {
    String time = "2024-01-21";
    String startTime = time + DateUtils.DAY_START;
    String endTime = time + DateUtils.DAY_END;
    Date start = new Date();
    Date end = new Date();
    try {
        start = DateUtils.parseThenUtc(startTime);
        end = DateUtils.parseThenUtc(endTime);
    } catch (ParseException e) {
        log.error("test failed: {}", e.getMessage());
    }
    List<Account> accountList = accountRepository.findByProfileChannelAndCreationDateBetween(ChannelEnum.DATONG_APP.getChannelCode(), start, end);
    log.info("size:{}", accountList.size());
}

输出如下:size:70829

没有报错,但是并不能说明没有问题。根据自己对于业务的理解,数据量显然不对劲,此渠道的全量数据是这么多才差不多。

也就是说,上面的Interface接口查询方法,只有渠道条件生效,日期没有生效??

至于为什么没有生效,请继续往下看。想看结论的直接翻到文末。

排查

不生效

MongoRepository是Spring Data MongoDB提供的,继承MongoRepository之后,就可以使用IDEA的智能提示快速编写查询方法。如下图所示:
在这里插入图片描述
但是:上面的这种方式只能对一级字段生效。如果想要过滤查询嵌套字段,则派不上用场。

此时,需要使用一个更强大的@Query注解。

但是,@Query和JPA方式不能一起使用。也就是上面的方法findByProfileChannelAndCreationDateBetween查询方法,经过简化后只保留一级字段,然后嵌套字段使用@Query方式:

@Query("{ 'profiles.channel': ?0 }")
List<Account> findByCreationDateBetween(String channel, Date s1, Date s2);

依旧是不生效的。

版本1

基于上面的结论,有一版新的写法:

@Query("{ 'profiles.channel': ?0, 'creationDate': {$gte: ?1, $lte: ?2} }")
List<Account> findByChannelAndCreationDate(String channel, Date start, Date end);

此时输出:size:28。这个数据看起来才比较正常(虽然后面的结论证明不是正确的)。

WARN告警

如果不过滤渠道呢?查询某个日期时间段内所有渠道的全量用户数据?

两种写法都可以:

long countByCreationDateBetween(Date start, Date end);

@Query("{ 'creationDate': {$gte: ?0, $lte: ?1} }")
long countByCreationDate(Date start, Date end);

等等。怎么第一种写法,IDEA给出一个WARN??
在这里插入图片描述

MongoDB日期

上面IDEA给出的Warning显而易见。因为MongoDB数据库字段定义是Instant类型:

@Data
@Document
public class Account {
    @Id
    protected String key;
    private Instant creationDate = Instant.now();
	private List<Profile> profiles = new ArrayList<>();
    private boolean firstTimeUsage = true;
}

IDEA作为宇宙最强IDE,给出WARN自然是有道理的。

作为一个代码洁癖症患者,看到IDEA的shi黄色告警,无法忍受。假设IDEA告警没有问题(极端少数情况下,IDEA告警也有可能误报,参考记一次Kotlin Visibility Modifiers引发的问题),为了消除告警,有两种方式:

  • 修改Account数据库实体类creationDate类型定义,Instant改成Date
  • Repository层接口方法不使用Date类型传参,而使用Instant类型传参。

那到底应该怎么修改呢?才能屏蔽掉IDEA的shi黄色告警WARN呢??

单元测试

数据库持久化实体PO类日期字段类型定义,到底该使用Date还是Instant类型呢??

在Google搜索关键词MongoDB日期的同时,不妨写点单元测试来执行一下。(注:此时此处行文看起来思路挺清晰,但在遇到陌生的问题是真的是无头苍蝇)

在保持数据库PO实体类日期字段类型定义不变的前提下,有如下两个查询Interface方法:

long countByCreationDateBetween(Date start, Date end);

@Query("{ 'creationDate': {$gte: ?0, $lte: ?1} }")
long countByCreationDate(Instant start, Instant end);

单元测试:

@Resource
private MongoTemplate mongoTemplate;
@Resource
private IAccountRepository accountRepository;

@Test
public void testCompareDateAndInstant() {
    String time = "2024-01-21";
    String startTime = time + DateUtils.DAY_START;
    String endTime = time + DateUtils.DAY_END;
    Date start = new Date();
    Date end = new Date();
    try {
        start = DateUtils.parseThenUtc(startTime);
        end = DateUtils.parseThenUtc(endTime);
    } catch (ParseException e) {
        log.error("testCompareDateAndInstant failed: {}", e.getMessage());
    }
    Criteria criteria = Criteria.where("creationDate").gte(start).lte(end);
    long count1 = mongoTemplate.count(new Query(criteria), Account.class);
    // idea warn
    long count2 = accountRepository.countByCreationDateBetween(start, end);
    long count3 = accountRepository.countByCreationDate(DateUtils.getInstantFromDateTimeString(startTime), DateUtils.getInstantFromDateTimeString(endTime));
    long count4 = accountRepository.countByCreationDate(DateUtils.parse(startTime).toInstant(), DateUtils.parse(endTime).toInstant());
    log.info("date:{},count1:{},count2:{},count3:{},count4:{}", time, count1, count2, count3, count4);
}

单元测试执行后打印输出:date:2024-01-21,count1:35,count2:35,count3:32,count4:29

换几个不同的日期,count1和count2都是一致的。也就是说,不管是使用Template,还是Repository方式,使用Date类型日期查询MongoDB数据,结果是一样的。count3和count4使用Instant类型查询MongoDB数据,结果不一致,并且和Date类型不一致。

为啥呢??

Instant vs Date

MongoDB中的日期使用Date类型表示,在其内部实现中采用一个64位长的整数,该整数代表的是自1970年1月1日零点时刻(UTC)以来所经过的毫秒数。Date类型的数值范围非常大,可以表示上下2.9亿年的时间范围,负值则表示1970年之前的时间。

MongoDB的日期类型使用UTC(Coordinated Universal Time)进行存储,也就是+0时区的时间。我们处于+8时区(北京标准时间),因此真实时间值比ISODate(MongoDB存储时间)多8个小时。也就是说,MongoDB存储的时间比ISODate早8小时。

验证8小时

通过DataGrip查看数据库集合字段类型是ISODate:
在这里插入图片描述
其格式是yyyy-MM-ddTHH:mm:ss.SSSZ
在这里插入图片描述
然后再看看时区问题。

同一个用户产生的数据(用户唯一ID都是65af62bee13f080008816500),在MySQL和MongoDB里都有记录。

MySQL数据如下(因为涉及敏感信息,截图截得比较小,熟悉DataGrip的同学,看到Tx: Auto,应该不难猜到就是MySQL):
在这里插入图片描述
而MongoDB记录的数据如下(同样也是出于截图敏感考虑,主流数据库里使用到ObjectId的应该不多吧,MongoDB是一个):
在这里插入图片描述
不难发现。MySQL里记录的数据比MongoDB里记录的数据晚8小时,也是一个符合实际的数据。

PS:此处的所谓符合实际,指的是符合用户习惯,我们App是一款低频App,极少有用户在半夜或凌晨使用,而MongoDB里则记录着大量凌晨的数据,实际上应该是北京时间早上的用户使用记录和数据。

从上面两个截图来看,虽然有打码处理,但依稀可以看到确实(参考下面在线加解密工具网站)是同一个用户(手机号)产生的两个不同数据库(MySQL及MongoDB)数据。

证明:MongoDB里存储的数据确实比MySQL的数据早8小时。

解决方案

PO实体类保持Instant类型不变,Repository层Interface接口方法传参Instant。平常使用的Date如何转换成Instant呢?

直接toInstant()即可,也就是上面的单元测试里面的第四种方式。方法定义:

/**
 * 加不加Query注解都可以。
 * 加注解的话,方法名随意,见名知意即可。
 * 不加注解的话,则需要保证查询字段是MongoDB一级字段,并且满足JPA约定大于配置规范。
 */
@Query("{ 'creationDate': {$gte: ?0, $lte: ?1} }")
long countByCreationDate(Instant start, Instant end);

查询方法:

long count = accountRepository.countByCreationDate(DateUtils.parse(startTime).toInstant(), DateUtils.parse(endTime).toInstant());

源码分析

Date.toInstant()源码

private transient BaseCalendar.Date cdate;
private transient long fastTime;

public Instant toInstant() {
    return Instant.ofEpochMilli(getTime());
}

/**
 * Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT
 * represented by this Date object.
 */
public long getTime() {
    return getTimeImpl();
}

private final long getTimeImpl() {
    if (cdate != null && !cdate.isNormalized()) {
        normalize();
    }
    return fastTime;
}

private final BaseCalendar.Date normalize() {
    if (cdate == null) {
        BaseCalendar cal = getCalendarSystem(fastTime);
        cdate = (BaseCalendar.Date) cal.getCalendarDate(fastTime,
                                                        TimeZone.getDefaultRef());
        return cdate;
    }

    // Normalize cdate with the TimeZone in cdate first. This is
    // required for the compatible behavior.
    if (!cdate.isNormalized()) {
        cdate = normalize(cdate);
    }

    // If the default TimeZone has changed, then recalculate the
    // fields with the new TimeZone.
    TimeZone tz = TimeZone.getDefaultRef();
    if (tz != cdate.getZone()) {
        cdate.setZone(tz);
        CalendarSystem cal = getCalendarSystem(cdate);
        cal.getCalendarDate(fastTime, cdate);
    }
    return cdate;
}

Instant.java源码:

/**
 * Constant for the 1970-01-01T00:00:00Z epoch instant.
 */
public static final Instant EPOCH = new Instant(0, 0);

public static Instant ofEpochMilli(long epochMilli) {
    long secs = Math.floorDiv(epochMilli, 1000);
    int mos = Math.floorMod(epochMilli, 1000);
    return create(secs, mos * 1000_000);
}
private static Instant create(long seconds, int nanoOfSecond) {
    if ((seconds | nanoOfSecond) == 0) {
        return EPOCH;
    }
    if (seconds < MIN_SECOND || seconds > MAX_SECOND) {
        throw new DateTimeException("Instant exceeds minimum or maximum instant");
    }
    return new Instant(seconds, nanoOfSecond);
}

敏感数据加解密

上面截图,MySQL表里,对手机号没有加密处理,直接明文存储;而在MongoDB数据库里,则进行ECB加密。加密工具类略,

此处,附上一个好用的在线加密工具网站,可用于加密手机号等比较敏感的数据,编码一般选择Base64,位数、模式、填充、秘钥等信息和工具类保持一致(除密钥外,一般都是默认):
在这里插入图片描述

工具类

DateUtils.java工具类源码如下

public static final String DAY_START = " 00:00:00";
public static final String DAY_END = " 23:59:59";
public static final String DATE_FULL_STR = "yyyy-MM-dd HH:mm:ss";

/**
 * 使用预设格式提取字符串日期
 *
 * @param date 日期字符串
 */
public static Date parse(String date) {
    return parse(date, DATE_FULL_STR);
}

/**
 * 不建议使用,1945-09-01 和 1945-09-02 with pattern = yyyy-MM-dd 得到不一样的时间数据,
 * 前者 CDT 后者 CST
 * 指定指定日期字符串
 */
public static Date parse(String date, String pattern) {
    SimpleDateFormat df = new SimpleDateFormat(pattern);
    try {
        return df.parse(date);
    } catch (ParseException e) {
        log.error("parse failed", e);
        return new Date();
    }
}

public static Date parseThenUtc(String date, String dateFormat) throws ParseException {
    SimpleDateFormat format = new SimpleDateFormat(dateFormat);
    Date start = format.parse(date);
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(start);
    calendar.add(Calendar.HOUR, -8);
    return calendar.getTime();
}

/**
 * 减 8 小时
 */
public static Date parseThenUtc(String date) throws ParseException {
    return parseThenUtc(date, DATE_FULL_STR);
}

中文解析

SimpleDateFormat,作为Java开发中最常用的API之一。

你真的熟悉吗?
线程安全问题?
是否支持中文日期解析呢?

具体来说,是否支持如yyyy年MM月dd日格式的日期解析?

测试程序:

public static void main(String[] args) {
    log.info(getNowTime("yyyy年MM月dd日"));
}

public static String getNowTime(String type) {
    SimpleDateFormat df = new SimpleDateFormat(type);
    return df.format(new Date());
}

打印输出如下:

20240123

结论:SimpleDateFormat支持对中文格式的日期进行解析。

看一下SimpleDateFormat的构造函数源码:

public SimpleDateFormat(String pattern) {
    this(pattern, Locale.getDefault(Locale.Category.FORMAT));
}

继续深入查看Locale.java源码:

private static Locale initDefault(Locale.Category category) {
    Properties props = GetPropertyAction.privilegedGetProperties();

    return getInstance(
        props.getProperty(category.languageKey,
                defaultLocale.getLanguage()),
        props.getProperty(category.scriptKey,
                defaultLocale.getScript()),
        props.getProperty(category.countryKey,
                defaultLocale.getCountry()),
        props.getProperty(category.variantKey,
                defaultLocale.getVariant()),
        getDefaultExtensions(props.getProperty(category.extensionsKey, ""))
            .orElse(defaultLocale.getLocaleExtensions()));
}

大概得知:SimpleDateFormat对于本地化语言的支持是通过Locale国际化实现的。

ISODate

另外在使用SimpleDateFormat解析这种时间时需要对T和Z加以转义。

public static final String FULL_UTC_STR = "yyyy-MM-dd'T'HH:mm:ss'Z'";
public static final String FULL_UTC_MIL_STR = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

public static String getBirthFromUtc(String dateStr) {
    SimpleDateFormat df = new SimpleDateFormat(FULL_UTC_STR);
    try {
        Date date = df.parse(dateStr);
        Calendar calender = Calendar.getInstance();
        calender.setTime(date);
        calender.add(Calendar.HOUR, 8);
        return date2Str(calender.getTime(), DATE_SMALL_STR);
    } catch (ParseException e) {
        throw new RuntimeException(e);
    }
}

结论

几个结论:

  • JPA写法对于单表查询非常简单,借助于IDEA智能提示,可以快速写出查询Interface方法
  • JPA很强,但对于关系型数据库的多表Join查询,或MongoDB的嵌套字段查询,则几乎派不上用场
  • @Query通过注解的方式可以大大简化API的使用
  • @Query写法和JPA写法不能混为一谈
  • @Query也不是万能的。必要时,还是得使用QBE,Query By Example,或Query Criteria

参考

  • MongoDB进阶与实战:微服务整合、性能优化、架构管理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1413447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql 存储过程学习

存储过程介绍 1.1 SQL指令执行过程 从SQL执行的流程中我们分析存在的问题: 1.如果我们需要重复多次执行相同的SQL&#xff0c;SQL执行都需要通过连接传递到MySQL&#xff0c;并且需要经过编译和执行的步骤; 2.如果我们需要执行多个SQL指令&#xff0c;并且第二个SQL指令需要…

哪吒汽车与经纬恒润合作升级,中央域控+区域域控将于2024年落地

近日&#xff0c;在2024哪吒汽车价值链大会上&#xff0c;哪吒汽车与经纬恒润联合宣布合作升级&#xff0c;就中央域控制器和区域域控制器展开合作&#xff0c;合作成果将在山海平台新一代车型上发布。 哪吒汽车首席技术官戴大力、经纬恒润副总裁李伟 经纬恒润在智能驾驶领域拥…

数据结构(绪论+算法的基本概念)

文章目录 一、绪论1.1、数据结构的基本概念1.2、数据结构三要素1.2.1、逻辑结构1.2.2、数据的运算1.2.3、物理结构&#xff08;存储结构&#xff09;1.2.4、数据类型和抽象数据类型 二、算法的基本概念2.1、算法的特性2.2、“好”算法的特质2.2.1、算法时间复杂度2.2.2、算法空…

一款强大的矢量图形设计软件:Adobe Illustrator 2023 (AI2023)软件介绍

Adobe Illustrator 2023 (AI2023) 是一款强大的矢量图形设计软件&#xff0c;为设计师提供了无限创意和畅行无阻的设计体验。AI2023具备丰富的功能和工具&#xff0c;让用户可以轻松创建精美的矢量图形、插图、徽标和其他设计作品。 AI2023在界面和用户体验方面进行了全面升级…

【GTK】GtkApplicationWindow/GtkApplication

一、GtkApplicationWindow 在GTK&#xff08;GIMP Toolkit&#xff09;应用程序开发中&#xff0c;并不是每个GTK程序都必须有一个GtkApplicationWindow&#xff0c;但大多数基于GTK的图形界面应用程序都会使用GtkApplicationWindow&#xff0c;因为它是一个方便的类&#xff…

幻兽帕鲁搭建私服,一键更新方法

看着帕鲁这么火&#xff0c;估计更新会变为常态了&#xff0c;如果有自己搭建私服的话&#xff0c;跟着我下面的方法去进行更新吧&#xff01; 如果你还没有自己的私服&#xff0c;快去三五十搞一个吧&#xff0c;只需三五分钟&#xff0c;叫上你的小伙伴一起去搞起来吧 只需3分…

EXCEL VBA抓取网页JSON数据并解析

EXCEL VBA抓取网页JSON数据并解析 链接地址&#xff1a; https://api.api68.com/CQShiCai/getBaseCQShiCaiList.do?lotCode10036&date2024-01-26 Sub test() On Error Resume Next Sheet.Select Sheet1.Cells.ClearContents [a1:g1] Split("preDrawIssue|preDrawTi…

呼叫中心语音质检解决方案-苏州磐石云

呼叫中心语音质检解决方案是一种利用自动语音识别&#xff08;ASR&#xff09;和自然语言处理&#xff08;NLP&#xff09;技术来分析和评估呼叫录音的工具。它可以帮助呼叫中心监控和改进客户服务质量&#xff0c;提供以下功能和特点&#xff1a; 语音转文本&#xff1a;利用A…

dd 命令创建10G大小的文件

您可以使用Linux命令行工具dd来创建10GB大小的文件。以下是创建10GB文件的命令&#xff1a;dd if/dev/zero offile.img bs1G count10 在上述命令中&#xff0c;if参数指定输入文件为/dev/zero&#xff0c;这是一个特殊的文件&#xff0c;读取它会返回空字符&#xff08;即零&am…

通过CanvasRenderer.SetColor和Image.color修改UI组件颜色的区别

1&#xff09;通过CanvasRenderer.SetColor和Image.color修改UI组件颜色的区别 2&#xff09;OPPO相关机型没法在Unity启用90或120FPS 3&#xff09;手机输入法中的emoji 4&#xff09;Unity Application Patching怎么用 这是第369篇UWA技术知识分享的推送&#xff0c;精选了UW…

Linux常见指令汇总

目录 pwd√ ls√ cd√ 对文件的理解√ which√ alias√ touch√ linux系统目录结构√ mkdir / tree √ rmdir / rm √ man√ nano√ cp√ mv√ cat√ echo√ linux设计理念和三种重定向总结√ more/less√ head/tail√ wc√ uniq√ date / cal√ find√…

Qt Design Studio+Pyside项目

Qt Design Studio设计出的项目结构有多个层级的目录&#xff0c;我们直接用类似Qt Creator工具的方式加载main.qml文件时会报错提示module "content" is not installed&#xff0c;将content加入importPath后还是报同样的错误。 Qt Design Studio生成的文件包含了.qm…

传统图像处理算法的缺点是什么?

问题描述&#xff1a;传统图像处理算法的缺点是什么&#xff1f; 问题解答&#xff1a; 传统图像处理方法是基于一组参数&#xff0c;这些参数是在特定条件下对获取到的图像进行手动调整的。这些条件的任何变化&#xff0c;例如照明&#xff0c;都可能影响这些方法的可检测效…

element+vue 之 v-limit 按钮操作权限

1.新建一个permission.js文件 import store from /storeexport default {inserted: function (el, binding) {const { perms: limits } store.state.userconst { value: params } bindingif (!limits.length) returnif (params && Array.isArray(params)) {if (!limi…

three.js 鼠标选中模型弹出标签

效果&#xff1a;请关注抖音 代码&#xff1a; <template><div><el-container><el-main><div class"box-card-left"><div id"threejs" style"border: 1px solid red;position: relative;"></div><…

前端工程化之:CSS工程化+Less

一、什么是Less&#xff1f; Less官网 Less中文网 Less 是一种更加简洁的样式代码&#xff0c;它非常像 CSS &#xff0c;但又不太一样&#xff0c;它让编写样式变得更容易。 Less 代码虽好&#xff0c;但它无法被浏览器识别&#xff0c;因此需要一个工具将其转换为纯正的 CS…

软件包管理:在CentOS 7中部署Tengine

目录 下载&#xff1a; 方法一&#xff1a; 方法二&#xff1a; 部署&#xff1a; 实验操作 下载&#xff1a; 方法一&#xff1a; 1、打开浏览器搜索tengine并点击官网 2、选择需要安装的版本并复制链接链接 标题栏处可以更改为中文界面 下滑选择版本单击下载 在远程连…

第四篇【传奇开心果短博文系列】鸿蒙开发技术点案例示例:TabBar和Navbar实现页面切换

传奇开心果短博文系列 系列短博文目录鸿蒙开发技术点案例示例短博文系列 短博文目录一、前言二、给Index.ets添加TabBar组件三、用TabBar组件实现页面切换四、给Index.ets添加Navbar组件五、使用Navbar独立实现页面切换六、Navbar和TabBar同时在Index.ets主页实现各自独立的页面…

taskflow 源码阅读笔记-1

之前写了一篇介绍Taskflow的短文&#xff1a;传送门 Taskflow做那种有前后依赖关系的任务管理还是不错的&#xff0c;而且他的源码里运用了大量C17的写法&#xff0c;觉得还是非常值得学习的&#xff0c;因此决定看一下他的源码&#xff0c;这里顺便写了一篇代码学习笔记。 概…

【深度学习】sdxl中的 text_encoder text_encoder_2 区别

镜像问题是&#xff1a;https://editor.csdn.net/md/?articleId135867689 代码仓库&#xff1a; https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/tree/main 截图&#xff1a; 为什么有两个CLIP编码器 text_encoder 和 text_encoder_2 &#xff1f; 在…