Database time zones java

Маппинг дат

Задача по сохранению Java-объектов в базу данных была актуальна чуть ли не сразу же после создания языка Java. В тот момент в языке Java был только один тип данных – Date, который хранил время по стандарту UNIX-time: как количество миллисекунд, прошедших с 1970 года.

Ну а в базах данных в то время были уже различные типы данных для дат, как минимум были отдельные типы для даты, времени и дата+время:

Поэтому создатели языка Java добавили в него специальный пакет – java.sql, который содержал классы:

Мапить такие классы сплошное удовольствие:

 @Entity public class TemporalValues < @Basic private java.sql.Date sqlDate; @Basic private java.sql.Time sqlTime; @Basic private java.sql.Timestamp sqlTimestamp; > 

Но так как программистам раньше приходилось работать с классом java.util.Date , то в Hibernate добавили специальную аннотацию @Temporal для того, чтобы управлять маппингом типа Date.

 // Если аннотация отсутствует, то в базе будет тип TIMESTAMP Date dateAsTimestamp; @Temporal(TemporalType.DATE) // будет замаплен на тип DATE Date dateAsDate; @Temporal(TemporalType.TIME) // будет замаплен на тип TIME Date dateAsTime; 

Для типа java.util.Calendar и типа java.util.Date по умолчанию используется тип TIMESTAMP для их представления в базе данных.

4.2 Новое время

В нынешнее время с маппингом все обстоит намного проще и лучше. Все базы данных поддерживают 4 типа данных для работы со временем:

  • DATE – дата: год, месяц и день.
  • TIME – время: часы, минуты, секунды.
  • TIMESTAMP – дата, время и наносекунды.
  • TIMESTAMP WITH TIME ZONE – TIMESTAMP и временная зона (имя зоны или смещение).

Для того, чтобы представить тип DATE в Java, нужно использовать класс java.time.LocalDate из JDK 8 DateTime API.

Тип TIME из базы данных можно представить двумя типами из Java: java.time.LocalTime и java.time.OffsetTime . Ничего сложного.

А точную дату и время, представленную типом TIMESTAMP в базе, в Java можно представить 4 типами:

Ну и наконец TIMESTAMP WITH TIME ZONE можно представить двумя типами:

Так как ты уже знаком с DateTime API, то запомнить это дело тебе труда не составит 🙂

Мапить их сплошное удовольствие:

 @Basic private java.time.LocalDate localDate; @Basic private java.time.LocalTime localTime; @Basic private java.time.OffsetTime offsetTime; @Basic private java.time.Instant instant; @Basic private java.time.LocalDateTime localDateTime; @Basic private java.time.OffsetDateTime offsetDateTime; @Basic private java.time.ZonedDateTime zonedDateTime; 

Аннотация @Basic означает, что поле должно быть обработано автоматически: Hibernate сам решит на какую колонку и тип должно быть замаплено данное поле.

4.3 Работа с временными зонами

Если временная зона является частью даты, то хранить их в базе просто – просто как обычную дату:

 @Basic private java.time.OffsetDateTime offsetDateTime; @Basic private java.time.ZonedDateTime zonedDateTime; 

Однако, если ты хочешь хранить временные зоны отдельно от даты:

 @Basic private java.time.TimeZone timeZone; @Basic private java.time.ZoneOffset zonedOffset; 

То Hibernate по умолчанию будет хранить их в типе VARCHAR. Что, собственно, логично, так как TimeZone обычно имеет строковое имя типа «UTC+3» или «Cairo».

Читайте также:  To and fro java

4.4 Установка своей временной зоны

Когда ты будешь работать с сохранением дат в базу данных, то столкнешься с тем, что есть аж 4 места, где можно задать текущую временную зону:

Если в СУБД не указана временная зона (TimeZone), то она возьмет ее из настроек операционной системы. Это может быть неудобно, так как резервные СУБД часто размещают в других датацентрах, у которых своя временная зона.

Поэтому почти у всех СУБД админы задают единую зону, чтобы данные можно было легко переносить с одного сервера на другой.

