NestJS-Knoten laden verschachtelte Kommentare in eine Abfrage mit Beziehungen?

10

Ich habe folgende Modelle:

User, Customer,Comment

Der Benutzer kann einen Kommentar abgeben Customer, der Benutzer kann rekursiv unbegrenzt auf den Kommentar eines anderen Benutzers antworten.

Ich habe dies getan, aber es ist auf nur eine Antwort beschränkt, und ich möchte alle Antworten NESTED erhalten:

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

Die Antwort, die ich erhalte, ist jedoch nur auf einer Ebene verschachtelt:

[
    {
        "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"
            }
        ]
    }
]

Wie kann ich eine Abfrage durchführen, um sie alle in Typorm zu verschachteln?

Entitätsdefinition (Kunden in Lead umbenannt) :

@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
quelle
1
Können Sie Ihre Entitätsdefinitionen hinzufügen?
Zenbeni
@zenbeni Hinzugefügt danke
Ben Beri

Antworten:

7

Sie verwenden im Grunde eine Adjacency list Tree.

Die Adjazenzliste ist ein einfaches Modell mit Selbstreferenzierung. Der Vorteil dieses Ansatzes ist die Einfachheit, ABER der Nachteil ist, dass Sie damit nicht mit tiefen Bäumen umgehen können.

Es gibt eine rekursive Möglichkeit, dies mit der Adjacency-Liste zu tun, aber es funktioniert nicht mit MySQL.

Die Lösung besteht darin, einen anderen Baumtyp zu verwenden. Andere mögliche Bäume sind:

  • Verschachtelte Menge : Es ist sehr effizient für Lesevorgänge, aber schlecht für Schreibvorgänge. Sie können nicht mehrere Wurzeln in verschachtelten Mengen haben.
  • Materialisierter Pfad : (auch Pfadaufzählung genannt) ist einfach und effektiv.
  • Schließungstabelle : Speichert die Beziehungen zwischen Eltern und Kind in einer separaten Tabelle. Ist sowohl beim Lesen als auch beim Schreiben effizient (das Aktualisieren oder Entfernen des übergeordneten Elements einer Komponente wurde noch nicht implementiert)
@Entity()
@Tree("nested-set") // or @Tree("materialized-path") or @Tree("closure-table")
export class Category {

    @PrimaryGeneratedColumn()
    id: number;

    @TreeChildren()
    children: Category[];

    @TreeParent()
    parent: Category;
}

Um einen Baum zu laden, verwenden Sie:

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

Nachdem Sie ein Baum-Repository erhalten haben, können Sie die nächsten Funktionen verwenden: findTrees(), findRoots(), findDescendants(), findDescendantsTree()und andere. Weitere Informationen finden Sie in der Dokumentation .

Erfahren Sie mehr über verschiedene Baumarten: Modelle für hierarchische Daten

Gabriel Vasile
quelle
1

Wie Gabriel sagte, sind andere Datenmodelle besser, um die Leistung zu erbringen, die Sie möchten. Wenn Sie das Datenbankdesign nicht ändern können, können Sie Alternativen verwenden (die weniger performant oder hübsch sind, aber was in der Produktion funktioniert, ist am Ende alles, was zählt).

Wenn Sie den Lead-Wert in Ihrem LeadComment festlegen, kann ich vorschlagen, dass Sie diesen Wert auch für Antworten auf den Stammkommentar bei der Antworterstellung festlegen (sollte im Code einfach sein). Auf diese Weise können Sie alle Kommentare zu Ihrem Kunden in einer Abfrage abrufen (einschließlich der Antworten).

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

Natürlich müssen Sie einen SQL-Stapel ausführen, um die fehlenden Spaltenwerte zu füllen, aber es ist eine einmalige Sache, und sobald auch Ihre Codebasis gepatcht ist, müssen Sie danach nichts mehr ausführen. Und es ändert nichts an der Struktur Ihrer Datenbank (nur an der Art und Weise, wie Daten gefüllt werden).

Dann können Sie Nodejs das ganze Zeug (Listen der Antworten) einbauen. Um den "Root" -Kommentar zu erhalten, filtern Sie einfach nach Kommentaren, die keine Antworten sind (die keine Eltern haben). Wenn Sie nur die Stammkommentare aus der Datenbank möchten, können Sie die Abfrage sogar nur in diese ändern (mit parentComment null in der SQL-Spalte).

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);

Dann können Sie Antworten auf die rootComments erhalten und die gesamte Liste rekursiv im Knoten erstellen.

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));

Es gibt wahrscheinlich optimierte Möglichkeiten, diese Listen zu berechnen. Dies ist für mich eine der einfachsten, die gemacht werden können.

Abhängig von der Anzahl der Kommentare kann es langsam werden (Sie können die Ergebnisse beispielsweise durch Zeitstempel und Anzahl begrenzen, damit sie gut genug sind?). Achten Sie also darauf, dass Sie nicht das Universum der Kommentare zu einem "Justin Bieber" -Lead abrufen, der erhalten wird viele Kommentare ...

Zenbeni
quelle