Por qué guardar las fechas en UTC en la base de datos

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

Java

PostgreSQL

Es rara la aplicación que trabajado con bases de datos no maneje fechas, quizá es menos habitual aplicaciones que trabajan con fechas y diferentes horarias, esto es haciendo alguna conversión entre zonas horarias. Si se nos presenta el caso de trabajar con fechas y diferentes zonas horarias haremos bien en hacer que las fechas que guardemos en la base de datos estén en la misma zona horaria al menos y convertirla posteriormente a la zona horaria que necesite la aplicación. UTC además de ser una zona horaria neutra evita el problema de que algunas bases de datos o lenguajes de programación para los campos fecha no guarda las zonas horarias con lo que puede ocurrirnos que guardemos la fecha en una zona horaria y la recuperemos en otra produciendo posiblemente incoherencias en las fechas por una hora.

¿Por qué elegir UTC?

Principalmente porque es una zona horaria neutra, universal y que elimina ambigüedades ya que que no tiene DST o horario de verano y podremos guardar las fechas sin temor a que al recuperarlas estén en otra zona horaria si la base de datos o el lenguaje de programación para guardarlas no las soporta.

Otros motivos que se mencionan en un comentario en inglés Always store dates/times in UTC (in the database) y algún otro en DateTime values should always be stored in UTC son que:

  • Calcular duraciones de tiempo es simple. El periodo de tiempo entre la 2:30 AM UTC y las 3:30 AM UTC es siempre una hora cosa que no ocurre en las horas que hay cambio de horario pudiendo ser el periodo entre cero y dos horas.
  • No hay fechas inválidas cuando se adelanta la hora por ejemplo de las 2:00 AM a las 3:00 AM, pudiendo ser que las 2:30 AM en esa zona horaria no exista.
  • Se evitan problemas al ordenar o agrupar fechas pudiendo ser el caso de que una fecha con tiempo 2:59 AM sea antes que las 2:01 AM por causa del cambio horario.
  • Los cambios horarios están sujetos a cambios nada predecibles y varían a lo largo del tiempo con relativa frecuencia con lo para calcular de forma fiable cuantas horas hay entre dos fechas se necesita guardar las variaciones históricas de DST. Ni las fechas de cambios DST son constantes ni las zonas horarias se mantienen fijas para las localizaciones.

Una vez recuperada la fecha en UTC podemos convertir de diferentes formas una fecha de una zona horaria a otra en Java y en cualquier otro lenguaje con las facilidades que proporcione según la zona horaria a visualizar la fecha.

¿Cúal es el caso que puede dar problemas?

Uno en el que la hora a guardar coincida con un cambio de hora de la zona horaria en la que guardemos las fechas. Por ejemplo, en España el año 2016 el cambio de horario de verano (DST/CEST) a horario de invierno (CET) se hará el 30 de octubre momento en el que a las 3:00 (CEST) volverán a ser las 02:00 pero con diferente zona horaria (CET).

Ejemplo

Supongamos que tenemos la fecha 30 de octubre a las 02:30 CEST y la guardamos en la base de datos pero sin la zona horaria pasando a estar implícita. En esta fecha y hora se produce un cambio horario de horario de verano a horario de invierno en España. Al recuperar la fecha será 30 de octubre a las 02:30 CET, la diferencia está entre el CEST y CET o la diferencia horaria +02:00 y +01:00. Una hora de diferencia entre la original y la que recuperamos de la base de datos después de hacer la conversión.

Esto puede probarse con el siguiente ejemplo de código de un programa Java que guarda y recupera de una base de datos PostgreSQL una fecha que está en el intervalo de cambio horario. En el ejemplo utilizaré Docker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package io.github.picodotdev.blogbitix.datetimedatabase;

import java.sql.*;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class Main {

    public static void main(String[] args) throws Exception {
        Class.forName("org.postgresql.Driver");
        Connection connection = DriverManager.getConnection("jdbc:postgresql://localhost/user", "user", "user");

        {
            PreparedStatement create = connection.prepareStatement("CREATE TABLE IF NOT EXISTS date (id SERIAL, date TIMESTAMP, PRIMARY KEY(id))");
            create.execute();

            PreparedStatement delete = connection.prepareStatement("DELETE FROM date");
            delete.execute();
        }

        {
            PreparedStatement insert = connection.prepareStatement("INSERT INTO date (date) VALUES (?)");
            ZonedDateTime date = ZonedDateTime.of(2016, 10, 30, 2, 30, 0, 0, ZoneId.of("Europe/Madrid"));
            System.out.printf("Before database: %s\n", date.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));

            insert.setTimestamp(1, Timestamp.valueOf(date.toLocalDateTime())); // !! timezone lost
            insert.execute();
        }

        {
            PreparedStatement select = connection.prepareStatement("SELECT date FROM date");
            ResultSet rs = select.executeQuery();
            rs.next();
            ZonedDateTime date = ZonedDateTime.ofInstant(rs.getTimestamp("date").toInstant(), ZoneId.of("Europe/Madrid"));
            System.out.printf("After database: %s\n", date.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
        }
    }
}
Main.java
1
2
Before database: 2016-10-30T02:30:00+02:00[Europe/Madrid]
After database: 2016-10-30T02:30:00+01:00[Europe/Madrid]
System.out
1
2
3
4
5
6
7
8
postgres:
    image: postgres
    ports:
        - "5432:5432"
    environment:
        - POSTGRES_USER=user
        - POSTGRES_PASSWORD=user
        - POSTGRES_DB=user
docker-compose.yml

Trabajar con fechas no es simple, es muy curioso y no debemos hacer suposiciones sobre las fechas que son incorrectas en las aplicaciones.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
cd misc/docker/postgresql/, docker-compose up, ./gradlew run


Comparte el artículo: