V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
MuXia
V2EX  ›  Java

询问一个关于 Java 日期在数据库存储的格式问题

  •  
  •   MuXia · 2022-07-12 16:20:36 +08:00 · 3408 次点击
    这是一个创建于 894 天前的主题,其中的信息可能已经有所发展或是发生改变。

    java 新人目前只在一家公司工作过

    想问下各位,一般 Java 写后端的话,对于日期字段的处理,一般数据库存储的是什么格式的?

    时间戳? yyyy-MM-dd HH:mm:ss 格式字符串? 还是其他的?

    第 1 条附言  ·  2022-07-12 19:57:52 +08:00

    会提出这个问题是因为今早看到了这篇帖子 /865425 中说到了时间库的问题,

    然后又去查询了一下文中提到时间库自带的一些方法,发现确实挺方便的,但没看到转化时间戳的方法(不知是我没看见,还是没有)

    结合现在开发的项目,就有了这个问题

    谢谢各位的回复,让我这个疑惑得以解开

    在校的时候,自己也写过一些小项目,当时就时间处理十分头大,现在逐渐清晰了

    32 条回复    2022-07-14 12:21:46 +08:00
    Jooooooooo
        1
    Jooooooooo  
       2022-07-12 16:23:49 +08:00
    都行, 看现有的系统用啥, 跟着用一样的.

    时间戳或者 string 或者 timestamp 都可以.
    bxb100
        2
    bxb100  
       2022-07-12 16:30:48 +08:00 via Android
    UTC 时间戳
    unco020511
        3
    unco020511  
       2022-07-12 16:34:43 +08:00
    有海外业务:时间戳,仅国内业务:存格式化后的字符串(GTM+8 本地时区)
    gam2046
        4
    gam2046  
       2022-07-12 16:36:28 +08:00
    数据库都有日期类型,如果实在特殊原因不想、不能用。时间戳是唯一选择。所有语言都有简单的方法将时间戳转换成日期,并且也方便做本地化。
    dqzcwxb
        5
    dqzcwxb  
       2022-07-12 16:37:56 +08:00
    你猜猜数据库为什么要弄一个 datetime 类型出来
    cheng6563
        6
    cheng6563  
       2022-07-12 16:42:44 +08:00
    跨时区的国际系统可以用时间戳,其他的用 datetime 就行了
    neptuno
        7
    neptuno  
       2022-07-12 17:02:40 +08:00
    新项目一律时间戳
    nothingistrue
        8
    nothingistrue  
       2022-07-12 17:53:26 +08:00
    常规数据库有两种日期时间格式,一种是年、月、日、时、分、秒、毫秒组合存储,另一种是时间戳存储,即自 1970 年 0 点开始的毫秒数,内部是数值类型。第一种类型没有时区(查询出来的显示值,不随环境变量当中的时区而改变),Mysql 还细分为 Date 和 DateTime ,Oracle 则统一为 Date ,对应的 Java 类型是 java.time.LoacalDate 和 LocalDateTime 。第二种类型有时区(查询出来的显示值,随环境变量当中的时区的不同而不同),有的数据库单位直到毫秒,有得能到微秒,对应的 Java 类型是 java.sql.Date 或 java.sql.Timestamp (取决于要哪个单位)。

    一般来说,如果是新项目,一律考虑使用 Date / DateTime - java.time.LocalDate / LocalDateTime ,即使要国际化(时区上你可以在程序层面再控制转换成 ZonedDateTime ,甚至还可以将时区国际化直接交给前端处理),在数据库上处理时区会是个灾难。
    dcsuibian
        9
    dcsuibian  
       2022-07-12 18:03:26 +08:00
    用 long 存毫秒级时间戳,足够用到天荒地老。
    MySQL 的 timestamp 不要用,只有 4 字节,除非你想在 2038 年引起下一个千年虫。

    对于精准时间点,时间戳特别好用。没有时区、夏令时问题。闰秒操作系统会帮你吃掉。
    连接数据库不用担心 serverTimezone=GMT%2B8 问题
    时间不对,排查点就基本可以缩小到 Format
    MuXia
        10
    MuXia  
    OP
       2022-07-12 19:40:48 +08:00
    @nothingistrue #8 回复的很详细,新知识 get
    dorothyREN
        11
    dorothyREN  
       2022-07-12 20:20:29 +08:00
    pg 的话 用 timestamptz 是带时区的时间戳,java 用 Timestamp 类型
    realpg
        12
    realpg  
       2022-07-13 01:53:10 +08:00
    任何时候都存时间戳,不分语言
    因为时间戳是绝对单位,且服务器设置好时区转换当地时间方便
    nothingistrue
        13
    nothingistrue  
       2022-07-13 10:05:04 +08:00
    @MuXia 忽略我之前的回复,有错误。各数据库的日期时间保存格式,都不相同,我说得只在 Mysql 上是正确的。映射那里也写错了,与 JDBC 规范不符合。

    正确的应该是:
    java.time.LocalDateTime ,无时区日期时间(显示值即值,没有内部值,对应的现实时间随时区浮动),JDBC 类型是 TIMESTAMP ;
    java.time.LocalDate ,无时区日期(显示值即值,没有内部值,与现实时间没有直接对应关系),JDBC 类型是 Date ;
    java.time.LocalTime ,无时区当天时间(显示值即值,没有内部值,对应的现实时间随时区+天浮动),JDBC 类型是 Time ;
    java.time.OffsetDateTime ,偏移量日期时间(对应现实完整时间,内部值固定,显示值随时区偏移),JDBC 类型是 TIMESTAMP ;
    java.time.OffsetTime ,偏移量当天时间(对应现实当天时间,内部值固定,显示值随时区偏移),JDBC 类型是 TIMESTAMP ;
    java.time.ZonedDateTime ,基本等同于 OffsetDateTime ,区别只是一个是 CST 时区,一个是+/-数字时区。

    详细可见 : https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#basic-provided 表 2 。注意上面的 Date 、Time 、TIMESTAMP 都是 JDBC 类型,具体是什么类型取决于各数据库厂商提供的 JDBC 驱动。另外 TIMESTAMP 是时间戳 + 时区,不是只有时间戳。

    这里面 Mysql 提供了个狗屎。它的 DateTime 只是歪打正着的跟 java.time.LocalDateTime 对应,但也只在 JVM 时区跟 数据库时区一致的情况下才这样,不一致的时候要出问题。而它的 Timestamp 则完全无法使用。
    MuXia
        14
    MuXia  
    OP
       2022-07-13 10:12:12 +08:00
    @nothingistrue #13 总感觉这对应关系挺混乱的,我现在处理起来都是直接数据库字段用 varchar 或 int 来存时间戳,在代码里面去转化
    nothingistrue
        15
    nothingistrue  
       2022-07-13 10:14:09 +08:00
    一般来说,不考虑国际化的时候,还是要用 java.time.LocalDateTime ,这个更贴近需求,而且就算狗屎 Mysql 也能正好提供实现。

    考虑国际化的时候,应当用 java.time.OffsetDateTime/java.time.ZonedDateTime ,但是在数据库映射上要做特殊处理,不是所有的数据库都支持这种映射,比如 Mysql 。
    MuXia
        16
    MuXia  
    OP
       2022-07-13 10:20:23 +08:00
    @nothingistrue #15 行,有空再去了解一下
    nothingistrue
        17
    nothingistrue  
       2022-07-13 10:20:28 +08:00
    @MuXia 不要用单一的数值类型存时间戳,那实际上隐式存了一个 JDBC 时区,该时区依赖于 JVM 时区和 JDBC 驱动,当 JVM 时区、JDBC 驱动、或者只是 JDBC 连接配置( Mysql 就是个典型)发生变化的时候,会发生很难处理的时间偏移问题。要存时间戳,必须用数据库的带时区 Timestamp ,或者数字列+时区列两列存储时间。
    MuXia
        18
    MuXia  
    OP
       2022-07-13 10:28:04 +08:00
    @nothingistrue #17 那#9 楼说的这个问题能否规避呢,如果纯国内应用的话,应该不用考虑时区变更的问题
    nothingistrue
        19
    nothingistrue  
       2022-07-13 10:32:49 +08:00
    @MuXia #17 若不考虑国际化,就用 java.time.LocalDateTime 对应 Mysql DateTime ,只要保证 JVM 、Mysql 、JDBC 连接配置都是东八区,其他时候就都不用考虑时区问题。
    MuXia
        20
    MuXia  
    OP
       2022-07-13 10:48:02 +08:00
    @nothingistrue #19
    ```
    MySQL 的 timestamp 不要用,只有 4 字节,除非你想在 2038 年引起下一个千年虫。
    ```
    九楼说的这个问题是不是无解?
    siweipancc
        21
    siweipancc  
       2022-07-13 11:03:53 +08:00 via iPhone
    @dcsuibian 最近搞智能硬件就用 long 存储了,还被同事吐槽,查个时间范围的数据还得手动转一次,我只能说开心就好
    dcsuibian
        22
    dcsuibian  
       2022-07-13 11:45:37 +08:00 via Android
    @nothingistrue
    @MuXia
    时间戳是跟当前在什么时区无关的。
    https://www.liaoxuefeng.com/article/978494994163392
    dcsuibian
        23
    dcsuibian  
       2022-07-13 11:50:46 +08:00 via Android
    nothingistrue
        24
    nothingistrue  
       2022-07-13 12:09:35 +08:00
    @dcsuibian #21 并不是,你所谓的无关,其实是隐含了 UTC 0 。数值型的时间戳,都是基于 UNIX 时间戳,而 UNIX 时间戳的定义是:从 UTC 1970 年 1 月 1 日 0 时 0 分 0 秒起至现在的总秒数。单独的一个数值,不是时间戳,带上时区,最起码要隐含 UTC 0 ,才是时间戳。

    其实时间戳最大的问题,不是带时区,而是这个时区怎么带,根本没有统一规范。有的总是存储 UTC0 , 有的读写时跟随随环境变量(会发生因不同时区导致的读写不一致问题),有的将时区跟数值一起保存。
    dcsuibian
        25
    dcsuibian  
       2022-07-13 13:05:00 +08:00
    @nothingistrue 不带时区的。你可以先正常运行一遍:
    System.out.println(System.currentTimeMillis());
    然后,换个时区再运行一次,你看看这两个数字差了多少就知道了。(毫秒)

    时间戳只是针对某个时间点的偏移量。只不过这个时间点是 UTC 1970 年 1 月 1 日 0 时 0 分 0 秒
    完全可以说时间戳是从 UTC+8 1970 年 1 月 1 日 8 时 0 分 0 秒 起至现在的秒数,这俩就是同一个时间点。
    只不过挑一个基准时间点,总归要挑个规整点的罢了。
    nothingistrue
        26
    nothingistrue  
       2022-07-13 17:54:05 +08:00
    @dcsuibian System.currentTimeMillis() 生成的是 UTC0 时间戳,隐式 UTC0 ,不代表没有 UTC 0 。这个区别很重要,因为有些工具生成的当前时间不是 UTC0 的。

    试试 Mysql 下执行这个 SELECT CURRENT_TIMESTAMP(),LOCALTIMESTAMP(),UTC_TIMESTAMP(),NOW() FROM DUAL;

    另外实际上只有 UNIX 时间戳才是从 UTC 1970 年 1 月 1 日 0 时 0 分 0 秒起至现在的总秒数,ISO 8601 时间戳就是年月日时分表时区的组合。
    dcsuibian
        27
    dcsuibian  
       2022-07-13 21:18:21 +08:00
    @nothingistrue MySQL 的 Timestamp 会在你 select 帮你格式化成文本帮助你阅读,格式化时就会用到时区信息,但底层的数字是没有的时区信息的。

    对应的,你套上 UNIX_TIMESTAMP()函数,然后再看看。
    SELECT
    UNIX_TIMESTAMP(CURRENT_TIMESTAMP()),UNIX_TIMESTAMP(LOCALTIMESTAMP()),UNIX_TIMESTAMP(UTC_TIMESTAMP()),UNIX_TIMESTAMP(NOW())
    FROM DUAL;

    世界上各个时区的人在同一时间点调用 System.currentTimeMillis() 拿到的是同一个数字。
    那用这个数字来表示时间点就不会因为时区、显示而产生歧义了啊
    dcsuibian
        28
    dcsuibian  
       2022-07-13 23:08:51 +08:00
    @nothingistrue 扯远了。
    回到用 long 存时间戳的问题上,假如现在有一台 MySQL (无论在哪儿),Java 程序 1 在北京,Java 程序 2 在纽约,它们都连接着这个数据库。

    实验 1:
    先是北京产生了一条记录,然后 10 分钟后纽约产生了一条记录。Java 程序都使用 System.currentTimeMillis()将得到的 long 数字存入数据库。那么这两条记录差的大概就是 10*60*1000 毫秒。无论你是否设置了 serverTimezone 参数,可测试。

    实验 2:
    使用如下 Java 程序插入一条新纪录。
    String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC%2B8";
    Connection conn = DriverManager.getConnection(url, "root", "password");
    PreparedStatement stmt = conn.prepareStatement("INSERT INTO record(`time`) VALUES(?)");
    Date date = new Date();
    stmt.setObject(1, date);
    stmt.execute();
    stmt.close();
    conn.close();
    在保留和去除 serverTimezone=UTC%2B8 这个参数的情况下分别插入一条数据。那么你在数据库里看到的时间差别就很大。这是因为虽然 MySQL 底层虽然用了存整形的方法存时间戳。但你 insert 的时候仍然得用字符串:
    INSERT INTO record(`time`) VALUES('2022-07-13 00:00:00')
    这就有了借助时区的转换过程,然后就会有问题。
    nothingistrue
        29
    nothingistrue  
       2022-07-14 09:40:07 +08:00
    @dcsuibian 你到现在还没发现问题吗,同一个 Unix 时间戳值,在读取 /显示的时候,不同时区是不一样的。

    你局限在数值的不变上,但一个显示值随时区变化的数值,压根就不能成为数据,完整的数据,要是数值+时区。这就是数值型时间戳必须额外带时区,或者说数值型时间戳跟时区相关的原因。

    你也局限在了 Java 上,java.util.Date 及其相关类的内部值,是存储的自 1970-1-1T00:00:00+0 到现在的毫秒数,但这只是 Java 的规范。Unix 时间戳不是国际标准,其他语言、数据库都可能定义自己的规范,比如有的语言会把时间戳定义为当前时区自 1970-1-1T00:00:00 到现在的毫秒数。结合使用的时候就容易出坑。
    dcsuibian
        30
    dcsuibian  
       2022-07-14 12:01:30 +08:00
    @nothingistrue
    ISO 是国际标准化组织,又不是只面向计算机从业者的,8601 是规定了显示方法。数据的存储和显示相分离的设计原则不是再正常不过了吗?
    我一开始不就说了使用“毫秒”级时间戳。确实时间戳没有国际标准。各种语言都可以轻松地处理。
    Python 的 time.time(),Java 的 System.currentTimeMills(),JavaScript 的 Date.now(),也就差一个 1000 处理。推荐使用“毫秒”时间戳只是因为整数比浮点更好处理罢了。
    最重要是,处理过程中没有涉及到任何“时区”相关的东西?

    这个数字只跟时间点有关。如果你没条件找个其它时区的人跟你一起试的话。至少换个系统时区再试试,关键是



    你到底有没有试过?
    dcsuibian
        31
    dcsuibian  
       2022-07-14 12:14:04 +08:00
    @nothingistrue
    [1-中国标准时间.jpg]( https://dcsuibian-public-resources.oss-cn-hangzhou.aliyuncs.com/img/1-%E4%B8%AD%E5%9B%BD%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4.jpg)

    [2-亚马逊标准时间.jpg]( https://dcsuibian-public-resources.oss-cn-hangzhou.aliyuncs.com/img/2-%E4%BA%9A%E9%A9%AC%E9%80%8A%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4.jpg)

    你看看在切换时间后,时间戳这两个数字差了多少? 36438 毫秒,也就是 36.4 秒,就是我在第一个运行完、截图、改时区等操作花了一会儿而已。
    dcsuibian
        32
    dcsuibian  
       2022-07-14 12:21:46 +08:00
    “有的语言会把时间戳定义为当前时区自 1970-1-1T00:00:00 到现在的毫秒数”
    根本就没有语言这么做,再者说就算这么做了,也跟我时间戳(无论是毫秒还是秒)这个普遍概念没关系了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   6107 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 02:18 · PVG 10:18 · LAX 18:18 · JFK 21:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.