Flask

Flask

Tutorial Flask: Tipos de datos en modelos

SQLAlchemy y definición de columnas en modelos Flask: Aprende a crear estructuras de bases de datos eficientes con columnas, restricciones y tipos de datos avanzados.

Aprende Flask GRATIS y certifícate

Definición de columnas y campos básicos en modelos

En Flask, SQLAlchemy es la herramienta preferida para interactuar con bases de datos de forma eficiente y legible. Para definir un modelo que represente una tabla en la base de datos, se crea una clase que hereda de db.Model. Cada atributo de esta clase será una columna en la tabla correspondiente.

A continuación, se muestra cómo definir un modelo básico llamado Usuario:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Usuario(db.Model):
    __tablename__ = 'usuarios'

    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    correo = db.Column(db.String(150), unique=True, nullable=False)
    contraseña = db.Column(db.String(200), nullable=False)
    fecha_registro = db.Column(db.DateTime, default=db.func.current_timestamp())

En este ejemplo, cada atributo como nombre, correo, etc., se define utilizando db.Column, especificando el tipo de dato y ciertas propiedades.

Tipos de datos básicos:

  • db.Integer: Representa un entero de tamaño variable.
  • db.String(size): Almacena una cadena de texto con una longitud máxima especificada por size.
  • db.Text: Almacena texto de longitud ilimitada.
  • db.DateTime: Almacena fecha y hora.
  • db.Float: Almacena números de punto flotante.
  • db.Boolean: Almacena valores booleanos (True o False).

Es importante elegir el tipo de dato adecuado para cada columna según la información que se desea almacenar.

Propiedades comunes de las columnas:

  • primary_key=True: Indica que la columna es la clave primaria de la tabla.
  • nullable=False: Establece que la columna no puede ser nula, es decir, debe contener un valor.
  • unique=True: Garantiza que los valores en esta columna sean únicos en toda la tabla.
  • default=valor: Especifica un valor por defecto si no se proporciona uno al crear un nuevo registro.
  • index=True: Crea un índice en esta columna para mejorar la velocidad de las consultas.

Por ejemplo, para asegurar que el correo electrónico sea único y obligatorio, se utiliza:

correo = db.Column(db.String(150), unique=True, nullable=False)

Clave primaria:

Cada modelo debe tener una clave primaria para identificar de manera única cada registro. Generalmente, se utiliza un campo id de tipo db.Integer con autoincremento. Esto se logra con primary_key=True, como en:

id = db.Column(db.Integer, primary_key=True)

Valores por defecto y funciones:

Para establecer valores por defecto, se puede asignar un valor estático o una función. Si se desea guardar la fecha y hora en que se crea un registro, se puede usar default=db.func.current_timestamp(), como en:

fecha_registro = db.Column(db.DateTime, default=db.func.current_timestamp())

Esto utiliza una función de base de datos para obtener la marca temporal actual al insertar un nuevo registro.

Ejemplo completo de un modelo:

class Producto(db.Model):
    __tablename__ = 'productos'

    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    descripcion = db.Column(db.Text)
    precio = db.Column(db.Float, nullable=False)
    en_stock = db.Column(db.Boolean, default=True)
    fecha_creacion = db.Column(db.DateTime, default=db.func.current_timestamp())

En este modelo Producto, hemos definido varios campos con diferentes tipos de datos y propiedades.

Resumen de propiedades de columnas:

  • db.Column: Constructor de columnas.
  • Tipos de datos
    • Especifican el tipo de dato de la columna (Integer, String, Float, etc.).
  • Restricciones:
    • nullable: Controla si el campo puede ser nulo.
    • unique: Asegura unicidad de valores.
    • index: Crea un índice para optimizar consultas.
    • default: Establece un valor por defecto.
    • primary_key: Define la clave primaria.

Consideraciones adicionales:

Al definir modelos, es fundamental que los nombres de las tablas y columnas sigan convenciones que faciliten el mantenimiento y la legibilidad del código. Por ejemplo, utilizar nombres en singular o plural de manera consistente y nombres de columnas descriptivos.

También es posible definir comentarios en las columnas para documentar su propósito:

edad = db.Column(db.Integer, nullable=False, comment='Edad del usuario en años')

Uso de tipos de datos específicos de MySQL:

Si se requiere utilizar un tipo de dato específico de MySQL, se puede importar de sqlalchemy.dialects.mysql:

from sqlalchemy.dialects.mysql import LONGTEXT

contenido = db.Column(LONGTEXT, nullable=False)

Esto permite aprovechar tipos de datos avanzados o personalizados que ofrece MySQL.

Propiedad __tablename__:

La propiedad __tablename__ permite especificar el nombre de la tabla en la base de datos. Si no se define, SQLAlchemy generará un nombre basado en el nombre de la clase en minúsculas.

Al definir modelos con sus columnas y campos básicos, se establece la estructura fundamental sobre la cual la aplicación interactuará con la base de datos, permitiendo manejar los datos de manera eficaz y coherente.

Propiedades y restricciones de columnas

Al definir modelos en Flask usando SQLAlchemy, es fundamental especificar las propiedades y restricciones de las columnas para garantizar la integridad y eficiencia de la base de datos. Estas propiedades permiten controlar aspectos como valores por defecto, validaciones y comportamientos específicos en las columnas.

Además de utilizar unique=True en columnas individuales, es posible definir restricciones de unicidad que involucren múltiples columnas mediante UniqueConstraint. Esto es útil cuando se requiere que la combinación de valores en varias columnas sea única.

from sqlalchemy.schema import UniqueConstraint

class Inscripcion(db.Model):
    __tablename__ = 'inscripciones'
    id = db.Column(db.Integer, primary_key=True)
    estudiante_id = db.Column(db.Integer, db.ForeignKey('estudiantes.id'), nullable=False)
    curso_id = db.Column(db.Integer, db.ForeignKey('cursos.id'), nullable=False)
    fecha_inscripcion = db.Column(db.DateTime, default=db.func.current_timestamp())

    __table_args__ = (
        UniqueConstraint('estudiante_id', 'curso_id', name='uix_estudiante_curso'),
    )

En este ejemplo, se garantiza que un estudiante no pueda inscribirse más de una vez en el mismo curso gracias a la restricción de unicidad definida en __table_args__.

Las restricciones de comprobación permiten validar que los valores almacenados en una columna cumplan ciertas condiciones. Se definen utilizando CheckConstraint.

from sqlalchemy import CheckConstraint

class Producto(db.Model):
    __tablename__ = 'productos'
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    precio = db.Column(db.Float, nullable=False)
    stock = db.Column(db.Integer, nullable=False)

    __table_args__ = (
        CheckConstraint('precio > 0', name='chk_precio_positivo'),
        CheckConstraint('stock >= 0', name='chk_stock_no_negativo'),
    )

Aquí, las restricciones de comprobación aseguran que el precio sea mayor que cero y que el stock no sea negativo.

Para establecer un valor por defecto que sea gestionado por la base de datos en lugar de por SQLAlchemy, se utiliza la opción server_default.

class Usuario(db.Model):
    __tablename__ = 'usuarios'
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    estado = db.Column(db.String(20), server_default='activo', nullable=False)

El uso de server_default asegura que, si no se proporciona un valor para estado, la base de datos asignará automáticamente 'activo'.

Para mejorar el rendimiento de las consultas, se pueden definir índices en columnas frecuentemente utilizadas en filtros o uniones.

class Cliente(db.Model):
    __tablename__ = 'clientes'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(150), nullable=False)
    telefono = db.Column(db.String(20))

    __table_args__ = (
        db.Index('idx_email', 'email'),
    )

Aunque index=True en una columna individual crea un índice, utilizar db.Index permite crear índices compuestos o nombrados.

Se pueden utilizar funciones como default y onupdate para controlar el comportamiento de las columnas al crear o actualizar registros.

class Documento(db.Model):
    __tablename__ = 'documentos'
    id = db.Column(db.Integer, primary_key=True)
    titulo = db.Column(db.String(200), nullable=False)
    ultima_modificacion = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp())

La propiedad onupdate actualiza automáticamente ultima_modificacion cada vez que el registro es modificado, utilizando la función de la base de datos.

Al trabajar con números decimales, es posible especificar la precisión y escala utilizando db.Numeric.

class Transaccion(db.Model):
    __tablename__ = 'transacciones'
    id = db.Column(db.Integer, primary_key=True)
    monto = db.Column(db.Numeric(precision=10, scale=2), nullable=False)
    fecha = db.Column(db.DateTime, default=db.func.current_timestamp())

Esto asegura que el campo monto almacene números con hasta 10 dígitos en total y 2 decimales.

En casos avanzados, es posible utilizar expresiones SQL directamente en las restricciones o valores por defecto.

from sqlalchemy.sql import expression

class Evento(db.Model):
    __tablename__ = 'eventos'
    id = db.Column(db.Integer, primary_key=True)
    publico = db.Column(db.Boolean, server_default=expression.true(), nullable=False)

Aquí, server_default=expression.true() establece el valor por defecto de publico a verdadero utilizando una expresión SQL.

Al utilizar tipos de datos y restricciones, es crucial tener en cuenta la compatibilidad con el motor de base de datos. Algunas restricciones o tipos pueden no ser soportados en MySQL o requerir sintaxis específica. Por ejemplo, MySQL tiene limitaciones con CheckConstraint.

Dado que MySQL puede ignorar las restricciones CHECK, es necesario implementar validaciones personalizadas a nivel de aplicación o usar alternativas como triggers.

class Empleado(db.Model):
    __tablename__ = 'empleados'
    id = db.Column(db.Integer, primary_key=True)
    edad = db.Column(db.Integer, nullable=False)
    salario = db.Column(db.Float, nullable=False)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.edad < 18:
            raise ValueError('La edad debe ser mayor o igual a 18')
        if self.salario <= 0:
            raise ValueError('El salario debe ser positivo')

En este ejemplo, se implementan validaciones personalizadas en el constructor para garantizar que se cumplan ciertas restricciones.

Creación de tablas a partir de modelos

Una vez definidos los modelos en Flask utilizando SQLAlchemy, es necesario crear las tablas correspondientes en la base de datos para almacenar los datos. Para ello, se sincronizan los modelos con la base de datos, generando las estructuras según las definiciones en el código.

El método más sencillo para crear las tablas es mediante la función db.create_all(). Esta función analiza todos los modelos registrados y crea las tablas asociadas en la base de datos MySQL. Es fundamental asegurarse de que la aplicación esté correctamente configurada y que la conexión a la base de datos sea válida.

A continuación, se muestra cómo configurar la aplicación y crear las tablas:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# Configuración de la conexión a MySQL
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://usuario:contraseña@localhost/nombre_basedatos'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# Definición de modelos
class Cliente(db.Model):
    __tablename__ = 'clientes'
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(150), unique=True, nullable=False)

if __name__ == '__main__':
    with app.app_context():
        db.create_all()

Es importante ejecutar db.create_all() dentro del contexto de la aplicación. Esto se logra utilizando app.app_context(), lo que permite acceder a la configuración y a las extensiones de Flask. Sin este contexto, se producirían errores al intentar interactuar con la base de datos.

La función db.create_all() crea las tablas únicamente si no existen previamente. Por lo tanto, es seguro ejecutarla en múltiples ocasiones. Sin embargo, es esencial tener en cuenta que si se realizan cambios en los modelos después de haber creado las tablas, db.create_all() no modificará la estructura existente. Para aplicar cambios en las tablas, se deben utilizar migraciones, que permiten actualizar la base de datos sin pérdida de datos.

Otra forma de gestionar la creación de tablas es mediante comandos personalizados utilizando el CLI de Flask. Esto ofrece una mayor flexibilidad y control sobre cuándo y cómo se crean las tablas.

import click
from flask.cli import with_appcontext

@app.cli.command('crear-tablas')
@with_appcontext
def crear_tablas():
    db.create_all()
    click.echo('Las tablas han sido creadas exitosamente.')

Para ejecutar este comando, se utiliza la línea de comandos:

flask crear-tablas

Este enfoque es especialmente útil en proyectos más grandes donde se desea mantener el código ordenado y evitar ejecutar código adicional al iniciar la aplicación.

También es posible especificar opciones adicionales para las tablas mediante __table_args__. Por ejemplo, se puede definir el motor de almacenamiento y el conjunto de caracteres:

class Producto(db.Model):
    __tablename__ = 'productos'
    __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'}
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    precio = db.Column(db.Numeric(precision=10, scale=2), nullable=False)

Estas configuraciones aseguran que las tablas utilicen el motor InnoDB y soporten el conjunto de caracteres utf8mb4, permitiendo almacenar emojis y caracteres especiales.

Para confirmar que las tablas se han creado correctamente, se puede utilizar una herramienta como MySQL Workbench o conectarse directamente a MySQL y ejecutar:

USE nombre_basedatos;
SHOW TABLES;

Esto mostrará la lista de tablas existentes, incluyendo aquellas creadas a partir de los modelos.

Es recomendable manejar la creación de tablas de forma controlada, especialmente en entornos de producción. En lugar de utilizar db.create_all(), se debe emplear una herramienta de migraciones que gestione los cambios en la estructura de la base de datos. Esto permite realizar alteraciones incrementales sin afectar los datos existentes.

Finalmente, es importante recordar que cualquier cambio en los modelos después de la creación inicial de las tablas no se reflejará automáticamente en la base de datos. Por ello, es esencial planificar y utilizar herramientas adecuadas para mantener la consistencia entre los modelos y la estructura de la base de datos.

Tipos de datos avanzados y personalizados

En SQLAlchemy, además de los tipos de datos básicos, podemos utilizar tipos de datos avanzados que permiten almacenar información más compleja en nuestras aplicaciones Flask. Estos tipos de datos ofrecen funcionalidades adicionales y son esenciales para modelar estructuras de datos sofisticados.

Tipos de datos avanzados integrados:

  • Enum: Permite almacenar valores de un conjunto predefinido de opciones, garantizando que una columna solo contenga uno de los valores especificados.
from sqlalchemy.dialects.mysql import ENUM

class Pedido(db.Model):
    __tablename__ = 'pedidos'
    id = db.Column(db.Integer, primary_key=True)
    estado = db.Column(ENUM('pendiente', 'enviado', 'entregado', 'cancelado'), nullable=False, default='pendiente')

En este ejemplo, la columna estado solo puede contener uno de los valores definidos, asegurando la integridad de los datos.

  • JSON: Permite almacenar datos en formato JSON, ideal para guardar estructuras anidadas sin necesidad de tablas adicionales.
from sqlalchemy.dialects.mysql import JSON

class Configuracion(db.Model):
    __tablename__ = 'configuraciones'
    id = db.Column(db.Integer, primary_key=True)
    ajustes = db.Column(JSON, nullable=False)

La columna ajustes puede almacenar diccionarios o listas de Python que se serializan automáticamente a JSON.

  • LargeBinary: Utilizado para almacenar datos binarios de gran tamaño, como archivos o imágenes.
 class Archivo(db.Model):
    __tablename__ = "archivos"
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    contenido = db.Column(db.LargeBinary, nullable=False)

Aquí, contenido almacena el contenido binario del archivo.

  • Date, Time y Interval: Para manejar fechas, horas y períodos de tiempo.
class Evento(db.Model):
    __tablename__ = 'eventos'
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    fecha = db.Column(db.Date, nullable=False)
    hora_inicio = db.Column(db.Time, nullable=False)
    duracion = db.Column(db.Interval)

Estos tipos permiten operaciones avanzadas con fechas y horas de manera eficiente.

  • Decimal: Para almacenar números decimales con precisión fija, ideal para cálculos financieros.
from sqlalchemy import DECIMAL

class Factura(db.Model):
    __tablename__ = 'facturas'
    id = db.Column(db.Integer, primary_key=True)
    total = db.Column(DECIMAL(precision=12, scale=2), nullable=False)

El campo total almacena valores numéricos con 12 dígitos en total y 2 decimales.

Creación de tipos de datos personalizados:

En ocasiones, es necesario definir un tipo de dato personalizado para satisfacer requisitos específicos. SQLAlchemy permite crear tipos personalizados mediante la creación de subclases de TypeDecorator.

Por ejemplo, si deseamos almacenar coordenadas geográficas como un par de números, podemos crear un tipo personalizado:

from sqlalchemy.types import TypeDecorator, String

class Coordenadas(TypeDecorator):
    impl = String(50)

    def process_bind_param(self, value, dialect):
        if value is not None:
            return f"{value[0]},{value[1]}"
        return None

    def process_result_value(self, value, dialect):
        if value is not None:
            lat, lon = value.split(',')
            return (float(lat), float(lon))
        return None

Usamos este tipo en un modelo:

class Lugar(db.Model):
    __tablename__ = 'lugares'
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    ubicacion = db.Column(Coordenadas, nullable=False)

Con este tipo personalizado, podemos asignar y recuperar coordenadas como tuplas de Python.

Uso de tipos de datos híbridos y propiedades columnarias:

Las propiedades híbridas permiten definir atributos en los modelos que actúan como columnas y métodos a la vez. Se utilizan para crear lógica adicional o calcular valores dinámicos.

from sqlalchemy.ext.hybrid import hybrid_property

class Producto(db.Model):
    __tablename__ = 'productos'
    id = db.Column(db.Integer, primary_key=True)
    precio_costo = db.Column(db.Numeric(10, 2), nullable=False)
    margen_ganancia = db.Column(db.Numeric(5, 2), nullable=False)

    @hybrid_property
    def precio_venta(self):
        return self.precio_costo * (1 + self.margen_ganancia / 100)

Con @hybrid_property, podemos acceder a precio_venta como si fuera una columna, pero sin almacenarla en la base de datos.

Tipos de datos mutables y tipos arbitrarios:

Cuando se almacenan tipos mutables como listas o diccionarios, se debe utilizar un método para notificar a SQLAlchemy sobre los cambios. Esto se logra con Mutable y MutableDict.

from sqlalchemy.ext.mutable import MutableDict

