赞
踩
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);
依旧是不生效的。
基于上面的结论,有一版新的写法:
@Query("{ 'profiles.channel': ?0, 'creationDate': {$gte: ?1, $lte: ?2} }")
List<Account> findByChannelAndCreationDate(String channel, Date start, Date end);
此时输出:size:28
。这个数据看起来才比较正常(虽然后面的结论证明不是正确的)。
如果不过滤渠道呢?查询某个日期时间段内所有渠道的全量用户数据?
两种写法都可以:
long countByCreationDateBetween(Date start, Date end);
@Query("{ 'creationDate': {$gte: ?0, $lte: ?1} }")
long countByCreationDate(Date start, Date end);
等等。怎么第一种写法,IDEA给出一个WARN??
上面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引发的问题),为了消除告警,有两种方式:
那到底应该怎么修改呢?才能屏蔽掉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类型不一致。
为啥呢??
MongoDB中的日期使用Date类型表示,在其内部实现中采用一个64位长的整数,该整数代表的是自1970年1月1日零点时刻(UTC)以来所经过的毫秒数。Date类型的数值范围非常大,可以表示上下2.9亿年的时间范围,负值则表示1970年之前的时间。
MongoDB的日期类型使用UTC(Coordinated Universal Time)进行存储,也就是+0时区的时间。我们处于+8时区(北京标准时间),因此真实时间值比ISODate(MongoDB存储时间)多8个小时。也就是说,MongoDB存储的时间比ISODate早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());
}
打印输出如下:
2024年01月23日
结论: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国际化实现的。
另外在使用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);
}
}
几个结论:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。