Os nodejs do NestJS carregam comentários aninhados em uma consulta com relações?

10

Eu tenho os seguintes modelos:

User, Customer,Comment

O usuário pode comentar em um Customer, o usuário pode responder ao comentário de outro usuário, recursivamente ilimitado.

Fiz isso, mas é limitado a apenas uma resposta e desejo obter todas as respostas NESTED:

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

No entanto, a resposta que recebo é aninhada apenas em um nível:

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

Como posso fazer uma consulta para aninhar todos eles em typeorm?

Definição da entidade (observe o cliente renomeado para 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
fonte
11
Você pode adicionar suas definições de entidade?
zenbeni 9/12/19
@zenbeni Adicionado obrigado
Ben Beri

Respostas:

7

Você está basicamente usando um Adjacency list Tree.

A lista de adjacências é um modelo simples com auto-referência. O benefício dessa abordagem é a simplicidade, MAS a desvantagem é que você não pode lidar com árvores profundas com isso.

Existe uma maneira recursiva de fazê-lo com a lista Adjacency, mas não funciona com o MySQL.

A solução é usar outro tipo de árvore. Outras árvores possíveis são:

  • Conjunto aninhado : é muito eficiente para leituras, mas ruim para gravações. Você não pode ter várias raízes no conjunto aninhado.
  • Caminho materializado : (também chamado de enumeração de caminhos) é simples e eficaz.
  • Tabela de fechamento : armazena as relações entre pai e filho em uma tabela separada. É eficiente nas leituras e gravações (a atualização ou remoção do pai de um componente ainda não foi implementada)
@Entity()
@Tree("nested-set") // or @Tree("materialized-path") or @Tree("closure-table")
export class Category {

    @PrimaryGeneratedColumn()
    id: number;

    @TreeChildren()
    children: Category[];

    @TreeParent()
    parent: Category;
}

Para carregar uma árvore, use:

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

Depois de obter um repositório em árvore, você pode usar as próximas funções: findTrees(), findRoots(), findDescendants(), findDescendantsTree()e outras. Consulte a documentação para obter mais informações.

Saiba mais sobre diferentes tipos de árvores: Modelos para dados hierárquicos

Gabriel Vasile
fonte
1

Como Gabriel disse, outros modelos de dados são melhores para fazer o que você deseja em termos de desempenho. Ainda assim, se você não pode alterar o design do banco de dados, pode usar alternativas (que têm menos desempenho ou são bonitas, mas o que funciona na produção é tudo o que importa no final).

Conforme você define o valor Lead em seu LeadComment, posso sugerir que você defina esse valor também nas respostas no comentário raiz sobre a criação de respostas (deve ser fácil no código). Dessa forma, você pode buscar todos os comentários sobre seu cliente em uma consulta (incluindo as respostas).

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

Obviamente, você precisará executar um lote SQL para preencher os valores de coluna ausentes, mas isso é uma coisa única e, uma vez que sua base de código também esteja corrigida, você não precisará executar nada depois. E isso não altera a estrutura do seu banco de dados (apenas a maneira como os dados são preenchidos).

Em seguida, você pode criar no nodejs todo o material (listas de respostas). Para obter o comentário "raiz", basta filtrar por comentário que não seja uma resposta (que não tenha pais). Se você deseja apenas os comentários raiz do banco de dados, você pode alterar a consulta apenas para esses (com parentComment nulo na coluna 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);

Em seguida, você pode obter respostas nos rootComments e criar a lista inteira recursivamente no nó.

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

Provavelmente existem maneiras mais otimizadas de calcular essas listas; essa é para mim uma das mais simples que podem ser feitas.

Dependendo do número de comentários, ele pode ficar lento (você pode limitar os resultados com carimbo de data e hora e número, por exemplo, para que seja bom o suficiente?). Cuidado, não procure o universo de comentários em um lead "Justin Bieber" que recebe muitos comentários ...

zenbeni
fonte