La gestión de fechas y horas es un aspecto fundamental en el desarrollo de aplicaciones modernas, especialmente en sistemas que dependen de la sincronización, registro de eventos o el manejo de tiempo en distintas zonas horarias.

SQLAlchemy, como ORM poderoso en Python, ofrece herramientas y funcionalidades para trabajar eficientemente con datos temporales en bases de datos relacionales.

En este artículo, exploraremos cómo manejar correctamente fechas y horas en SQLAlchemy, desde la definición de modelos hasta las mejores prácticas para evitar errores comunes.

Tabla de contenidos

¿Por Qué Es Importante la Correcta Gestión de Fechas en SQLAlchemy?

Al desarrollar una API, las fechas juegan un papel crítico, especialmente en escenarios donde los datos temporales (como citas, eventos o registros) son fundamentales.

Una mala gestión de fechas puede ocasionar:

  • Inconsistencias en zonas horarias.
  • Errores al comparar fechas debido a formatos incorrectos.
  • Pérdida de precisión en operaciones temporales.

SQLAlchemy proporciona tipos de datos especializados y métodos para simplificar estas tareas.

Tipos de Datos para Fechas y Horas en SQLAlchemy

SQLAlchemy ofrece los siguientes tipos para trabajar con datos temporales:

1. Date

Representa una fecha sin información de hora. Utilizado para campos como cumpleaños o fechas específicas.

Python
import datetime
from sqlalchemy import Date
from sqlalchemy.orm import Mapped, mapped_column
# ...demás código

class User(Base):
    __tablename__ = 'users'
    id: Mapped[int] = mapped_column(primary_key=True)
    birth_date: Mapped[datetime.date] = mapped_column(Date())

2. Time

Representa solo la hora del día, útil para horarios recurrentes o campos de tiempo sin fecha asociada.

Python
import datetime
from sqlalchemy import Time
from sqlalchemy.orm import Mapped, mapped_column
# ...demás código

class Schedule(Base):
    __tablename__ = 'schedules'
    # ... otros campos
    start_time: Mapped[datetime.time] = mapped_column(Time())

3. DateTime

Almacena una fecha completa con hora, incluyendo opcionalmente información de zona horaria.

Python
import datetime
from sqlalchemy import DateTime
from sqlalchemy.orm import Mapped, mapped_column
# ...demás código

class Appointment(Base):
    __tablename__ = 'appointments'
    # ... otros campos
    start_datetime: Mapped[datetime.datetime] = mapped_column(
        DateTime(timezone=True))
    end_datetime: Mapped[datetime.datetime] = mapped_column(
        DateTime(timezone=True))

4. Interval

Representa un intervalo de tiempo. Útil para calcular duraciones.

Python
import datetime
from sqlalchemy import Interval
from sqlalchemy.orm import Mapped, mapped_column
# ...demás código

class Task(Base):
    __tablename__ = 'tasks'
    # ... otros campos
    duration: Mapped[datetime.timedelta] = mapped_column(Interval())

Casos de Uso Comunes en Modelos

1. Campos de Auditoría

Los campos created_at y updated_at son estándar para rastrear cambios en registros.

Python
import datetime
from sqlalchemy import DateTime
from sqlalchemy.orm import Mapped, mapped_column
# ...demás código

class BaseModel(Base):
    __abstract__ = True
    created_at: Mapped[datetime.datetime] = mapped_column(
        DateTime(timezone=True), default=datetime.datetime.now)
    updated_at: Mapped[datetime.datetime] = mapped_column(
        DateTime(timezone=True), default=datetime.datetime.now, onupdate=datetime.datetime.now)

2. Registro de Eventos

Para sistemas que requieren rastrear actividades, calendarios, etc.

Python
import datetime
from sqlalchemy import DateTime
from sqlalchemy.orm import Mapped, mapped_column
# ...demás código

class Event(Base):
    __tablename__ = 'events'
    id: Mapped[int] = mapped_column(primary_key=True)
    event_datetime: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True))

3. Gestión de Citas

Con start_datetime y end_datetime se puede manejar reservas y eventos.

Python
import datetime
from sqlalchemy import DateTime
from sqlalchemy.orm import Mapped, mapped_column
# ...demás código

