[Django] Union avec annotation

a marqué ce sujet comme résolu.

Bonjour,

J’essaie de créer un flux de syndication commun à plusieurs models de ma base de données.

Ce code fonctionne :

class RssBlogFeed(BaseRssFeed):
    title = "Blog"
    link = "http://127.0.0.1:8000/blog/"
    description = "Toutes les publications"

    def items(self, publication):
        articles = Article.objects.filter(hidden=False).only(
            'title',
            '_html_description',
            'pub_date'
            )

        author = Value("matt", CharField(max_length=64))
        webmarks = Webmark.objects.only(
            'title',
            '_html_description',
            'pub_date')

        news = New.objects.only(
            'title',
            '_html_description',
            'pub_date')

        return articles.union(webmarks, news).order_by('-pub_date')

Mais j’aimerai que le flux précise l’auteur de l’article. Les webmark et les news n’ont pas d’auteur (ce sont des liens externes)

Donc :

class RssBlogFeed(BaseRssFeed):
    title = "Blog"
    link = "http://127.0.0.1:8000/blog/"
    description = "Dernieres publications"

    def items(self, publication):
        articles = Article.objects.filter(hidden=False).only(
            'title',
            '_html_description',
            'pub_date',
            'author'
            )

        author = Value("default_author", CharField(max_length=64))
        webmarks = Webmark.objects.annotate(author=author).only(
            'title',
            '_html_description',
            'pub_date')

        news = New.objects.only(
            'title',
            '_html_description',
            'pub_date').annotate(author=author) # Résultat identique si annotate après only, ça ne change rien.

        return articles.union(webmarks, news).order_by('-pub_date')

Et là patatra :

django.db.utils.ProgrammingError: ERREUR:  les UNION types text et timestamp with time zone ne peuvent pas correspondre
LINE 1: ...mark"."title", "blog_webmark"."html_description", "blog_webm...

Qu’est-ce que j’ai mal fait ?

Salut,

Est-ce que tu peux donner plus d’informations sur ce que tu utilises, quelle est cette classe BaseRssFeed (s’agit-il de django.contrib.syndication.views.Feed ?), que contiennent tes modèles Article, Webmark et News, qu’est-ce que Value ?

Je ne connais pas du tout Django, mais dans d’autres contextes, UNION entre 2 flux nécessite que les 2 flux aient exactement la même structure (même nombre de champs, et même typage).

Ici, en voyant le code, je pense que tu essaies d’unir ARTICLES (4 colonnes) avec WEBMARKS et NEWS (3 colonnes).

Et en voyant le message d’erreur, on dirait que c’est plus le typage des colonnes que le nombre de colonnes qui est en cause.

Quoi qu’il en soit, c’est cette piste qu’il faut creuser. Et selon moi, ajouter une colonne Author avec un contenu fictif à ton dataframe WEBMARKS et idem pour le dataframe NEWS.

quelle est cette classe BaseRssFeed

C’est juste une classe abstraite pour factoriser un peu les différents types de flux :

class BaseRssFeed(Feed):
    def item_title(self, obj):
        return obj.title

    def item_description(self, obj):
        return obj.description

    def item_pubdate(self, item):
        return item.pub_date

que contiennent tes modèles Article, Webmark et News

Les champs title, _html_description et pub_date sont communs aux trois modèles. Article contient en plus un champ author. Ensuite ils contiennent tous d’autres champs, qui ne nous intéresse pas forcément ici.

Webmark est une liste de liens web dignes d’intéret. News est très proche, mais fonctionne plus comme un fil d’actualités.

class ArticleBase(models.Model):
    """Abstract class for drafts and articles."""

    title = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(null=False, blank=True, unique=True)
    author = models.CharField(max_length=64)
    md_description = MarkdownxField(max_length=256, blank=True)
    _html_description = models.TextField(blank=True,
                                         db_column="html_description",
                                         editable=False)
    md_body = MarkdownxField(blank=True)
    _html_body = models.TextField(blank=True, db_column="html_body",
                                  editable=False)
    creation_date = models.DateTimeField("Creation", auto_now_add=True)
    last_mod_date = models.DateTimeField("Last modification", auto_now=True)
    tags = TaggableManager()
    files = models.ManyToManyField("File", blank=True)

    class Meta:
        abstract = True
        ordering = ['-last_mod_date']

[…]

class Article(ArticleBase):
    """Published articles."""

    pub_date = models.DateTimeField("Publication",
                                    auto_now_add=True)
    hidden = models.BooleanField(default=False)
    comments_deactivated = models.BooleanField(default=False)
    comments_closed = models.BooleanField(default=False)

[…]

class Link(models.Model):
    url = models.URLField(max_length=256)
    title = models.CharField(max_length=256)
    slug = models.SlugField(null=False, blank=True, unique=True)
    md_description = MarkdownxField()
    _html_description = models.TextField(blank=True,
                                         db_column="html_description",
                                         editable=False)
    pub_date = models.DateTimeField("Publication", auto_now_add=True)
    tags = TaggableManager()

    class Meta:
        abstract = True

[…]

class Webmark(Link):
    class Meta(Link.Meta):
        ordering = ["title"]

    def get_absolute_url(self):
        return reverse("blog:webmark_detail", kwargs={"slug": self.slug})

class New(Link):
    bookmarked = models.BooleanField(default=False)
    expiration_date = models.DateTimeField(blank=True)

    class Meta(Link.Meta):
        ordering = ["-pub_date"]

    def save(self, *args, **kwargs):
        if not self.expiration_date:
            self.expiration_date = datetime.datetime.now() + datetime.timedelta(
            days=Config.default_news_storage_duration)
        super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse("blog:new_detail", kwargs={"slug": self.slug})

qu’est-ce que Value

À oui, pardon : il y a un from django.db.models import Value, CharField au début du fichier. Pour être honnête je ne sais pas exactement ce que c’est. J’ai pompé sur les exemple utilisant annotate que j’ai trouvé sur internet.

Je ne connais pas du tout Django, mais dans d’autres contextes, UNION entre 2 flux nécessite que les 2 flux aient exactement la même structure (même nombre de champs, et même typage).

Oui, c’est ça. Le problème vient du champ author qui n’existe pas pour les webmarks et les news. Du coup, j’ai essayé de leur mettre une valeur par défaut avec annotate. L’attribut author est bien ajouté (j’ai fait le test avec un print, mais ça ne lui convient pas pour l’union.

Ici, en voyant le code, je pense que tu essaies d’unir ARTICLES (4 colonnes) avec WEBMARKS et NEWS (3 colonnes).

Exactement. J’aimerai que le visiteur ai la possibilité de s’abonner à toutes les publications du site, indifféremment de leur types, avec un seul flux.

Et en voyant le message d’erreur, on dirait que c’est plus le typage des colonnes que le nombre de colonnes qui est en cause.

Oui, c’est ça que je ne comprends pas. Comme si author avait bien été ajouté.

Et selon moi, ajouter une colonne Author avec un contenu fictif à ton dataframe WEBMARKS et idem pour le dataframe NEWS.

C’est ce qu’est censé faire le annotate

+0 -0

Je rejoins elegance à défaut d’avoir plus d’info sur les classes mères comme le demande entwanne. Il s’agit visiblement d’une opération d’ORM qui se traduit en SQL par

SELECT query1
UNION
SELECT query2
ORDER BY somefield

Et dans ce cas, il faut que query1 et query2 renvoient le même nombre de champs et que ces champs/colonnes soient de même type dans leur ordre d’apparition… Regarde les exemples de w3schools pour en savoir plus sur cette clause.


Édition (nos messages se sont croisés)

Oui, c’est ça. Le problème vient du champ author qui n’existe pas pour les webmarks et les news. Du coup, j’ai essayé de leur mettre une valeur par défaut avec annotate. L’attribut author est bien ajouté (j’ai fait le test avec un print, mais ça ne lui convient pas pour l’union.

Soit j’ai mal lu, mais il n’y a pas de pub_date dans webmarks si ?

Reédition (toujours bien lire avant de répondre)

Les pub_date sont lignes 28–29 et 44.
Par contre, webmarks est trié sur un champ texte (ligne 54) tandis que news est trié sur un champ horodatage (ligne 64) …et il n’arrive pas à réconcilier cela (les ORM ont leurs limites dans le SQL généré ; à coup sûr il fait juste l’union des deux requêtes qui ont chacune leur ordre de tri, alors qu’il faudrait regrouper les tris à la fin)

+0 -0

Soit j’ai mal lu, mais il n’y a pas de pub_date dans webmarks si ?

Si. Webmark hérite de Link

Et dans ce cas, il faut que query1 et query2 renvoient le même nombre de champs et que ces champs/colonnes soient de même type dans leur ordre d’apparition

Oui, oui j’ai bien compris. En fait je me demande si l’ordre d’apparition n’est pas celui du model, en non celui qui est décrit dans only. Mais dans ce cas je ne sais pas trop comment le changer, sauf à supprimer l’héritage. Il n’y a pas un moyen de garantir l’ordre des champs ?

Nos réponses se sont encore croisées.

En fait je me demande si l’ordre d’apparition n’est pas celui du model, en non celui qui est décrit dans only.

1e49ba0eba

C’est exactement cela.

Par contre, webmarks est trié sur un champ texte (ligne 54) tandis que news est trié sur un champ horodatage (ligne 64) …et il n’arrive pas à réconcilier cela (les ORM ont leurs limites dans le SQL généré ; à coup sûr il fait juste l’union des deux requêtes qui ont chacune leur ordre de tri, alors qu’il faudrait regrouper les tris à la fin)

Gil Cot

Le problème est identifié. La solution par contre, je ne sais pas…

Mais dans ce cas je ne sais pas trop comment le changer, sauf à supprimer l’héritage. Il n’y a pas un moyen de garantir l’ordre des champs ?

1e49ba0eba

Actuellement l’ordre de chacun est garanti car contraint par l’héritage. Tu devrais pouvoir réorganiser dans ton flux mais le gros souci est surtout que tu as deux tris (type et noms de colonnes) concurrents… Essaye peut-être un tri sur les deux ? order_by('title,-pub_date')

+0 -0

En fait je me demande si l’ordre d’apparition n’est pas celui du model, en non celui qui est décrit dans only.

— 1e49ba0eba

C’est exactement cela.

 — Gil Cot

Après test, c’est bien un problème d’ordre d’apparition des champs dans les modèles. J’ai changé mis author après last_mod_date dans Article et j’ai modifié mon feed :

class ArticleBase(models.Model):
    """Abstract class for drafts and articles."""

    title = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(null=False, blank=True, unique=True)
    # author = models.CharField(max_length=64)
    md_description = MarkdownxField(max_length=256, blank=True)
    _html_description = models.TextField(blank=True,
                                         db_column="html_description",
                                         editable=False)
    md_body = MarkdownxField(blank=True)
    _html_body = models.TextField(blank=True, db_column="html_body",
                                  editable=False)
    creation_date = models.DateTimeField("Creation", auto_now_add=True)
    last_mod_date = models.DateTimeField("Last modification", auto_now=True)
    # Déplacé après last_mod_date :
    author = models.CharField(max_length=64)
    tags = TaggableManager()
    files = models.ManyToManyField("File", blank=True)
class RssBlogFeed(BaseRssFeed):
    title = "Blog"
    link = "http://127.0.0.1:8000/blog/"
    description = "Dernieres publications"

    def items(self, publication):
        articles = Article.objects.only(
            'title',
            '_html_description',
            'last_mod_date', # changement de champ
            'author').filter(hidden=False)
        author = Value("moi", CharField(max_length=64))
        webmarks = Webmark.objects.only(
            'title',
            '_html_description',
            'pub_date').annotate(author=author).order_by('-pub_date')
        news = New.objects.only(
            'title',
            '_html_description',
            'pub_date').annotate(author=author).order_by('-pub_date')

        return articles.union(webmarks, news).order_by('-last_mod_date')


    def item_author_name(self, item):
        return item.author

    def item_pubdate(self, item):
        return item.last_mod_date

Ça ne plante plus.

Mais…

Lorsque j’ajoute le feed à mon aggrégateur de flux, seuls les articles sont visibles.

Pourtant si je rajoute un print(item) dans def item_author_name(self, item) ou def item_pubdate(self, item), toutes les publications sont passées dans ces fonctions. Je ne comprends pas…

Sinon :

Par contre, webmarks est trié sur un champ texte (ligne 54) tandis que news est trié sur un champ horodatage (ligne 64) …et il n’arrive pas à réconcilier cela (les ORM ont leurs limites dans le SQL généré ; à coup sûr il fait juste l’union des deux requêtes qui ont chacune leur ordre de tri, alors qu’il faudrait regrouper les tris à la fin)

— Gil Cot

Là par contre, je ne te suis pas. Ça c’est l’ordre de tri des entités, pas des champs. Au pire, en admettant que le order_by (ligne 25, premier post) ne fonctionne pas, j’aurai les publications dans le désordre, ce qui n’est pas le cas ici. Ou j’ai mal compris ? Quoi qu’il en soit, j’ai remis -pub_date pour l’ordre de tri, le temps de debugguer mon machin.

Ok, il semblerai que le feed fasse uniquement appel au get_absolute_url de la classe à l’origine de l’union (ici Article.get_absolute_url()). Et les agrégateurs de flux n’acceptent pas plusieurs fois le même item_link.

Bon, je pensais pas que ce serait aussi compliqué de cummuler plusieurs flux…

C’est bon, j’ai trouvé la solution ! C’est très bêtes en fait. Il n’y a pas besoin de faire d’union :

class RssBlogFeed(BaseRssFeed):
    title = "Blog"
    link = "http://127.0.0.1:8000/blog/"
    description = "Dernieres publications"

    def items(self):
        articles = Article.objects.filter(hidden=False)
        webmarks = Webmark.objects.all()
        news = New.objects.all()

        return *articles, *webmarks, *news

    def item_author_name(self, item):
        try:
            return item.author
        except AttributeError:
            return None

    def item_pubdate(self, item):
        return item.last_mod_date

Du coup les items ne sont pas triés, mais de toute manière le tri se fera directement dans l’agrégateur.

Et le order(['title']) n’as aucune importance. Merci de votre aides en tout cas.

+0 -0

Du coup ça oblige la méthode items à charger en mémoire l’ensemble des flux alors que précédemment c’était la DB qui gérait ça.

Tu peux peut-être te débrouiller avec un itertools.chain pour simplement renvoyer un itérateur sur les résultats (à voir si ça ne pose pas de problème avec le curseur DB) mais sinon ça m’étonne qu’il n’y ait pas moyen de calculer l’union de tout cela.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte