Bienvenue!

Ressources

Read the Source, Luke

Yoda

Les modèles

Lorsque vous concevez des applications complexes, centralisez (autant que possible) la logique "métier" dans les modèles et leurs méthodes

Révisions rapides

  • Models

  • Fields

  • Managers

Héritage de modèles

  • abstract

  • Héritage multiple

  • proxy (modèles mandataires)

  • Imports circulaires

abstract

👍 Ajouter des champs générique pour tous vos modèles

👎 Colonnes supplémentaires (pas optimal si données souvent vides)

👎 Pas d'héritage multiple avec abstract

class DatedModel(Model):
    class Meta:
        abstract = True
    created = DateTimeField(auto_now_add=True)
    modified = DateTimeField(auto_now=True)

class Company(DatedModel):
    name = TextField()
date = DatedModel()  # Objection!
fedex = Company(name="Fedex")
fedex.created
        

Héritage multiple (1/2)

Création d'un OneToOneField automatique vers le parent et l'enfant si il existe

class Vehicle(DatedModel):
    color = CharField()

class Truck(Vehicle):
    owner = ForeignKey(Company)
        

Héritage multiple (2/2)

Tous les champs de Vehicle seront aussi disponible dans Truck, même si les données se trouveront dans des tables différentes

👍 Possibilité d'hériter de plusieurs classes, modèles de base (Vehicle) non-modifié et accessible

👍 Pratique pour étendre en conservant l'existant

👎 Jointures ou requêtes supplémentaires