class Usuario(db.Model):
    __tablename__ = 'usuarios'
    id = db.Column(db.Integer, primary_key=True)
    preferencias = db.Column(MutableDict.as_mutable(JSON), nullable=False, default=dict)

Con MutableDict, los cambios dentro del campo preferencias serán detectados automáticamente, permitiendo actualizaciones adecuadas en la base de datos.

Implementación de tipados anidados y composite types:

En MySQL, no se soportan directamente los tipos compuestos como en otros motores, pero se pueden simular utilizando tipos JSON o creando tipos personalizados.

Almacenamiento de listas y estructuras complejas:

Para almacenar una lista de valores, podemos utilizar el tipo JSON. Con JSON:

class Encuesta(db.Model):
    __tablename__ = 'encuestas'
    id = db.Column(db.Integer, primary_key=True)
    respuestas = db.Column(MutableDict.as_mutable(JSON), nullable=False, default=list)

Las respuestas se almacenan como una lista en formato JSON.

Consideraciones sobre tipos específicos de MySQL:

Podemos utilizar tipos específicos del dialecto MySQL para aprovechar funcionalidades especiales.

  • YEAR: para almacenar años.
from sqlalchemy.dialects.mysql import YEAR

class Vehiculo(db.Model):
    __tablename__ = 'vehiculos'
    id = db.Column(db.Integer, primary_key=True)
    modelo = db.Column(YEAR, nullable=False)
  • SET: para almacenar conjuntos de valores.
from sqlalchemy.dialects.mysql import SET

class Usuario(db.Model):
    __tablename__ = 'usuarios'
    id = db.Column(db.Integer, primary_key=True)
    roles = db.Column(SET('admin', 'editor', 'lector'), default='lector', nullable=False)

Uso de enums a nivel de aplicación:

En lugar de los enums del dialecto MySQL, es recomendable usar la clase Enum de Python para mejorar la portabilidad.

from enum import Enum as PyEnum

class EstadoPedido(PyEnum):
    pendiente = 'pendiente'
    enviado = 'enviado'
    entregado = 'entregado'
    cancelado = 'cancelado'

class Pedido(db.Model):
    __tablename__ = 'pedidos'
    id = db.Column(db.Integer, primary_key=True)
    estado = db.Column(db.Enum(EstadoPedido), nullable=False, default=EstadoPedido.pendiente)

Al utilizar db.Enum(EstadoPedido), se mejora la integración con Python y se garantiza una mejor compatibilidad entre diferentes motores de base de datos.

Tipos de datos UUID y direcciones IP:

Para manejar identificadores únicos universales o direcciones IP, podemos usar tipos especializados.

import uuid
from sqlalchemy.dialects.mysql import BINARY

class Sesion(db.Model):
    __tablename__ = 'sesiones'
    id = db.Column(BINARY(16), primary_key=True, default=lambda: uuid.uuid4().bytes)
    usuario_id = db.Column(db.Integer, db.ForeignKey('usuarios.id'), nullable=False)

Aquí, id es un UUID almacenado como binario de 16 bytes.

Para direcciones IP:

from ipaddress import ip_address
from sqlalchemy_utils import IPAddressType

class Acceso(db.Model):
    __tablename__ = 'accesos'
    id = db.Column(db.Integer, primary_key=True)
    direccion_ip = db.Column(IPAddressType, nullable=False)

Alcance de los tipos personalizados en Flask:

Crear y utilizar tipos personalizados permite adaptar la capa de persistencia a las necesidades específicas de la aplicación. Facilita la manipulación de datos complejos y asegura que la lógica de negocio esté alineada con la estructura de datos.

Es importante probar y validar el comportamiento de estos tipos para garantizar que la aplicación funcione de manera correcta y eficiente. Además, se debe considerar la portabilidad y compatibilidad con el motor de base de datos seleccionado.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

20 % DE DESCUENTO

Plan mensual

19.00 /mes

15.20 € /mes

Precio normal mensual: 19 €
58 % DE DESCUENTO

Plan anual

10.00 /mes

8.00 € /mes

Ahorras 132 € al año
Precio normal anual: 120 €
Aprende Flask GRATIS online

Todas las lecciones de Flask

Accede a todas las lecciones de Flask y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Flask y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la estructura básica de un modelo SQLAlchemy en Flask.
  • Identificar y aplicar tipos de datos adecuados para columnas.
  • Configurar propiedades como primary_key, nullable y unique.
  • Utilizar funciones y valores por defecto.
  • Implementar restricciones de unicidad y comprobación.
  • Crear y gestionar tablas a partir de modelos.
  • Aplicar tipos de datos avanzados y personalizados.