一、 三大场景下的最佳时间类型
1、JSON:字符串(String)
当服务需要与外部世界(前端、APP、第三方服务)通过 API 交换数据时,最通用的格式是 JSON。然而,JSON 本身并没有“日期时间”这个基本类型。
因此,我们必须选择一种所有系统都能理解的格式来表示时间。标准化的字符串成为了不二之选。
为什么是字符串?
通用性:任何语言都能轻松处理字符串。
可读性:"2025-07-30T16:09:00" 这样的格式,人类开发者一眼就能看懂,极大地方便了调试。
标准化:业界事实上的标准 ISO 8601 格式 (YYYY-MM-DDTHH:mm:ss) 避免了歧义。
最佳实践:在 DTO (Data Transfer Object) 中,将所有需要与外部交互的时间字段,都定义为 String 类型。
// DataProductItemDto.java
public class DataProductItemDto {
// ...
private String listTime; // 接收来自外部的原始时间字符串
}
这样做的好处是,把“如何解析这个字符串”的复杂性留给了健壮的后端,而不是在数据进入系统的第一步就可能因格式问题而失败。
YYYY-MM-DDTHH:mm:ss 和 YYYY-MM-DD HH:mm:ss 分别是什么格式?
YYYY-MM-DDTHH:mm:ss:
名称: 这是 ISO 8601 标准日期时间格式的最常见形式。 ISO 8601 是一套由国际标准化组织(ISO)发布的、用于表示日期和时间的国际标准。它定义的就是一套文本(字符串)的表示规则。
特点: 关键在于中间的那个大写字母 T。它是一个标准的分隔符,用来明确地分隔日期部分和时间部分。
用途: 这种格式在计算机系统之间的数据交换中被广泛使用(例如,API 的 JSON 响应、XML 数据),因为它没有歧义,非常便于机器解析。
YYYY-MM-DD HH:mm:ss:
名称: 这是一种更偏向人类可读的日期时间格式,也是很多数据库(如 MySQL)默认的 DATETIME 格式。'yyyy-MM-dd HH:mm:ss' 是 DATETIME 类型能理解和展示的一种字符串格式,但它不是 DATETIME 类型在数据库中存储的物理格式。
特点: 它使用一个空格来分隔日期和时间。
用途: 常见于日志文件、数据库显示以及需要直接呈现给用户看的场景。
2、Java 业务逻辑:专用日期时间对象
一旦时间数据进入了后端系统,首要任务就从“数据交换”变成了“业务处理”。这时,字符串的劣势尽显无疑,而 Java 的专用日期时间类型则大放异彩。
为什么必须用专用类型?
类型安全: 保证了变量里存储的一定是一个有效的时刻。
强大的计算与比较能力: java.util.Date 和(更推荐的)Java 8+ 的 java.time 包 (LocalDateTime, ZonedDateTime 等) 提供了海量的 API,可以轻松实现日期的前后比较 (isBefore())、加减计算 (plusDays()) 等操作。
最佳实践:在 Entity (实体类) 或 Domain Object (领域对象) 中,将所有时间字段都定义为 Java 的专用日期时间类型。
// DataProduct.java
public class DataProduct {
// ...
private LocalDateTime listTime; // 使用 LocalDateTime 进行内部处理
}
3、数据库:DATETIME / TIMESTAMP
当我们需要将时间数据永久保存时,数据库的专用类型是唯一正确的选择。
为什么必须用专用类型?
高效查询与索引: 数据库能为 DATETIME 或 TIMESTAMP 字段建立高效的索引,使得按时间范围的查询(例如 WHERE list_time BETWEEN ... AND ...)快如闪电。如果用字符串,这将导致无法使用索引的全表扫描。
原生计算函数: 数据库提供了丰富的日期函数(DATE_ADD, DATEDIFF 等),可以直接在 SQL 层面完成复杂的日期计算。
数据完整性: 字段类型本身就是一种约束,确保了只有有效的日期时间才能被存入,从源头保证了数据质量。
CREATE TABLE `data_product` (
-- ...
`list_time` DATETIME DEFAULT NULL COMMENT '产品上架时间',
-- ...
);
DATETIME vs TIMESTAMP 的区别
这是 MySQL 中一个非常经典的问题,它们都用来存储日期和时间,但有几个关键的区别:
特性 | DATETIME | TIMESTAMP |
存储空间 | 8 字节 | 4 字节 |
表示范围 | 1000-01-01 00:00:00 到 9999-12-31 23:59:59 (范围大) | 1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC (范围小) |
时区处理 | 与时区无关 (Timezone-Independent) | 与时区相关 (Timezone-Dependent) |
自动更新 | 默认无,但可以手动配置 (ON UPDATE CURRENT_TIMESTAMP) | 默认就有 ON UPDATE CURRENT_TIMESTAMP 的特性 (在旧版本MySQL中) |
最重要的区别:时区处理
对于 create_time 和 update_time 这种审计字段,使用 TIMESTAMP 是一个非常好的选择。
对于其他业务相关的、需要表示特定本地时间的字段,使用 DATETIME 通常更简单、更不容易出错。
二、数据流转与转换
如果在不同层次使用了不同的类型,那么必然需要在它们之间进行转换。
这个转换通常发生在 Service 层或专门的 Converter/Mapper 中(例如使用 MapStruct)。
标准的数据流转与转换过程
入口 (Inbound): JSON -> DTO -> Entity
Controller: 接收 HTTP 请求,Spring (配合 Jackson) 将 JSON 中的 "listTime": "..." 字符串,自动绑定到 DTO 的 String listTime 属性上。
Service/Converter: 执行转换。调用日期解析工具(如 LocalDateTime.parse()),将 DTO 中的 String 解析成 LocalDateTime 对象。
赋值: 将这个 LocalDateTime 对象,赋值给 Entity 的 LocalDateTime listTime 属性。
持久化: ORM 框架 (如 MyBatis) 将 Entity 中的 LocalDateTime 对象正确地写入数据库的 DATETIME 字段。
出口 (Outbound): Entity -> VO -> JSON
查询: 从数据库查出 Entity,其 listTime 属性是一个 LocalDateTime 对象。
Service/Converter: 执行转换。调用日期格式化工具(如 DateTimeFormatter),将 Entity 中的 LocalDateTime 对象,格式化成前端需要的 "YYYY-MM-DD HH:mm:ss" 格式的字符串。
赋值: 将这个字符串,赋值给 VO (View Object,一种特殊的DTO) 的 String listTime 属性。
Controller: 返回 VO 对象,Spring (配合 Jackson) 将其序列化为 JSON,最终 listTime 字段以字符串的形式呈现在前端。
三、MapStruct 中的日期转换
MapStruct 让这个转换过程变得极其优雅。只需要在 Mapper 接口中声明规则即可:
@Mapper(componentModel = "spring")
public interface DataProductConverter {
@Mapping(source = "listTime", target = "listTime", dateFormat = "yyyy-MM-dd'T'HH:mm:ss")
DataProduct toEntity(DataProductItemDto dto); // String -> Date
@Mapping(source = "listTime", target = "listTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
DataProductVo toVo(DataProduct entity); // Date -> String
}
@Mapping(..., dateFormat = "yyyy-MM-dd'T'HH:mm:ss"):
作用: 这行代码是在明确地告诉 MapStruct:“请准备一个认识字母 T 的日期解析/格式化工具。”
'T': 在 SimpleDateFormat 或 DateTimeFormatter 的模式字符串中,任何不属于预定义格式字母(如 y, M, d)的普通字符,如果想让它被原样匹配,就需要用单引号 '' 把它包起来。所以 'T' 的意思就是“请精确匹配一个大写字母T”。
何时使用: 当你确定要处理的日期字符串中,分隔符就是 T 时,必须使用这个格式。
@Mapping(..., dateFormat = "yyyy-MM-dd HH:mm:ss"):
作用: 这行代码是告诉 MapStruct:“请准备一个认识空格的日期解析/格式化工具。”
何时使用: 当你要处理的日期字符串中,分隔符是空格时,就使用这个格式。
总结: 它们的唯一区别,就是定义了日期和时间之间的分隔符。你必须根据你实际要处理的字符串,选择与之完全匹配的 dateFormat 模式。
四、总结
对外用字符串 (DTO/VO):与外部系统(API, 前端)交互时,使用 ISO 8601 格式的字符串作为时间的“通用护照”,保证兼容性和可读性。
对内用对象 (Entity):在 Java 业务逻辑和领域模型中,使用 java.time 包下的专用日期时间对象,以利用其强大的类型安全和计算能力。
存储用专用列 (Database):在数据库中,使用 DATETIME 或 TIMESTAMP 类型进行持久化,以确保查询性能、计算能力和数据完整性。
在边界处转换 (Service/Converter):在数据进入和流出你的业务核心层时,进行明确的、有意识的类型转换。