NestJS nodejs carga comentarios anidados en una consulta con relaciones?

10

Tengo los siguientes modelos:

User` Customer`Comment

El usuario puede comentar sobre un Customer, el usuario puede responder al comentario de otro usuario, recursivamente ilimitado.

He hecho esto, pero está limitado a una sola respuesta, y quiero obtener todas las respuestas NESTED:

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

Sin embargo, la respuesta que obtengo solo está anidada en un nivel:

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

¿Cómo puedo hacer una consulta para anidarlos a todos en typeorm?

Definición de entidad (nota cliente renombrado a 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
fuente
1
¿Puedes agregar tus definiciones de entidad?
zenbeni
@zenbeni Añadido gracias
Ben Beri

Respuestas:

7

Básicamente estás usando un Adjacency list Tree.

La lista de adyacencia es un modelo simple con autorreferencia. El beneficio de este enfoque es la simplicidad, PERO el inconveniente es que no puede manejar árboles profundos con eso.

Hay una forma recursiva de hacerlo con la lista de adyacencia, pero no funciona con MySQL.

La solución es usar otro tipo de árbol. Otros posibles árboles son:

  • Conjunto anidado : es muy eficiente para lecturas, pero malo para escrituras. No puede tener múltiples raíces en un conjunto anidado.
  • Ruta materializada : (también llamada Enumeración de ruta) es simple y efectiva.
  • Tabla de cierre : almacena las relaciones entre padre e hijo en una tabla separada. Es eficiente tanto en lecturas como en escrituras (la actualización o eliminación de un componente principal aún no se ha implementado)
@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 cargar un árbol use:

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

Después de obtener un repositorio de árbol, puede usar las siguientes funciones: findTrees(), findRoots(), findDescendants(), findDescendantsTree()y otras. Ver documentación para más.

Obtenga más información sobre los diferentes tipos de árboles: modelos para datos jerárquicos

Gabriel Vasile
fuente
1

Como dijo Gabriel, otros modelos de datos son mejores para hacer lo que desea en cuanto al rendimiento. Aún así, si no puede cambiar el diseño de la base de datos, puede usar alternativas (que son menos efectivas o bonitas, pero lo que funciona en la producción es todo lo que importa al final).

Al establecer el valor de Lead en su LeadComment, puedo sugerirle que establezca este valor también en las respuestas en el comentario raíz en la creación de respuestas (debería ser fácil en el código). De esta forma, puede obtener todos los comentarios de su cliente en una sola consulta (incluidas las respuestas).

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

Por supuesto, tendrá que ejecutar un lote de SQL para completar los valores de columna que faltan, pero es una cosa única, y una vez que su base de código también esté parchada, no tendrá que ejecutar nada después. Y no cambia la estructura de su base de datos (solo la forma en que se rellenan los datos).

Luego puede construir en nodejs todo el material (listas de respuestas). Para obtener el comentario "raíz", simplemente filtre por comentario que no sean respuestas (que no tengan padres). Si solo desea los comentarios raíz de la base de datos, incluso puede cambiar la consulta a solo estos (con parentComment nulo en la columna 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);

Luego puede obtener respuestas en rootComments y construir la lista completa de forma recursiva en el nodo.

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

Probablemente hay formas más optimizadas para calcular estas listas, esta es para mí una de las más simples que se pueden hacer.

Dependiendo de la cantidad de comentarios, puede ser lento (puede limitar los resultados por marca de tiempo y número, por ejemplo, para que sea lo suficientemente bueno), así que tenga cuidado, no busque el universo de comentarios en un Lead "Justin Bieber" que obtenga muchos comentarios ...

Zenbeni
fuente