class Appointment(Base):
    __tablename__ = 'appointments'
    id: Mapped[int] = mapped_column(primary_key=True)
    start_datetime: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True))
    end_datetime: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True))

Ejemplos de Consultas con Campos Temporales

Los gestores de bases de datos están optimizados para operaciones como agrupado, ordenado y filtrado, utilizando índices, algoritmos eficientes y paralelismo interno.

SQLAlchemy nos facilita el acceso a toda la potencia y eficiencia de las consultas SQL con métodos y funciones especiales. Aquí te comparto algunos ejemplos:

1. Consultar Registros de Hoy

Si necesitas filtrar los registros correspondientes al día actual:

Python
from datetime import date
# ...demás código

today = date.today()

query = session.query(Event).filter(
    Event.event_datetime.date() == today
)

2. Filtrar Registros de los Últimos 7 Días

Para obtener registros recientes en un rango específico:

Python
from datetime import UTC, datetime, timedelta
# ...demás código

seven_days_ago = datetime.now(UTC) - timedelta(days=7)

query = session.query(Event).filter(
    Event.event_datetime.date() >= seven_days_ago
)

3. Filtrar Registros por Hora Exacta

Para encontrar registros que coincidan con una hora específica:

Python
from datetime import time
# ...demás código

specific_time = time(14, 0)  # 2:00 PM

query = session.query(Schedule).filter(
    Schedule.start_time == specific_time
)

4. Buscar Registros en el Pasado

Si deseas encontrar todos los registros con fecha y hora anteriores al momento actual:

Python
from datetime import datetime, UTC
# ...demás código

now = datetime.now(UTC)

query = session.query(Appointment).filter(
    Appointment.start_datetime < now
)

5. Buscar Registros en el Futuro

Similar al anterior, pero para registros con fecha y hora posteriores al momento actual:

Python
from datetime import datetime, UTC
# ...demás código

now = datetime.now(UTC)

query = session.query(Appointment).filter(
    Appointment.start_datetime > now
)

6. Filtrar Registros Dentro de un Mes

Encuentra registros pertenecientes a un mes específico:

Python
from sqlalchemy import extract
# ...demás código

query = session.query(Event).filter(
    extract('month', Event.event_datetime) == 12  # Diciembre
)

7. Consultar Registros Dentro de un Año Específico

De manera similar, puedes filtrar registros dentro de un año específico:

Python
from sqlalchemy import extract
# ...demás código

query = session.query(Event).filter(
    extract('year', Event.event_datetime) == 2024
)

8. Encontrar Registros Que Cruzan un Intervalo de Tiempo

Para consultas más avanzadas, como encontrar citas que cruzan un rango de tiempo:

Python
from datetime import UTC, datetime
from sqlalchemy import and_, or_
# ...demás código

interval_start = datetime.fromisoformat("2024-12-08T03:00:00-03:00")
interval_end = datetime.fromisoformat("2024-12-08T05:00:00-03:00")
interval_start_utc = interval_start.astimezone(UTC)
interval_end_utc = interval_end.astimezone(UTC)

# Consulta appointments que abarcan completamente el intervalo especificado
query1 = session.query(Appointment).filter(
    and_(
        Appointment.start_datetime <= interval_start_utc,
        Appointment.end_datetime >= interval_end_utc
    )
)

# Consulta appointments que se solapan total o parcialmente con el intervalo especificado
query2 = session.query(Appointment).filter(
    or_(
        and_(
            Appointment.start_datetime <= interval_start_utc, 
            Appointment.end_datetime > interval_start_utc
        ),
        and_(
            Appointment.start_datetime < interval_end_utc, 
            Appointment.end_datetime > interval_start_utc
        )
    )
)

9. Agrupar Registros por Día, Mes, Año

Si necesitas agrupar y contar registros, hacerlo en la base de datos reduce el volumen de datos transferidos entre la base de datos y el cliente Python, ya que solo se devuelven los datos procesados.

Utilizar label() en las columnas permite acceder a los resultados de manera más clara en el código Python al procesar los datos obtenidos.

Agrupar por Día

Generar un resultado donde cada fila corresponde a un día específico y su respectivo conteo de registros:

Python
from sqlalchemy import func
# ...demás código

query = session.query(
    func.date(Appointment.start_datetime).label("day"), 
    func.count(Appointment.id).label("appointments_count")
).group_by(
    func.date((Appointment.start_datetime))
)

for row in query.all():
    print(f"Día: {row.day}, Citas: {row.appointments_count}")

Agrupar por Año

Python
from sqlalchemy import extract, func
# ...demás código

query = session.query(
    extract('year', Appointment.start_datetime).label('year'),  # Extrae el año
    func.count(Appointment.id).label('appointments_count')  # Cuenta las citas
).group_by(
    extract('year', Appointment.start_datetime)  # Agrupa por año
).order_by('year')  # Opcional: ordenar por el año

for row in query.all():
    print(f"Año: {row.year}, Citas: {row.appointments_count}")

Agrupar por Mes y Año

Si deseas agrupar tanto por año como por mes (por ejemplo, contar citas por mes dentro de cada año):

Python
from sqlalchemy import extract, func
# ...demás código

query = session.query(
    extract('year', Appointment.start_datetime).label('year'),  # Extrae el año
    extract('month', Appointment.start_datetime).label('month'),  # Extrae el mes
    func.count(Appointment.id).label('appointments_count')  # Cuenta las citas
).group_by(
    extract('year', Appointment.start_datetime),  # Agrupa por año
    extract('month', Appointment.start_datetime)  # Agrupa por mes
).order_by('year', 'month')  # Opcional: ordenar por año y mes

for row in query.all():
    print(f'Año: {row.year}, Mes: {row.month}, Citas: {row.appointments_count}')
    

10. Filtrar por Duración Usando Interval

Si trabajas con campos de tipo Interval, puedes filtrar por duraciones específicas:

Python
from datetime import timedelta
# ...demás código

query = session.query(Task).filter(
    Task.duration > timedelta(hours=2)  # Duración mayor a 2 horas
)

11. Consultar el Registro Más Reciente o Más Antiguo

Para obtener el registro con la fecha más reciente:

Python
query = session.query(Appointment).order_by(Appointment.start_datetime.desc()).first()

Y para la fecha más antigua:

Python
query = session.query(Appointment).order_by(Appointment.start_datetime.asc()).first()

12. Combinar Fechas y Hora en Consultas

Si solo tienes una fecha y necesitas incluir una hora específica para filtrado:

Python
from datetime import datetime
# ...demás código

specific_datetime = datetime(2024, 12, 25, 10, 0)  # 25 de diciembre a las 10:00 AM

query = session.query(Appointment).filter(
    Appointment.start_datetime == specific_datetime
)

13. Comparar Duraciones Entre Columnas

Si tu modelo incluye varios campos de tipo Interval y necesitas compararlos:

Python
query = session.query(Task).filter(
    Task.duration > Task.estimated_duration
)

14. Consultar Fechas Relativas Usando Funciones SQL

SQLAlchemy permite usar funciones nativas de SQL para cálculos avanzados:

Python
from sqlalchemy import func
# ...demás código

query = session.query(Event).filter(
    Event.event_date > func.current_date()  # Eventos después de hoy
)

Buenas Prácticas al Manejar Fechas en SQLAlchemy

  1. Almacena siempre las fechas en UTC.
    Esto facilita la interoperabilidad en sistemas distribuidos y evita problemas de conversión de zonas horarias.
  2. Valida las fechas antes de almacenarlas.
    Usa middlewares o validaciones personalizadas para asegurarte de que las fechas cumplan las reglas de negocio.
  3. Utiliza índices en columnas de fecha.
    Si planeas realizar consultas frecuentes basadas en fechas, añade índices para optimizar el rendimiento.
  4. Evita manejar lógica de zonas horarias directamente en SQL.
    Haz las conversiones necesarias en tu aplicación para mantener la consistencia.

Si te interesa profundizar en este tema, aquí en el blog puedes encontrar este artículo sobre buenas prácticas en la gestión de fechas al desarrollar una API RESTful. 🤓


Conclusión

La gestión de fechas en SQLAlchemy es una habilidad crucial para desarrolladores web.

Al comprender los tipos de datos, métodos de consulta y mejores prácticas, puedes garantizar que tus aplicaciones manejen fechas y horas de manera precisa y eficiente.

Ya sea que estés desarrollando una API de citas o un sistema de auditoría, SQLAlchemy ofrece las herramientas necesarias para trabajar con datos temporales de forma robusta y escalable.

Etiquetado en: