В своих приложениях Java (с Spring Boot и Spring Data JPA) я обычно использую Instant. С другой стороны, я хотел бы использовать наиболее подходящий тип данных для значений времени.
Не могли бы вы разъяснить мне эти вопросы? Какой тип данных я должен предпочесть для хранения даты и времени, когда:
1. Хранить время именно как отметку времени (я не уверен, что Instant - лучший вариант)?
2. Для обычных случаев, когда мне просто нужны дата и время (насколько я знаю, старая библиотека устарела, но не уверен, какую библиотеку мне использовать).
Я также рассматриваю TimeZone, но не уверен, что использование LocalDateTime с UTC решит мою проблему.
Любая помощь будет оценена по достоинству.
"старая библиотека" — какая библиотека?
«DateTime» — что это за класс? Нет такого класса в комплекте с Java.
«Я хотел бы использовать наиболее подходящий тип данных» — с какой целью? Определить правильный выбор означает соответствие некоторым требованиям или параметрам, продиктованным вашей целью/задачей.
«Для обычных случаев» — что такое «нормальный случай»?
«Сохранять время точно как отметку времени» — что вы подразумеваете под «точно»? Что вы подразумеваете под "отметкой времени"?
Предположим, нам нужно охватить весь диапазон дат и времени. Если есть определенная проблема, которой у вас нет, это либо сворачивает различные типы в «ну, тогда они взаимозаменяемы», либо просто означает, что вам не нужно использовать определенную часть API. Дело в том, что вам нужно понимать, что представляют собой эти типы, и как только вы это узнаете, вы знаете, какой из них применить. Потому что, даже если различные типы java.time технически делают то, что вы хотите, код будет более гибким и намного проще для чтения, если используемые вами типы представляют то, что вы хотите. По той же причине String[] student = new String[] {"Joe McPringle", "56"};, возможно, механически представляет имя и возраст ученика, но все намного проще, если вы напишете class Student { String name; int age; } и используете его вместо этого.
Представьте, что вы хотите проснуться в 07:00 утра. Не потому, что у вас назначена встреча, вам просто нравится вставать рано.
Итак, вы устанавливаете будильник на 07:00 утра, ложитесь спать, и ваш будильник сразу же срабатывает в 7. Пока все хорошо. Однако затем вы садитесь в самолет и летите из Амстердама в Нью-Йорк. (в Нью-Йорке на 6 часов раньше). Затем вы снова ложитесь спать. Должен ли будильник срабатывать в 01:00 ночи или в 07:00 утра?
Оба ответа правильные. Вопрос в том, как вы «храните» этот сигнал тревоги, и чтобы ответить на этот вопрос, вам нужно выяснить, что сигнал тревоги пытается представлять.
Если намерение «07:00, где бы я ни находился в то время, когда должен сработать будильник», правильным механизмом хранения данных является java.time.LocalDateTime, который хранит время в человеческом выражении (годы, месяцы, дни, часы, минуты, и секунд), а не в компьютерных терминах (мы вернемся к этому позже), и вообще не включает часовой пояс. Если будильник должен срабатывать каждый день, то вам это тоже не нужно, так как LDT хранит дату и время, отсюда и название, вместо этого вы должны использовать LocalTime.
Это потому, что вы хотели сохранить концепцию «будильник должен сработать в 7 часов» и ничего более. У вас не было намерения сказать: «Будильник должен сработать, когда люди в Амстердаме согласятся, что сейчас 07:00», и у вас не было намерения сказать: «Когда Вселенная прибудет в этот точный момент времени, подайте звуковой сигнал». тревога". У вас было намерение сказать: «Когда будет 07:00, где бы вы сейчас ни находились, бейте тревогу», так что сохраните это, то есть LocalTime.
Тот же принцип применим к LocalDate: он хранит кортеж год/месяц/день без понятия, где.
Это приводит к некоторым, возможно, шатким выводам: учитывая объект LocalDateTime, невозможно спросить, сколько времени пройдет, пока не прибудет этот LDT. Также невозможно сравнить любой данный момент времени с LDT, потому что эти вещи — яблоки и апельсины. Понятие «18 февраля 2023 года, ровно 7 утра» не является единственным временем. В конце концов, в Нью-Йорке этот «момент» наступает на целых 6 часов раньше, чем в Амстердаме. Вы можете сравнить только 2 LocalDateTimes.
Вместо этого вам придется сначала «поместить» свой LDT где-нибудь, преобразовав его в один из других типов (ZonedDateTime или даже Instant), запросив API java.time: «Хорошо, я хочу, чтобы этот конкретный LDT находился в определенном часовом поясе.
Следовательно, если вы пишете свое приложение для будильника, вам нужно будет взять сохраненный будильник (объект LocalTime), преобразовать его в Instant (именно так работает природа «сколько сейчас времени, т.е. System.currentTimeMillis()»), говоря: это LocalTime, в текущий день в текущем местном часовом поясе, как мгновение, и ТОГДА сравнивая эти два результата.
Представьте, что незадолго до вылета в Нью-Йорк вы записались на прием к своему местному (в Амстердаме) парикмахеру. Их повестка дня была довольно насыщенной, поэтому встреча была назначена на 20 июня 2025 года, в 11:00.
Если вы останетесь в Нью-Йорке на несколько лет, правильное время для вашего календаря, чтобы напомнить вам, что через час у вас назначена встреча с парикмахером, точно не 10:00 20 июня 2025 года в Нью-Йорке. Вы бы пропустили встречу к тому времени. Вместо этого ваш телефон должен чирикать вам, что у вас остался час, чтобы добраться до своего парикмахера (немного сложно, конечно, из Нью-Йорка) в 04:00 посреди ночи.
Звучит так, будто мы можем сказать, что встреча парикмахера — это конкретный момент времени. Однако это неверно. ЕС уже принял закон, согласованный всеми государствами-членами, согласно которому все страны ЕС должны отменить летнее время. Однако в этом законе не указывается крайний срок и, что особенно важно, не указывается часовой пояс, который необходимо выбрать каждому государству-члену ЕС. Поэтому в какой-то момент Нидерланды собираются изменить часовой пояс. Скорее всего, они предпочтут придерживаться либо постоянного летнего времени (в этом случае они будут постоянно находиться в формате UTC+2, по сравнению с их текущей ситуацией, когда они находятся в формате UTC+2 летом и в формате UTC+1 зимой, при этом, в частности, разные даты, когда произойдет переключение по сравнению с Нью-Йорком!), или остаться на зимнее время, то есть UTC+1 навсегда.
Допустим, они выбрали зимнее время навсегда.
В тот день, когда в здании голландского парламента ударит молоток, закрепляющий закон о том, что голландцы больше не будут переводить стрелки часов в марте, это день, когда ваша встреча сдвинется на один час. В конце концов, ваш парикмахер не собирается залезать в свою книгу встреч и сдвигать все встречи на час. Нет, ваша встреча останется на 20 июня 2025 года в 11:00. Если у вас есть бегущие часы, отсчитывающие секунды до встречи с парикмахером, когда этот молоток опустится, он должен подскочить на 3600 секунд.
Это противоречит сути: эта встреча с парикмахером действительно не является единичным моментом во времени. Это человеческое/политическое соглашение о том, что ваша встреча назначена на то время, когда, по общему мнению Амстердама, это 20 июня 2025 года, 11:00 – и кто знает, когда этот момент на самом деле наступит; это зависит от политического выбора.
Таким образом, вы не можете «решить» это, сохранив момент времени, и это показывает, что понятия «момент времени» и «год/месяц/день час:минута:секунда в определенном часовом поясе» не совсем взаимозаменяемы.
Правильный тип данных для этой концепции — ZonedDateTime. Это представляет дату и время в человеческом выражении: год/месяц/день час:секунда:минута и часовой пояс. Он не сокращается, сохраняя момент времени в epochmillis или что-то в этом роде. Если молоток упадет, и ваш JDK обновит свои определения часовых поясов, вопрос «сколько секунд до моей встречи» будет правильно смещен на 3600 секунд, что вы и хотите.
Поскольку это для встреч, и нет смысла хранить только время встречи, но не дату, нет таких вещей, как ZonedDate или ZonedTime. В отличие от первого, который представлен в трех вариантах (LocalDateTime, LocalDate и LocalTime), здесь только ZonedDateTime.
Представьте, что вы пишете компьютерную систему, которая регистрирует произошедшее событие.
Это событие, естественно, имеет связанную с ним временную метку. Оказывается, из-за серьезных политических потрясений законы страны решили, что ретроспективно страна находилась в другом часовом поясе, чем вы думали, когда произошло событие. Применение той же логики, что и в случае с парикмахером (где фактический момент времени подскакивает на 3600 секунд, когда опускается молоток), неверно. Временная метка представляет собой момент времени, когда что-то произошло, а не встречу в бухгалтерской книге. Он не должен прыгать на 3600.
Часовой пояс здесь действительно не имеет значения. Смысл хранения «отметки времени» для события журнала заключается в том, чтобы вы знали, когда оно произошло, неважно, где оно произошло (а если и произошло, то это принципиально отдельное понятие).
Правильный тип данных для этого — java.time.Instant. Мгновение вообще не знает о часовых поясах и не является человеческим понятием. Это «компьютерное время» - хранится в миллисекундах с согласованной эпохи (полночь, UTC, 1970, новые годы), здесь нет необходимости в информации о часовом поясе. Естественно, нет вариантов только для времени или только для даты, эта штука даже толком не знает, что такое "дата" - какое-то причудливое человеческое понятие, к которому компьютерное время ни в малейшей степени не относится.
Вы можете легко перейти от ZonedDateTime к Instant. Есть метод без аргументов, который делает это. Но обратите внимание:
Результаты в разных результатах: 2 момента не будут одинаковыми, но ZDT одинаковы. ZDT представляет строку назначения в книге парикмахера (которая никогда не менялась - 20 июня 2025 года, 11:00), момент представляет момент времени, когда вы должны появиться, что действительно изменилось.
Если вы сохраните встречу с парикмахером как объект java.time.Instant, вы опоздаете на встречу с парикмахером на час. Вот почему важно хранить вещи такими, какие они есть. Запись к парикмахеру — это ZonedDateTime. хранить его как что-либо еще было бы неправильно.
Преобразования редко бывают по-настоящему простыми. Не существует единого способа преобразовать одну вещь в другую — вам нужно подумать о том, что представляют собой эти вещи, что подразумевает преобразование, а затем последовать их примеру.
Пример: вы пишете систему ведения журнала. Внутренние части хранят события журнала в какой-либо базе данных, а внешние части читают эту базу данных и показывают события журнала пользователю-администратору для просмотра. Поскольку пользователь-администратор — человек, он хочет видеть его в терминах, которые он понимает, скажем, время и дату в соответствии с UTC (это программист, им нравятся такие вещи).
Хранилище системы ведения журналов должно хранить концепцию Instant: Эпоха миллис и без часового пояса, потому что это не имеет значения.
Внешний интерфейс должен читать их как Instant (это всегда плохая идея делать тихие преобразования!) - затем подумайте, как отобразить это для пользователя, выясните, что пользователь хочет, чтобы они были локальными в UTC, и, таким образом, вы бы тогда на лету, для каждого события, которое будет напечатано на экране, преобразуйте Instant в ZonedDateTime в зоне, которую хочет пользователь, а оттуда в LocalDateTime, которую вы затем визуализируете (потому что пользователь, вероятно, не хочет видеть UTC в каждой строке , их экранное пространство ограничено).
Было бы неправильно хранить метки времени как UTC ZonedDateTimes, и еще более неправильно хранить их как LocalDateTimes, полученные путем запроса текущего LocalDT в UTC, когда происходит событие, а затем сохраняя его. Механически все эти вещи будут работать, но все типы данных неверны. А это усложнит дело. Представьте, что пользователь действительно хочет увидеть событие журнала по европейскому/амстердамскому времени.
Мир сложнее, чем горстка часовых поясов. Например, почти вся континентальная Европа в настоящее время использует «CET» (центральноевропейское время), но некоторые думают, что это относится к европейскому зимнему времени (UTC+1), что-то, что относится к текущему состоянию в Центральной Европе: UTC+1 в зимой, UTC+2 летом. (Есть также CEST, центральноевропейское летнее время, что означает UTC + 2 и не является двусмысленным). Когда страны ЕС начнут применять новый закон, чтобы избавиться от летнего времени, это, вероятно, например. В Нидерландах на западном краю зоны CET используется другое время, чем в Польше на восточном краю. Следовательно, «вся центральная Европа» слишком широка. Трехбуквенные аббревиатуры также отнюдь не уникальны. Различные страны используют «EST» для обозначения «восточного стандартного времени», например, не только в восточной части США.
Следовательно, единственный правильный способ представления названий часовых поясов — это использование таких строк, как Europe/Amsterdam или Asia/Singapore. Если вам нужно визуализировать их как 09:00 PST для жителей западного побережья США, это проблема рендеринга, поэтому напишите метод рендеринга, который превращает America/Los_Angeles в PST, что является проблемой локализации и не имеет ничего общего со временем.
Большое спасибо за хорошие объяснения, проголосовал. Рассматривая приложения Spring Boot с базами данных, например. PostgreSQL и MySQL, можно ли так сказать? >>>
1. Я мог бы использовать LocalDateTime или Instant, когда мне не нужно сохранять часовой пояс, и приложение будет использоваться только в одном и том же месте?
2. Когда нам нужно также сохранить часовой пояс, следует ли использовать ZonedDateTime?
3. Насколько я понимаю, Instant можно использовать в качестве метки времени вместо LocalDateTime, верно?
@batman Использование LocalDateTime для отметки времени журнала было бы ужасной идеей. Instant — единственно правильный тип для такого дела.
Дело в том, что здесь нет правильного ответа, потому что это зависит от того, что означают данные, которые вы пытаетесь сохранить. Это время, когда произошло событие? Instant идеально подходит для временной метки сообщения журнала. Это назначение в офисе терапевта? ZonedDateTime; LocalDateTime является гранично приемлемым, если система принципиально не предназначена для выхода за пределы страны с одним часовым поясом.
Затем «как LocalDateTime, LocalTime, LocalDate, ZonedDateTime и Instant сопоставляются с базами данных» — это очень сложная проблема. Одна из основных проблем заключается в том, что JDBC API понимает это неправильно, но все современные механизмы БД (на самом деле, если память не изменяет, требуется спецификацией jDBC v5) разрешают, например, .getObject(col, LocalDateTime.class), поэтому вы можете избежать неработающего .getDate(col).
Таким образом, попытка ответить на вопрос «что делать весной» требует знания [A] механизма БД и того, что они думают о типах даты/времени, [B] драйвера JDBC и того, делает ли он это правильно или искажает (пример , H2 все портит - люди в Европе/Амстердаме, хранящие даты рождения на современных JVM, облажались, исходя из опыта здесь), [C] как Spring переводит материал DB в JDBC. Я не знаю [C], я едва знаю A/B специально для postgres.
Конечно, выбор наиболее подходящего зависит от требований, но я просто хотел бы знать примеры сценариев, когда я решу выбрать один из них. Основываясь на ваших последних комментариях, можем ли мы сказать, что часовой пояс не важен, тогда мы можем использовать либо LocaldateTime, либо Instant (для более точных вещей, например, ведения журнала). А когда речь идет о зоне, то мы можем использовать ZonedDateTime, верно?
Нет - вам нужно выбрать тип, который правильно представляет то, что у вас есть. Если вы храните встречи парикмахеров с точки зрения j.t.Instant, ваш код неверен, даже если сегодня он отлично работает. Изменения часового пояса запутают вас, и ваш код будет трудно читать. Следовательно, «но будет ли это технически работать» — неуместный вопрос, и ваша настойчивость в ответе, похоже, указывает на то, что вы не понимаете сути этого вопроса. Каждое событие, связанное со временем, имеет ОДИН правильный тип данных из 5 (LDT/LD/LT/ZDT/Instant).
Извините, я не учел разницу во времени в одном и том же месте. В этой сцене, какие типы даты и времени я должен выбрать в общих ситуациях, например. при ведении журналов, при учете или не учете зоны/летнего времени для одной и той же локации?
Отметки времени журнала? Instant. Назначения (скажем, сохранение «этот врач написал эту заметку об этом пациенте, когда они увидели его в своем кабинете в момент времени Y») — ZonedDateTime. Дни рождения? Местная Дата.
Большое спасибо, последний комментарий тоже очень полезен
Ответ от rzwitserloot правильный и мудрый. Кроме того, вот краткое описание различных типов. Для получения дополнительной информации см. мой ответ на аналогичный вопрос.
Чтобы сохранить время именно как временную метку (я не уверен, что Instant - лучший вариант)?
Если вы хотите отслеживать момент, конкретную точку на временной шкале:
Instant
OffsetDateTime
ZonedDateTime
Если вы хотите отслеживать только дату и время суток без контекста смещения или часового пояса, используйте LocalDateTime. Этот класс не представляет момент, не является точкой на временной шкале.
Для обычных случаев, когда мне просто нужны дата и время
Если вы абсолютно уверены, что вам нужна только дата со временем суток, но вам не нужен контекст смещения или часового пояса, используйте LocalDateTime.
используя LocalDateTime с UTC
Это противоречие и не имеет смысла. Класс LocalDateTime не имеет концепции UTC, а также концепции смещения от UTC или часового пояса.
Спринг-данные JPA
Спецификация JDBC 4.2+ сопоставляет стандартные типы данных SQL с классами Java.
TIMESTAMP WITH TIME ZONE
столбцы сопоставляются с OffsetDateTime
в Java.TIMESTAMP WITHOUT TIME ZONE
столбцы сопоставляются с LocalDateTime
в Java.DATE
столбцы сопоставляются с LocalDate
.TIME WITHOUT TIME ZONE
столбцы сопоставляются с LocalTime
.Стандарт SQL также упоминает TIME WITH TIME ZONE, но этот тип не имеет смысла (только подумайте об этом!). Насколько мне известно, комитет по SQL никогда не объяснял, что они имели в виду. Если вы должны использовать этот тип, Java определяет соответствующий класс ZoneOffset.
Обратите внимание, что JDBC не сопоставляет никакие типы SQL ни с Instant, ни с ZonedDateTime. Вы можете легко конвертировать в/из сопоставленного типа OffsetDateTime.
Instant instant = myOffsetDateTime.toInstant() ;
OffsetDateTime myOffsetDateTime = instant.atOffset( ZoneOffset.UTC ) ;
… и:
ZonedDateTime zdt = myOffsetDateTime.atZoneSameInstant( myZoneId ) ;
OffsetDateTime odt = zdt.toOffsetDateTime() ; // The offset in use at that moment in that zone.
OffsetDateTime odt = zdt.toInstant().atOffset( ZoneOffset.UTC ) ; // Offset of zero hours-minutes-seconds from UTC.
Я также рассматриваю TimeZone
Класс TimeZone является частью ужасных устаревших классов даты и времени, которые много лет назад были вытеснены современными классами java.time. Заменено на ZoneId и ZoneOffset.
Большое спасибо за хорошие объяснения, проголосовал. Рассматривая приложения Spring Boot с базами данных, например. PostgreSQL и MySQL, можно ли так сказать? >>>
1. Я мог бы использовать LocalDateTime или Instant, когда мне не нужно сохранять часовой пояс, и приложение будет использоваться только в одном и том же месте?
2. Когда нам нужно также сохранить часовой пояс, следует ли использовать ZonedDateTime?
3. Насколько я понимаю, Instant можно использовать в качестве метки времени вместо LocalDateTime, верно?
Взгляните на обсуждения в В чем разница между Instant и LocalDateTime? Ответы включают обзоры концепций, примеры кода Java и рекомендации по использованию. Это может помочь.