Adelgazando los modelos de Django

Héctor Pablos López

PyConES 2016

#whoami

Héctor Pablos López

Front-End developer @ StyleSage

www.stylesage.co

hector@stylesage.co

TODOs

  1. El problema
  2. La solución
  3. Los inconvenientes
  4. Las alternativas
  5. Conclusión

1. El problema

Modelos de Django "Gordos"

¿Los fat models no son "best practice"?

1. El problema: Modelos de Django "Gordos"

Fat model

Overweight model

  • Fields (por supuesto)
  • Propiedades
  • Métodos "sencillos"
  • Todo lo anterior
  • Métodos "complejos"
  • Validaciones de datos
  • Manejo de excepciones
  • Queries

Indicadores de "overweight model"

1. El problema: Modelos de Django "Gordos"

Métodos que interaccionan con propiedades internas de otros modelos

class Car(models.Model):
    owner = models.ForeignKey(User)


class User(models.Model):

    # Fields and simple methods...

    def get_car_models_owned(self)
        return [car.model for car in self.car_set.all()]

1. El problema: Modelos de Django "Gordos"

Métodos que ejecutan queries

class User(models.Model):

    # Fields and simple methods...

    def get_red_owned_cars(self)
        return self.car_set.filter(color='red')

Indicadores de "overweight model"

1. El problema: Modelos de Django "Gordos"

Validaciones o tratamiento de excepciones

class User(models.Model):

    # Fields and simple methods...

    def get_owned_cars_built_in_year(self, year)
        # Check the year is correct
        try:
            if 1900 <= year <= 2016:
                return self.car_set.filter(build_year=year)
            else:
                raise SomeKindOfException
        except Exception:
            return "An error has happened"

Indicadores de "overweight model"

1. El problema: Modelos de Django "Gordos"

Creo que debería adelgazar...

Además de overweight models

1. El problema: Modelos de Django "Gordos"

Módulos controlador o de utilidades

# users_controller.py

def get_active_users():
    return User.objects.filter(is_banned=False, account_expiry_date__gte=datetime.now())


# cars_controller.py

def get_luxury_cars()
    return Car.objects.filter(price__get=100000)

Para tratar conjuntos de modelos

1. El problema: Modelos de Django "Gordos"

Vistas sobrecargadas que modifican los modelos

def buy_car(request, car_id):
    try:
        car_to_buy = Cars.objects.get(car_id)
        if car_to_buy.price > request.user.available_money:
            raise SomeKindOfException                       # Validating logic
        request.user.car_set.add(car_to_buy)                # Manipulating model
    except Car.DoesNotExist:
        raise SomeKindOfException()

Además de overweight models

1. El problema: Modelos de Django "Gordos"

Dependencias circulares

Model

"Controller"

View

Además de overweight models

1. El problema: Modelos de Django "Gordos"

Regla general

Never write to a model field or call save() directly. Always use model methods and manager methods for state changing operations.

Tom Christie - https://www.dabapps.com/blog/django-models-and-encapsulation/

2. La Solución

Healthy models, proxy models, model managers

2. La solución: Thin models, proxy models, model managers

Paso 1: Adelgazar el modelo

  • Fields
  • @properties y  métodos simples
class Car(models.Model):
    owner = models.ForeignKey(User)
    plate = models.CharField(max_length=8)
    km_run = models.IntegerField()

    @property
    def mi_run(self):
        return self.km_run * 0.621371

Nada más!!

2. La solución: Thin models, proxy models, model managers

Paso 2: Crear proxy models

  • Uno por modelo
  • Métodos complejos
    • Modificar el modelo original
    • Validaciones
    • Interacción con otros modelos
class CarProxy(Car):

    def add_to_km_run(self, km_to_add):
        assert km_to_add > 0
        self.km_run += km_to_add
        self.owner.add_to_km_run(km_to_add)
        self.save()

    class Meta:
        proxy = True

2. La solución: Thin models, proxy models, model managers

Paso 3: Crear model managers

  • Uno por modelo
  • Métodos que actúan sobre conjuntos de modelos
class CarManager:

    def get_luxury_cars()
        return Car.objects.filter(price__get=100000)

2. La solución: Thin models, proxy models, model managers

Paso 4: Limpiar vistas

Las vistas sólo se encargan de:

  1. Validar datos de entrada
  2. Llamar a métodos de modelos, proxies y managers
  3. Devolver datos (render template, json...)
def buy_car(request, car_id):
    try:
        car_to_buy = Cars.objects.get(car_id)
        request.user.buy_car(car_to_buy)
        return successful_response("Some message")
    except Exception as e:
        return map_exception_to_response(e)

2. La solución: Thin models, proxy models, model managers

Reglas de importación

  • Como si fuera una arquitectura por capas no estricta
  • Los modelos no importan ninguno de los elementos

Modelos

Proxies

Managers

Vistas

3. Los inconvenientes

3. Los inconvenientes

Building blocks

Convertir un modelo a un proxy

car = User.objects.get(id=1)
car.__class__ = CarProxy

Convertir un queryset de modelos a queryset de proxies

user_owned_cars_qs = User.car_set
user_owned_cars_qs.model = CarProxy

request.user

Fácil de solucionar con un middleware

class ReplaceProxyUserInRequest(object):
    """
    Middleware to process each request, changing the default request.user,
    which is an instance of the User model, and replacing it with an
    instance of UserProxy with the same data.
    """
    def process_request(self, request):
        if request.user.is_authenticated():
            request.user.__class__ = UserProxy
        return None

3. Los inconvenientes

Foreign keys

Los proxies devuelven modelos, no otros proxies

class UserProxy(User):

    class Meta:
        proxy = True

    @property
    def workbook_set(self):
        qs = super(UserProxy, self).car_set
        qs.model = CarProxy
        return qs

Los "sets" se pueden enmascarar con propiedades

3. Los inconvenientes

Foreign keys

Los proxies devuelven modelos, no otros proxies

class CarProxy(User):

    class Meta:
        proxy = True

    @property
    def proxy_user(self):
        owner = super(CarProxy, self).owner
        owner.__class__ = CarProxy
        return owner

3. Los inconvenientes

4. Las alternativas

4. Las alternativas

django-polymorphic

Proyecto de Django para generar modelos polimórficos:

Se eliminan los drawbacks

https://github.com/django-polymorphic/django-polymorphic

4. Las alternativas

... Dejarlo estar

+ No afectan los inconvenientes mencionados

- Modelos de miles de líneas de código, con varios tipos de lógica mezclada

Respetando siempre buenas prácticas generales de programación:

  • No aplicar lógica a los modelos que no deban conocer
  • No sobrecargar las vistas con save() y cambios en propiedades

5. Conclusión

Esta es sólo una forma de muchas de mejorar la estructura de código en Django (¡y funciona!)

Seguro que tienes la tuya, ¡Cuéntanosla!

Héctor Pablos López

Front-End developer @ StyleSage

www.stylesage.co

hector@stylesage.co

We are hiring!!

stylesage.co/careers

Adelgazando los modelos de Django

By Hector Pablos

Adelgazando los modelos de Django

  • 3,489