Похожая ситуация и с Java-приложением. Оно тоже может запускаться на различных серверах в разных датацентрах, поэтому обычно ему задают временную зону явно.

Или во время работы программы:

 TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 

И, конечно, Hibernate позволяет задать свою временную зону явно.

Во-первых, ее можно указать при конфигурировании SessionFactory:

 settings.put( AvailableSettings.JDBC_TIME_ZONE, TimeZone.getTimeZone("UTC") ); 

Во-вторых, временную зону можно указать для конкретной сессии :

 Session session = sessionFactory() .withOptions() .jdbcTimeZone(TimeZone.getTimeZone("UTC")) .openSession(); 

4.5 Аннотация @TimeZoneStorage

Часто бывает ситуация, что программисты начали проектировать базу из расчета работы в одной стране (и одной временной зоне), а затем спустя пару лет им понадобилось добавить поддержку работы в разных временных зонах.

Поэтому они просто добавили в базу отдельную колонку для хранения временной зоны. Это настолько частая ситуация, что Hibernate добавил специальную аннотацию, которая позволяет хранить TimeZone конкретной даты в отдельной колонке.

 @TimeZoneStorage(TimeZoneStorageType.COLUMN) @TimeZoneColumn(name = "birthday_offset_offset") @Column(name = "birthday_offset") private OffsetDateTime offsetDateTimeColumn; @TimeZoneStorage(TimeZoneStorageType.COLUMN) @TimeZoneColumn(name = "birthday_zoned_offset") @Column(name = "birthday_zoned") private ZonedDateTime zonedDateTimeColumn; 

Это костыль. Но есть ему и оправдание: он появился еще во времена, когда DateTime API еще не было. А в классе java.util.Date нельзя было хранить TimeZone.

Очень надеюсь, что ты не часто будешь встречать такое в своем коде.

Источник

The best way to handle time zones in a Java web application

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, I’m going to show you what is the best way to handle time zones when developing a Java web application.

I applied all these best practices while developing RevoGain, a web application that allows you to calculate the gains you realized while trading stocks, commodities, or crypto using Revolut.

What is difficult about time and time zones?

Handling time is very difficult. If you don’t believe me, check out this awesome list of time-related fallacies.

Читайте также:  Java import project class

Now, to make matters worse, a web application request spans on at least three distinct layers:

Each of these layers can observe a different time zone, therefore making things even more difficult for us, software developers.

To simplify the time zone handling process, it’s best practice to store timestamps in UTC (Coordinated Universal Time) and convert the timestamps to local time in the web layer so that users can observe their local time zone.

Database time zone

Databases can use the local time zone of the underlying operating system or a custom define time zone.

For consistency, it’s best if all database timestamps are stored in UTC because, this way, it’s going to be much easier to calculate timestamp intervals since all user timestamps are relative to the same time zone.

More, if a user moves to a different time zone, there’s no need to change the already-stored user=specific date/time information since the conversion can be done in the web layer anyway.

So, if you are using MySQL, you can set the database time zone to UTC in the /etc/mysql/my.cnf configuration file, like this:

Or, if you are using PostgreSQL, you can set the database time zone to UTC in the /var/lib/postgresql/data/postgresql.conf configuration file, as follows:

If you happen to be using Amazon Aurora, then you don’t need to set the UTC time zone because Aurora uses UTC by default.

As I explained in this article, RevoGain uses Amazon Aurora MySQL, so no change was needed in order to use UTC on the database side.

Server time zone

By default, a Java application uses the system time zone. Again, if you are using AWS, then the default time zone is UTC. You can see that when requesting the application logs as the log messages timestamp are relative to UTC.

If your JVM is not using the UTC timezone, but the database is, then you have two options.

Setting the default server time zone

You can set the operating system timezone to UTC, or if you cannot change that, you can set the default JVM timezone.

The JVM time zone can be set using the user.timezone property:

java -Duser.timezone="UTC" com.revogain.RevoGainApplication

Convert to a given timezone using JDBC

