Flask

Flask

Tutorial Flask: Validaciones y constraints

Aprende a implementar validaciones y constraints en SQLAlchemy para garantizar la integridad y seguridad de tus datos en modelos Python.

Aprende Flask y certifícate

Validaciones de campo

Las validaciones de campo en SQLAlchemy permiten garantizar la integridad de los datos directamente en el modelo, antes de que lleguen a la base de datos. Estas validaciones se ejecutan automáticamente cuando intentamos crear o modificar registros, proporcionando una capa de seguridad adicional que complementa las restricciones de la base de datos.

SQLAlchemy ofrece múltiples mecanismos para implementar validaciones, desde constraints básicos hasta validaciones personalizadas más complejas. La ventaja de implementar estas validaciones a nivel de modelo es que se aplican consistentemente independientemente de cómo se acceda a los datos.

Validaciones con el decorador validates

El decorador @validates es la forma más directa de implementar validaciones personalizadas en SQLAlchemy. Este decorador permite definir funciones que se ejecutan automáticamente cuando se asigna un valor a un campo específico.

from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import validates
from datetime import datetime
import re

class Usuario(db.Model):
    __tablename__ = 'usuarios'
    
    id = Column(Integer, primary_key=True)
    email = Column(String(120), nullable=False, unique=True)
    edad = Column(Integer)
    telefono = Column(String(15))
    fecha_registro = Column(DateTime, default=datetime.utcnow)
    
    @validates('email')
    def validar_email(self, key, email):
        patron = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(patron, email):
            raise ValueError("El formato del email no es válido")
        return email.lower()
    
    @validates('edad')
    def validar_edad(self, key, edad):
        if edad is not None and (edad < 0 or edad > 120):
            raise ValueError("La edad debe estar entre 0 y 120 años")
        return edad

En este ejemplo, las funciones de validación reciben tres parámetros: self (la instancia del modelo), key (el nombre del campo) y el valor que se está asignando. La función debe retornar el valor validado o lanzar una excepción si la validación falla.

Validaciones múltiples y transformaciones

El decorador @validates también permite validar múltiples campos con una sola función y realizar transformaciones de datos durante la asignación:

class Producto(db.Model):
    __tablename__ = 'productos'
    
    id = Column(Integer, primary_key=True)
    nombre = Column(String(100), nullable=False)
    precio = Column(Integer)  # Precio en centavos
    codigo = Column(String(20), unique=True)
    
    @validates('nombre', 'codigo')
    def validar_texto(self, key, valor):
        if not valor or not valor.strip():
            raise ValueError(f"El campo {key} no puede estar vacío")
        
        # Transformación: convertir a mayúsculas para códigos
        if key == 'codigo':
            return valor.upper().strip()
        
        return valor.strip()
    
    @validates('precio')
    def validar_precio(self, key, precio):
        if precio is not None and precio <= 0:
            raise ValueError("El precio debe ser mayor que cero")
        return precio

Validaciones con hybrid_property

Los hybrid properties ofrecen una alternativa elegante para campos que requieren validaciones complejas o cálculos dinámicos:

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import Column, String, Integer

class Empleado(db.Model):
    __tablename__ = 'empleados'
    
    id = Column(Integer, primary_key=True)
    _salario_base = Column('salario_base', Integer)  # Almacenado en centavos
    dni = Column(String(9), unique=True)
    
    @hybrid_property
    def salario_base(self):
        return self._salario_base / 100 if self._salario_base else 0
    
    @salario_base.setter
    def salario_base(self, valor):
        if valor < 0:
            raise ValueError("El salario no puede ser negativo")
        if valor > 1000000:  # 10,000 euros
            raise ValueError("El salario excede el límite máximo")
        self._salario_base = int(valor * 100)
    
    @validates('dni')
    def validar_dni(self, key, dni):
        if not dni or len(dni) != 9:
            raise ValueError("El DNI debe tener exactamente 9 caracteres")
        
        # Validación básica del formato DNI español
        numeros = dni[:8]
        letra = dni[8].upper()
        
        if not numeros.isdigit():
            raise ValueError("Los primeros 8 caracteres del DNI deben ser números")
        
        letras_validas = "TRWAGMYFPDXBNJZSQVHLCKE"
        letra_esperada = letras_validas[int(numeros) % 23]
        
        if letra != letra_esperada:
            raise ValueError("La letra del DNI no es correcta")
        
        return dni.upper()

Validaciones condicionales y dependientes

En ocasiones necesitamos validaciones que dependen del valor de otros campos o del estado del objeto:

from sqlalchemy import Column, String, Integer, Boolean, DateTime
from datetime import datetime, timedelta

class Reserva(db.Model):
    __tablename__ = 'reservas'
    
    id = Column(Integer, primary_key=True)
    fecha_inicio = Column(DateTime, nullable=False)
    fecha_fin = Column(DateTime, nullable=False)
    confirmada = Column(Boolean, default=False)
    precio_total = Column(Integer)
    descuento_aplicado = Column(Integer, default=0)
    
    @validates('fecha_fin')
    def validar_fecha_fin(self, key, fecha_fin):
        if self.fecha_inicio and fecha_fin <= self.fecha_inicio:
            raise ValueError("La fecha de fin debe ser posterior a la fecha de inicio")
        
        # No permitir reservas de más de 30 días
        if self.fecha_inicio and (fecha_fin - self.fecha_inicio).days > 30:
            raise ValueError("Las reservas no pueden exceder 30 días")
        
        return fecha_fin
    
    @validates('descuento_aplicado')
    def validar_descuento(self, key, descuento):
        if descuento < 0 or descuento > 100:
            raise ValueError("El descuento debe estar entre 0 y 100")
        
        # Solo permitir descuentos altos en reservas confirmadas
        if descuento > 50 and not self.confirmada:
            raise ValueError("Descuentos superiores al 50% requieren confirmación previa")
        
        return descuento

Manejo de errores en validaciones

Es importante capturar y manejar adecuadamente los errores de validación en nuestras rutas de Flask:

from flask import request, jsonify
from sqlalchemy.exc import IntegrityError

@app.route('/usuarios', methods=['POST'])
def crear_usuario():
    try:
        datos = request.get_json()
        
        usuario = Usuario(
            email=datos.get('email'),
            edad=datos.get('edad'),
            telefono=datos.get('telefono')
        )
        
        db.session.add(usuario)
        db.session.commit()
        
        return jsonify({
            'mensaje': 'Usuario creado exitosamente',
            'id': usuario.id
        }), 201
        
    except ValueError as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 400
        
    except IntegrityError as e:
        db.session.rollback()
        return jsonify({
            'error': 'Error de integridad en la base de datos',
            'detalle': 'Posible duplicado en campo único'
        }), 409

Las validaciones de campo proporcionan una capa robusta de protección que garantiza la calidad de los datos almacenados. Al combinar el decorador @validates con hybrid properties y un manejo adecuado de errores, podemos crear modelos que no solo almacenen datos, sino que también los validen y transformen según las reglas de negocio de nuestra aplicación.

Aprende Flask online

Otras 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

Ejercicios de programación de Flask

Evalúa tus conocimientos de esta lección Validaciones y constraints con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.