NestJS nodejs charge les commentaires imbriqués dans une requête avec des relations?

10

J'ai les modèles suivants:

User, Customer,Comment

L'utilisateur peut commenter un Customer, l'utilisateur peut répondre au commentaire d'un autre utilisateur, récursivement illimité.

Je l'ai fait mais c'est limité à une seule réponse, et je veux obtenir toutes les réponses IMPRIMÉ:

public async getCommentsForCustomerId(customerId: string): Promise<CustomerComment[]> {
    return this.find({where: {customer: {id: customerId}, parentComment: null}, relations: ['childComments']});
}

Cependant, la réponse que j'obtiens n'est imbriquée qu'à un seul niveau:

[
    {
        "id": "7b5b654a-efb0-4afa-82ee-c00c38725072",
        "content": "test",
        "created_at": "2019-12-03T15:14:48.000Z",
        "updated_at": "2019-12-03T15:14:49.000Z",
        "childComments": [
            {
                "id": "7b5b654a-efb0-4afa-82ee-c00c38725073",
                "content": "test reply",
                "created_at": "2019-12-03T15:14:48.000Z",
                "updated_at": "2019-12-03T15:14:49.000Z",
                "parentCommentId": "7b5b654a-efb0-4afa-82ee-c00c38725072"
            }
        ]
    }
]

Comment puis-je faire une requête pour les imbriquer tous dans typeorm?

Définition d'entité (notez que le client a été renommé Lead) :

@Entity('leads_comments')
export class LeadComment {

  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(type => LeadComment, comment => comment.childComments, {nullable: true})
  parentComment: LeadComment;

  @OneToMany(type => LeadComment, comment => comment.parentComment)
  @JoinColumn({name: 'parentCommentId'})
  childComments: LeadComment[];

  @RelationId((comment: LeadComment) => comment.parentComment)
  parentCommentId: string;

  @ManyToOne(type => User, {cascade: true})
  user: User | string;

  @RelationId((comment: LeadComment) => comment.user, )
  userId: string;

  @ManyToOne(type => Lead, lead => lead.comments, {cascade: true})
  lead: Lead | string;

  @RelationId((comment: LeadComment) => comment.lead)
  leadId: string;

  @Column('varchar')
  content: string;

  @CreateDateColumn()
  created_at: Date;

  @UpdateDateColumn()
  updated_at: Date;
}
Ben Beri
la source
1
Pouvez-vous ajouter vos définitions d'entité?
zenbeni
@zenbeni Ajouté merci
Ben Beri

Réponses:

7

Vous utilisez essentiellement un Adjacency list Tree.

La liste d'adjacence est un modèle simple avec auto-référencement. L'avantage de cette approche est la simplicité, MAIS l'inconvénient est que vous ne pouvez pas gérer les arbres profonds avec cela.

Il existe une manière récursive de le faire avec la liste d'adjacence, mais cela ne fonctionne pas avec MySQL.

La solution consiste à utiliser un autre type d'arbre. D'autres arbres possibles sont:

  • Ensemble imbriqué : son très efficace pour les lectures, mais mauvais pour les écritures. Vous ne pouvez pas avoir plusieurs racines dans un ensemble imbriqué.
  • Chemin matérialisé : (également appelé Énumération du chemin) est simple et efficace.
  • Table de fermeture : stocke les relations entre parent et enfant dans une table séparée. Est efficace à la fois en lecture et en écriture (la mise à jour ou la suppression du parent d'un composant n'a pas encore été implémentée)
@Entity()
@Tree("nested-set") // or @Tree("materialized-path") or @Tree("closure-table")
export class Category {

    @PrimaryGeneratedColumn()
    id: number;

    @TreeChildren()
    children: Category[];

    @TreeParent()
    parent: Category;
}

Pour charger un arbre, utilisez:

const manager = getManager();
const trees = await manager.getTreeRepository(Category).findTrees();

Après avoir obtenu un référentiel arborescent, vous pouvez utiliser les fonctions suivantes: findTrees(), findRoots(), findDescendants(), findDescendantsTree()et d'autres. Consultez la documentation pour en savoir plus.

En savoir plus sur les différents types d'arbres: modèles de données hiérarchiques

Gabriel Vasile
la source
1

Comme Gabriel l'a dit, les autres modèles de données font mieux ce que vous voulez en termes de performances. Néanmoins, si vous ne pouvez pas modifier la conception de la base de données, vous pouvez utiliser des alternatives (qui sont moins performantes ou jolies, mais ce qui fonctionne en production est tout ce qui compte au final).

Lorsque vous définissez la valeur Lead dans votre LeadComment, je peux vous suggérer de définir cette valeur également sur les réponses sur le commentaire racine sur la création de la réponse (cela devrait être facile dans le code). De cette façon, vous pouvez récupérer tous les commentaires sur votre client en une seule requête (y compris les réponses).

const lead = await leadRepository.findOne(id);
const comments = await commentRepository.find({lead});

Bien sûr, vous devrez exécuter un lot SQL pour remplir les valeurs de colonne manquantes, mais c'est une chose unique, et une fois votre base de code corrigée, vous n'aurez plus rien à exécuter par la suite. Et cela ne change pas la structure de votre base de données (juste la façon dont les données sont remplies).

Ensuite, vous pouvez construire dans nodejs le tout (listes de réponses). Pour obtenir le commentaire "racine", filtrez simplement par commentaire qui ne sont pas des réponses (qui n'ont pas de parents). Si vous voulez simplement les commentaires racine de la base de données, vous pouvez même modifier la requête uniquement à ceux-ci (avec parentComment null dans la colonne SQL).

function sortComment(c1: LeadComment , c2: LeadComment ): number {
    if (c1.created_at.getTime() > c2.created_at.getTime()) {
    return 1;
    }
    if (c1.created_at.getTime() < c2.created_at.getTime()) {
        return -1;
    }
    return 0;
}
const rootComments = comments
    .filter(c => !c.parentComment)
    .sort(sortComment);

Ensuite, vous pouvez obtenir des réponses sur rootComments et créer la liste entière de manière récursive dans le nœud.

function buildCommentList(currentList: LeadComment[], allComments: LeadComment[]): LeadComment[] {
    const lastComment = currentList[currentList.length - 1];
    const childComments = allComments
        .filter(c => c.parentComment?.id === lastComment.id)
        .sort(sortComment);
    if (childComments.length === 0) {
        return currentList;
    }
    const childLists = childComments.flatMap(c => buildCommentList([c], allComments));
    return [...currentList, ...childLists];
}

const listsOfComments = rootComments.map(r => buildCommentList([r], comments));

Il existe probablement des moyens plus optimisés pour calculer ces listes, c'est pour moi l'une des plus simples qui puisse être faite.

En fonction du nombre de commentaires, cela peut devenir lent (vous pouvez limiter les résultats par horodatage et nombre par exemple pour qu'il soit suffisant?) Alors attention, ne récupérez pas l'univers des commentaires sur un Lead "Justin Bieber" beaucoup de commentaires ...

Zenbeni
la source