If you cannot change the OS or the JVM timezone, you can still convert a Java Date/Time or Timestamp to a specific time zone using the following two JDBC methods:

  • PreparedStatement#setTimestamp(int parameterIndex, Timestamp x, Calendar cal) – to convert the timestamp that goes to the database
  • ResultSet#getTimestamp(int columnIndex, Calendar cal) – to convert the timestamp coming from the database

If you are using Spring Boot, you can achieve this goal by setting the following configuration property in your application.properties file:

spring.jpa.properties.hibernate.jdbc.time_zone=UTC

Behind the scenes, this setting will instruct Hibernate to use the provided time zone when reading and writing timestamp column values.

For more details about the hibernate.jdbc.time_zone configuration property, check out this article.

The browser time zone

We can, therefore, use the user time zone to convert all UTC timestamps stored in the database or created in the JVM. To do that, we need to store the user’s time zone during authentication.

Читайте также:  How to start java app

In the login page, the authentication form contains a timeZoneOffset hidden field:

The timeZoneOffset hidden input field value is set to the user time zone offset:

  

Saving the user-specific time zone on the web server

We can read the timeZoneOffset value using the following web filter:

public class TimeZoneOffsetFilter implements Filter < @Override public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException < TimeZoneOffsetContext.set(request.getParameter("timeZoneOffset")); chain.doFilter(request, response); TimeZoneOffsetContext.reset(); >>

The TimeZoneOffsetFilter can be registered via the HttpSecurity from Spring Security.

http.addFilterBefore( new TimeZoneOffsetFilter(), UsernamePasswordAuthenticationFilter.class )

The TimeZoneOffsetContext is just a placeholder utility that stores the time zone information so that we can read it after Spring Security authenticates the user login request:

public class TimeZoneOffsetContext < private static final ThreadLocaltimeZoneOffsetHolder = new ThreadLocal<>(); public static String get() < return timeZoneOffsetHolder.get(); >public static void set(String timeZoneOffset) < timeZoneOffsetHolder.set(timeZoneOffset); >public static void reset() < timeZoneOffsetHolder.remove(); >>

We can set the user time zone in the Spring Security UserDetails object that’s associated to the currently logged user, like this:

@Service @Transactional(readOnly = true) public class UserService implements UserDetailsService < . @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException < User user = userRepository.findByEmail(username); if (user == null) < throw new UsernameNotFoundException(""" This email or password are invalid. Please review them and try again. """ ); >return new ApplicationUserDetails(user) .setTimeZoneOffset( TimeZoneOffsetContext.get() ); > . >

The ApplicationUserDetails stores the time zone information and provides timestamp formatting capabilities:

public class ApplicationUserDetails implements UserDetails < public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( "dd/MM/uuuu HH:mm:ss" ); private User user; private ZoneOffset zoneOffset; public ApplicationUserDetails(User user) < this.user = user; >. public ZoneOffset getZoneOffset() < return zoneOffset; >public ApplicationUserDetails setTimeZoneOffset(String timeZoneOffset) < if (timeZoneOffset != null) < int offsetMinutes = Integer.valueOf(timeZoneOffset) * -1; this.zoneOffset = ZoneOffset.ofTotalSeconds(offsetMinutes * 60); >return this; > public String getFormattedDateTime(LocalDateTime dateTime) < if(zoneOffset != null) < OffsetDateTime serverOffsetDateTime = dateTime.atZone( ZoneId.systemDefault() ).toOffsetDateTime(); OffsetDateTime clientOffsetDateTime = serverOffsetDateTime .withOffsetSameInstant(zoneOffset); return DATE_TIME_FORMATTER.format(clientOffsetDateTime); >return dateTime.format(DATE_TIME_FORMATTER); > >

Converting timestamps to the user time zone

Now, we can convert timestamps to the user-specific time zone. For instance, when displaying the activity log, we can shift the operation timestamp to the user’s time zone, as follows:

      

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

When dealing with time zones, it’s best to use UTC as much as possible and only convert the timestamp to the current user time zone when rendering the UI.

In a Java web application, we can use the Spring Security UserDetails to store the user time zone and transpose the UTC timestamps fetched from the database or created on the web server to the local time zones of every logged user.

Источник

Оцените статью