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
- El problema
- La solución
- Los inconvenientes
- Las alternativas
- 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:
- Validar datos de entrada
- Llamar a métodos de modelos, proxies y managers
- 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