👎 Peut complexifier le code (vérification d'existance de sous-objets)

mon_camion = Truck(color='red', owner=fedex)
mon_camion.color
mon_camion.vehicle
mon_camion.save()  # Pour obtenir l'ID
mon_vehicule = Vehicle.objects.get(pk=mon_camion.id)
assert mon_vehicule.truck == mon_camion
        

proxy (modèles mandataires)

Doc Django

Permet de modifier le comportement d'un modèle sans changer la façon dont il stocke ses données

class Car(Vehicle):
    class Meta:
        proxy = True
    def __str__(self):
        return "{} car".format(self.color)
    def paint(self, color):
        self.color = color
        

Imports circulaires

Si vous rencontrez le message d'erreur "Cannot import <module>", il peut s'agir d'un problème d'import circulaire

La solution consiste à importer le module dans la méthode où il est utilisée, plutôt qu'à la racine, en haut du fichier

Pour les clefs étrangères, plutôt que la classe elle-même, utilisez une chaîne contenant son nom

class Truck(Vehicle):
    owner = ForeignKey(Company)
        

Devient:

class Truck(Vehicle):
    owner = ForeignKey("Company")
        

Création d'un champ personnalisé (1/2)

👍 Les champs personnalisés permettent de stocker des données arbitraires tout en contrôlant la façon dont on les enregistre

👍 Peut engendrer des gains de performance spectaculaires dans ceraines circonstances

👎 Attention aux implémentations qui dépendent du moteur de base de données

👎 Dé-corrélation avec les données physiquement présentes dans la base

👎 Peut engendrer une certaine complexité: un niveau d'abstraction en plus (en trop?)

Création d'un champ personnalisé (2/2)

class CharMaxlength25Field(models.Field):
    def db_type(self, connection):
        return 'char(25)'  # Type spécifique à Postgresql

class BetterCharField(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super(BetterCharField, self).__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'char(%s)' % self.max_length  # Version paramétrique

    def get_prep_value(self, value):
        return str(value)[:self.max_length]  # De Python vers la BDD

    def to_python(self, value):
        return str(value)  # De la BDD vers Python

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_prep_value(value)
        

http://cjlarose.com/2014/01/14/django-custom-field-version-number.html

Création d'un Manager personnalisé (1/2)

Chaque modèle possède un manager par défaut, il s'agit de l'attribut objects

Un manager personnalisé permet de limiter (ou d'enrichir) le queryset lié à un modèle

Au lieu de requêter sur l'ensemble des objets comme on le fait sur objects, on se limite à un sous-ensemble

👍 Améliore la qualité du code, c'est à dire sa facilité de compréhension et sa maintenabilité

👎 Il faut se rappeler de l'utiliser 😅

Création d'un Manager personnalisé (2/2)

Par exemple, un manager personnalisé peut être utilisé pour cacher des objets au lieu de les supprimer

Cela permet d'ajouter une corbeille dans votre application, ou une fonction d'annulation de suppression

            class ActiveUserManager(models.Manager):
                def get_queryset(self):
                    return super(UserManager, self).get_queryset().filter(active=True)

            class User(models.Model):
                # Fields
                active = BooleanField(default=True)

                # Managers
                active_objects = ActiveManager()
        

Mise en pratique

  1. Créez un nouveau projet: application de gestion de finances personnelles
  2. Créer un modèle Expense
  3. Ce modèle doit stocker les données de façon efficiente et utiliser les éléments suivants:
    • Hériter d'un modèle abstrait: DatedModel
    • Posséder un champ personnalisé: MoneyField qui stocke les somme d'argent sous forme d'entiers dans la base (avec les centimes)
    • Utiliser un Manager personnalisé: active_objects

Tests unitaires

Les tests unitaires permettent de limiter les bugs induits et donc vous donnent confiance dans la robustesse de votre application

Écrire des tests sous Django

Plusieurs classes existent pour réaliser des tests unitaires sous Django:

  • django.test.TestCase
    • django.test.TransactionTestCase
      • django.test.LiveServerTestCase: Lance un vrai serveur Django ce qui permet de tester l'interface avec Selenium ou PhantomJS
      • django.test.StaticLiveServerTestCase: Idem + fichiers statiques sans avoir besoin de lancer collectstatic

Patching et Mocking

  • unittest.mock
  • unittest.patch

Couverture de code (code coverage)

L'outil coverage
https://coverage.readthedocs.io/en/coverage-4.4.2/

                # pip install coverage
                coverage run manage.py test
            

PyCharm Professional intègre un outil de couverture de code

Mise en pratique

Écrire les tests unitaires de notre application exemple

Bonus

Interfacer notre application avec fixer.io et mocker la requête.

Requêtes avancées

L'ORM de Django permet de réconcilier le modèle Objet de Python avec le stockage SQL...

Au prix d'une multiplication des requêtes, qui peut ralentir considérablement notre application

Nous allons voir comment mitiger ce problème

Annotate: avantages et limites

Annotate permet l'équivalent d'un group by en SQL

Compter le nombre de dépenses par utilisateur:

from django.db.models import Count
user = User.objects.all().annotate(Count('expense_set'))[0]
user.expense_set__count
        

Compter le total par utilisateur:

from django.db.models import Sum
user = User.objects.all().annotate(total=Sum('expense_set__amount'))
user.total
        

Une seule requête à chaque fois!

Aggregate

Aggregate permet d'aggréger des données en une seule par somme ou comptage

from django.db.models import Sum
User.objects.all().aggregate(grand_total=Sum('expense_set__amount'))
        

Utilisation avancée de select_related et prefetch_related

select_related

select_related permet d'économiser une requête lorsqu'on accède à un objet lié à un autre par une clef étrangère

expense = Expense.objects.get(pk=1)  # 1 requête
expense.user  # 1 requête

expense = Expense.objects.select_related('user').get(pk=1)  # 1 requête
expense.user  # Pas de requête!
            

Utilisation avancée de select_related et prefetch_related

select_related

select_related permet d'économiser une requête lorsqu'on accède à un objet lié à un autre par une clef étrangère

for expense in Expense.objects.all()  # 1 requête
    expense.user  # 1 requête à chaque fois!

for expense in Expense.objects.select_related('user').get(pk=1)  # 1 requête avec jointure
    expense.user  # Pas de requête!
            

Utilisation avancée de select_related et prefetch_related

prefetch_related

prefetch_related permet d'éviter de multiplier les requêtes quand on accède à un OneToMany (ForeignKey inversée)

        for user in User.objects.all():  # 1 requête
            for expense in user.expense_set.all():  # 1 requête par user!
                print(expense.amount)

        for user in User.objects.prefetch_related('expense_set').all():  # 2 requêtes: 1 pour User + 1 pour Expense
            for expense in user.expense_set.all():  # Pas de requête!
                print(expense.amount)
            

Mise en pratique

Réduction du nombre de requêtes

Les vues

Les vues sont l'adaptateur entre les modèles et l'affichage. Elles permettent de mettre les données au format souhaité en sortie afin que ces dernières soient exploitables

Révisions

Les vues sous forme de classes (class-based views)

Vues génériques

  • django.views.generic.TemplateView
    • django.views.generic.ListView
    • django.views.generic.DetailView

Les Mixins (1/2)

Mixins simples

  • django.views.generic.base.ContextMixin
  • django.views.generic.base.TemplateResponseMixin

Mixins mono-objets

  • django.views.generic.detail.SingleObjectMixin
  • django.views.generic.detail.SingleObjectTemplateResponseMixin

Mixins multi-objets

  • django.views.generic.list.MultipleObjectMixin
  • django.views.generic.list.MultipleObjectTemplateResponseMixin

Les Mixins (2/2)

Mixins d'édition

  • django.views.generic.edit.FormMixin
  • django.views.generic.edit.ModelFormMixin
  • django.views.generic.edit.ProcessFormView
  • django.views.generic.edit.DeletionMixin

Les middlewares (1/2)

Le middleware, comme son nom l'indique ("le truc au milieu") se situe entre l'arrivée d'une requête et son traitement par la vue.

Un middleware permet d'altérer très simplement le comportement global de Django

Les middlewares (2/2)

Voilà la liste des middlewares utilisés par défaut en v1.11:

  • django.middleware.security.SecurityMiddleware
  • django.contrib.sessions.middleware.SessionMiddleware
  • django.middleware.common.CommonMiddleware
  • django.middleware.csrf.CsrfViewMiddleware
  • django.contrib.auth.middleware.AuthenticationMiddleware
  • django.contrib.messages.middleware.MessageMiddleware
  • django.middleware.clickjacking.XFrameOptionsMiddleware

La sécurité est un concept global, un seul oubli et votre application n'est plus sûre.

Les middleware sont donc particulièrement bien adaptés pour assurer la sécurité de votre application.

Mise en pratique

Création d'un mixin de vue générique et d'un middleware

Les templates

Les templates génèrent le code HTML. Evitez (autant que possible) d'y placer la moindre logique (a fortiori la logique "métier").

Révisions

Filters

{{ expense.name|title }}

{{ expense.name|title|safe }}

Tags

{% block %}

{% include %}

{% render expense_form %}

Filters personnalisés

Les filtres permettent de modifier le format d'affichage d'un élément de façon générique

Tags personnalisés

Les tags personnalisés sont utiles pour injecter une logique personnalisée dans le template lorsque qu'on ne peut pas faire autrement.

À utiliser avec parcimonie.

Mise en pratique

Création d'un filtre personnalisé

Sécurité avancée

Django possède un arsenal de protections contre les attaques courantes. Ces protections sont activées par défaut.

Il est néanmoins indispensable d'en connaître le fonctionnement et les limites.

D'autre part, c'est vous qui êtes responsables de l'accès aux données et leur cloisonnement par utilisateur.

Révisions

Protection contre le CSRF (csrf token)

Customisation de l'authentification

  • JWT: JSON Web Tokens
  • Clef d'API

Authentification par OAuth, SAML2

Il s'agit de Single Sign On (SSO).

Mise en pratique

Sécuriser notre application et séparer les données de chaque utilisateur de façon sécurisée.

Migrations

Les migrations permettent de synchroniser l'état de notre base de données avec l'état de notre code

Migration de données: Python et SQL

Les migrations de données permettent de réaliser une modification de données de façon répétable.

Fusion de migrations

Des migrations de schéma peuvent se retrouver sur des branches de versionning séparées et dépendre d'une même migration "mère"

Au moment de la fusion des branches (par un git merge, par exemple), ces migrations vont entrer en conflit car l'outil de migration de Django sera incapable de déterminer quelle migration choisir

Dans ce cas, si les modifications concernent des tables différentes, il est possible de créer une migration supplémentaire qui servira à fusionner (merge) ces deux migrations "filles".

python manage.py makemigrations --merge

Mise en pratique

Créer une migration de données personnalisée

Django Rest Framework

DRF est un framework qui enrichit Django pour permettre de concevoir rapidement des API et Web Services.

Les ViewSets

Le concept de ViewSet permet de concevoir rapidement un ensemble de vues génériques.

Les ViewSets redirigent les requêtes suite à leur routage, et les dirigent vers le Serializer approprié.

Les Serializers

Dans Django Rest Framework, le Serializer remplace en quelque sorte le template HTML

Les Serializers définissent la façon dont les objets Django sont convertis depuis et vers le format cible qui est en général le JSON, parfois le XML.

Mise en pratique

Ajouter une API auto-documentée à notre application

Performance

  1. Make it work
  2. Make it right
  3. Make it fast

Profiling

À partir d'un certain niveau de complexité, il est impossible d'appréhender de visu les "points chauds" d'un programme

Pour identifier ces goulets d'étranglement, le profiling est un outil puissant et nécessaire

https://docs.python.org/3/library/profile.html

Django Debug Toolbar

Un outil très intéressant permettant d'afficher les requêtes SQL brutes et d'analyser le temps écoulé pour chacune

Gestion du cache

https://docs.djangoproject.com/en/1.11/topics/cache/ Django s'interface par défaut avec les back-ends de cache suivants:

  • Memcache: Extrêmement rapide, distribué et volatile (limité à la RAM disponible)
  • La base de données: Rapide et persistant. Possibilité de déporter le cache sur une autre base pour éviter de charger la base principale
  • Le système de fichiers: Un peu plus lent, persistant et limité seulement à la capacité du disque
  • Mémoire locale: Rapide, non-persistant et non-distribué
  • "Dummy": Ne cache pas (!), est uniquement utilisé pour tester l'interface de cache
  • Redis: Disponible grâce au plugin django-redis

Il est aussi possible de créer son propre backend de cache si les backends par défaut ne conviennent pas à votre besoin

Mise en pratique

Améliorer les performances de notre application

Gardons contact

Sylvain Josserand - [email protected]

intuitivo.fr

Conditions d'utilisation

L'utilisation de ce support et sa copie dans un but non lucratif est autorisée, à condition de conserver cette diapositive.

L'utilisation commerciale de ce support n'est pas autorisée sans mon autorisation préalable. Contactez-moi.

/