191 votes

Comment extraire un enregistrement aléatoire en utilisant l'ORM de Django ?

J'ai un modèle qui représente les peintures que je présente sur mon site. Sur la page principale, j'aimerais en montrer quelques-unes : la plus récente, celle qui n'a pas été visitée depuis le plus longtemps, la plus populaire et une autre au hasard.

J'utilise Django 1.0.2.

Alors que les 3 premiers sont faciles à réaliser en utilisant les modèles de Django, le dernier (aléatoire) me pose quelques problèmes. Je peux bien sûr le coder dans ma vue, pour quelque chose comme ceci :

number_of_records = models.Painting.objects.count()
random_index = int(random.random()*number_of_records)+1
random_paint = models.Painting.get(pk = random_index)

Cela ne ressemble pas à quelque chose que j'aimerais avoir dans ma vue - cela fait entièrement partie de l'abstraction de la base de données et devrait être dans le modèle. De plus, ici je dois m'occuper des enregistrements supprimés (alors le nombre de tous les enregistrements ne me couvrira pas toutes les valeurs clés possibles) et probablement beaucoup d'autres choses.

Y a-t-il d'autres options pour le faire, de préférence dans l'abstraction du modèle ?

0 votes

La façon dont vous affichez les choses et les choses que vous affichez font partie du niveau "Vue" ou de la logique commerciale qui devrait aller dans le niveau "Contrôleur" de MVC, à mon avis.

0 votes

Dans Django, le contrôleur est la vue. docs.djangoproject.com/fr/dev/faq/general/

0 votes

Il devrait y avoir une fonction intégrée pour cela - une qui n'utilise pas order_by('?')

281voto

muhuk Points 6526

Il suffit d'utiliser :

MyModel.objects.order_by('?').first()

Il est documenté dans API QuerySet .

78 votes

Veuillez noter que cette approche peut être très lente, comme documenté :)

6 votes

"peut être coûteux et lent, selon le backend de la base de données que vous utilisez." - Avez-vous une expérience des différents backends de base de données (sqlite/mysql/postgres) ?

4 votes

Je ne l'ai pas testé, c'est donc une pure spéculation : pourquoi serait-il plus lent que de récupérer tous les éléments et d'effectuer la randomisation en Python ?

187voto

Emil Ivanov Points 18594

Utilisation de order_by('?') tuera le serveur de données le deuxième jour de la production. Une meilleure solution est quelque chose comme ce qui est décrit dans Obtenir une ligne aléatoire d'une base de données relationnelle .

from django.db.models.aggregates import Count
from random import randint

class PaintingManager(models.Manager):
    def random(self):
        count = self.aggregate(count=Count('id'))['count']
        random_index = randint(0, count - 1)
        return self.all()[random_index]

50 votes

Quels sont les avantages de model.objects.aggregate(count=Count('id'))['count'] sur model.objects.all().count()

15 votes

Bien qu'elle soit bien meilleure que la réponse acceptée, notez que cette approche nécessite deux requêtes SQL. Si le nombre change entre les deux, il est possible d'obtenir une erreur hors limites.

0 votes

Peut-être que l'annotation random(self) devrait être annotée avec "@transaction.atomic" pour éviter les problèmes de comptage des changements ? docs.djangoproject.com/ja/1.9/topics/db/transactions

26voto

Mikhail Korobov Points 6225

Les solutions avec order_by('?')[:N] sont extrêmement lentes même pour les tables de taille moyenne si vous utilisez MySQL (je ne sais pas pour les autres bases de données).

order_by('?')[:N] sera traduit en SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT N requête.

Cela signifie que pour chaque ligne du tableau, la fonction RAND() sera exécutée, puis le tableau entier sera trié selon la valeur de cette fonction et les N premiers enregistrements seront retournés. Si vos tables sont petites, c'est parfait. Mais dans la plupart des cas, cette requête est très lente.

J'ai écrit une fonction simple qui fonctionne même si les identifiants ont des trous (certaines lignes ont été supprimées) :

def get_random_item(model, max_id=None):
    if max_id is None:
        max_id = model.objects.aggregate(Max('id')).values()[0]
    min_id = math.ceil(max_id*random.random())
    return model.objects.filter(id__gte=min_id)[0]

Elle est plus rapide que order_by('?') dans presque tous les cas.

32 votes

Aussi, malheureusement, c'est loin d'être aléatoire. Si vous avez un enregistrement avec l'identifiant 1 et un autre avec l'identifiant 100, il renverra le second dans 99 % des cas.

11voto

Soviut Points 26384

Vous pourriez créer un manager sur votre modèle pour faire ce genre de choses. Pour comprendre d'abord ce qu'est un gestionnaire, le Painting.objects est un gestionnaire qui contient all() , filter() , get() etc. La création de votre propre gestionnaire vous permet de pré-filtrer les résultats et de faire travailler toutes ces mêmes méthodes, ainsi que vos propres méthodes personnalisées, sur les résultats.

EDIT : J'ai modifié mon code pour refléter la order_by['?'] méthode. Notez que le gestionnaire renvoie un nombre illimité de modèles aléatoires. Pour cette raison, j'ai inclus un peu de code d'utilisation pour montrer comment obtenir un seul modèle.

from django.db import models

class RandomManager(models.Manager):
    def get_query_set(self):
        return super(RandomManager, self).get_query_set().order_by('?')

class Painting(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=50)

    objects = models.Manager() # The default manager.
    randoms = RandomManager() # The random-specific manager.

Utilisation

random_painting = Painting.randoms.all()[0]

Enfin, vous pouvez avoir de nombreux gestionnaires sur vos modèles, alors n'hésitez pas à créer des LeastViewsManager() o MostPopularManager() .

4 votes

L'utilisation de get() ne fonctionne que si vos pk sont consécutifs, c'est-à-dire que vous ne supprimez jamais d'éléments. Sinon, vous risquez d'essayer d'obtenir un pk qui n'existe pas. L'utilisation de .all()[random_index] ne souffre pas de ce problème et n'est pas moins efficace.

0 votes

J'ai compris cela, c'est pourquoi mon exemple reproduit simplement le code de la question avec un gestionnaire. Il appartiendra toujours à l'OP d'élaborer son contrôle des limites.

1 votes

Au lieu d'utiliser .get(id=random_index), ne serait-il pas préférable d'utiliser .filter(id__gte=random_index)[0:1] ? Premièrement, cela permet de résoudre le problème des pks non consécutifs. Deuxièmement, get_query_set devrait retourner... un QuerySet. Or, dans votre exemple, ce n'est pas le cas.

5voto

Arnaud Points 973

Vous pourriez simplement le faire :

models.Painting.objects.all().order_by('?')[:1].get()

ce qui est beaucoup plus efficace.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X