• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

One-to-Many Self Referencing 문제

23.08.04 01:22 작성 조회수 252

0

안녕하세요 One-to-Many 관계 정의 시에 해결이 잘 안되서 문의 드립니다.

Typeorm 사이트에서 정보를 참조도 해보고 구글링도 해봐서 시도해봤지만 명확하게 원하던 결과가 나오지 않아서 문의드립니다.

@Entity("Parts", { schema: "workplaces" })
export class Parts {

    @PrimaryColumn('varchar', { name: "partsNumber", unique: true, nullable: false })
    partsNumber: string;

    @Column('varchar', { name: 'partsName', length: 500, nullable: true })
    name: string;

    @Column('bool', { name: 'isMain', nullable: false, default: false })
    isMain: boolean;

    @CreateDateColumn()
    createDate: Date;

    @UpdateDateColumn()
    updateDate: Date;

    @DeleteDateColumn()
    deleteDate: Date | null;

    // 여기부터가 문의 드리는 One to Many 관계설정 코드입니다.
    @ManyToOne(() => Parts, (parts) => parts.alternative, { onDelete: "SET NULL", onUpdate: "CASCADE" })
    parts: Parts;

    @OneToMany(() => Parts, (alternative) => alternative.parts)
    @JoinColumn([{ name: "alternative", referencedColumnName: "partsNumber" }])
    alternative: Parts[];
}

보시면 아시겠지만, Parts 테이블에 alternative란 항목의 다수의 Parts 타입을 데이터로 받도록 구성한 것인데요, 부품의 경우, 대체 부품이 여럿이 존재해서 isMain 항목을 통해서 부품이 main부품일 경우에만 데이터를 저장하고 호출해주며, main 부품이 아닌경우, 연결된 main parts 의 정보를 호출해주도록 하기 위해서 입니다.

실제로 이렇게 구성할 경우, 아래와 같은 데이터를 저장할시

{
    "partsNumber": "341241",
    "name": "341241 Dryer Drum Belt",
    "isMain": true,
    "alternative": [
        { "partsNumber": "14210003" },
        { "partsNumber": "20065315" },
    ]
}

이상없이 저장이 완료됩니다.

Main part(parent) 검색시에 보통 사용하던 One to Many에서처럼 alternative에 제대로 2개의 배열객체가 출력이 됩니다만, 문제는 alternative 데이터를 검색하여 main이 되는 part를 출력하지 못하는 점인데요. Self Referencing 으로 One to Many관계를 설정시에 children측에서 parent 쪽의 정보를 호출하려면 어떻게 설정해야 하나요?

항상 답변을 주셔서 감사합니다!

답변 1

답변을 작성해보세요.

1

parts가 비어있는 상황이신거죠?

alternative 검색 시 typeorm은 어떻게 사용하셨나요?

늦은시간..이라기보다 이른 시간이되겠네요 ;; 답변 감사합니다.

원래 자세히 다 달려고 했는데 너무 내용이 길어져서 줄여서 올리다보니 정보가 부족했군요.

우선 새로운 main part를 입력시엔, 우선 alternative에 해당하는 2객체가 이미 db에 있는지 확인후에 없다면 partsNumber와 isMain(false)만을 가진 빈껍데기 객체를 우선 생성해준 후,이 생성된 Parts타입의 객체의 배열을 새로운 main part의 alternative 값에 할당하여 생성해줬고요. 결과로 업데이트된 Parts 테이블엔 alternative parts 2개 객체, 14210003 20065315 총 3개의 새로운 Parts 가 등록되어있습니다.

여기서 main part인 341241의 partsPartsNumber 항목은 빈칸이고요, 다른 위에 말했던 alternative parts 2객체엔 main part의 partsNumber 값인 341241이 각각 들어있습니다.

따라서 341241의 값을 읽을 시엔

const data = await this.PartsRepository.createQueryBuilder("partsList")
            .leftJoinAndSelect("partsList.alternative", "alternative")
            .where("partsList.partsNumber = :partsNumber", { partsNumber })
            .getOne() 

이렇게 하면 341241의 alternative에 2개의 Parts 객체가 이상없이 출력됩니다.

하지만 db table 안에 있는 partsPartsNumber 를 통해서 연결된 main part가 341241이란걸 어떤 식으로 해야 출력될지를 모르겠네요.

새로운 데이터 입력 코드는 아래와 같습니다. 급히 간추려 보았는데 빠진게 없는듯하네요

async addPartsData(addParts: PartsDto) {

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
        const target = await queryRunner.manager
            .getRepository(Parts)
            .findOne({ where: { partsNumber: addParts.partsNumber } });

        if (target) {
            throw new ForbiddenException("The Parts data are already in our Database!");
        }

        let alternativePartsList: Parts[] = [];

        if (addParts.alternative && addParts.alternative.length) {
            for (let i = 0; i < addParts.alternative.length; i++) {
                let result = await queryRunner.manager.getRepository(Parts).findOne({
                    where: { partsNumber: addParts.alternative[i].partsNumber }
                })
                if (!result) {
                    alternativePartsList.push(await queryRunner.manager.getRepository(Preferences).save(
                        {
                            partsNumber: addParts.alternative[i].partsNumber,
                            isMain: false
                        }
                    ));
                } else {
                    alternativePartsList.push(result);
                }
            }
        }

        const newPart = queryRunner.manager.getRepository(Parts).create();
        newPart.name = addParts.name;
        newPart.partsNumber = addParts.partsNumber;
        newPart.isMain = true;
        newPart.alternative = alternativePartsList;
        await queryRunner.manager.getRepository(Parts).save(newPart);
       
        await queryRunner.commitTransaction();
        return { success: true, message: "Uploading completed!" };

    } catch (error) {
        console.error(error);
        await queryRunner.rollbackTransaction();
        throw new ForbiddenException(error);
    } finally {
        await queryRunner.release();
    }
const data = await this.PartsRepository.createQueryBuilder("partsList")
            .leftJoinAndSelect("partsList.parts", "parts")
            .where("partsList.partsNumber = :partsNumber", { partsNumber })
            .getOne() 

이렇게 불러와보셨나요?

여긴 아침이라.. 자고 일어나보니 답변이 올라와서 좀 전에 확인했습니다.

알려주신대로 하니 제대로 검색이 되네요. 지금 코드에도 저렇게 검색했던 코드가 주석처리되어있는데 왜 안됐었는지 모르겠네요; 아마도 저리 테스트할때 코드를 변경했었나봅니다..

다 시도해본 줄 알고 데이터베이스 설정부터 다시 바꿔보고 하느라 많은 시간을 소모했는데 해결해 주셔서 감사합니다!

추가로 문의 드릴께 있어서 답변드립니다.

워에서 했던 대로 설정해주어서 이젠 문제없이 alternative 항목이 제대로 출력이 되는데요

한가지 문제가 있는게 사실은 Parts Entity엔 alternative 말고도 몇개의 Many To Many와 One to Many 관계를 설정하였는데요.

@Entity("Parts", { schema: "workplaces" })
export class Parts {

    @PrimaryColumn('varchar', { name: "partsNumber", unique: true, nullable: false })
    partsNumber: string;

    @Column('varchar', { name: 'partsName', length: 500, nullable: true })
    name: string;

    @OneToMany(() => Images, (images) => images.preferences)
    images: Images[];

    // 여기부터가 문의 드리는 One to Many 관계설정 코드입니다.
    @ManyToOne(() => Parts, (parts) => parts.alternative, { onDelete: "SET NULL", onUpdate: "CASCADE" })
    parts: Parts;

    @OneToMany(() => Parts, (alternative) => alternative.parts)
    @JoinColumn([{ name: "alternative", referencedColumnName: "partsNumber" }])
    alternative: Parts[];
}

위에서처럼 만약 한개의 부품 데이터가 여러개의 이미지를 가지는 경우를 위해 One to Many 관계로 Images Entity와 연결시켜줬을 시, 앞에서의 쿼리를 실행하면 alternative 부품들이 표시되지만 이 부품들의 이미지는 표시가 안됩니다.

일반적이라면 당연히 아래처럼,

const data = await this.Parts.createQueryBuilder("alternative")

.leftJoinAndSelect("alternative.images", "images")

처럼 불러와야겠지만, alternative 자체로 FK로 다른 테이블의 정보를 불러온 것이다보니,

제대로 출력시킬 쿼리를 작성하는데 어려움을 겪고 있습니다.

물론 어거지로 출력하려면 data.alternative를 각각으로 해서 alternative 리스트들에 한해서 또 직접 select쿼리를 생성해주면 되기야 하겠지만, 제대로된 코드는 아닌듯해서요.

One To Many 관계의 데이터의 또 다른 One To Many항목을, 즉 FK로 읽어들인 table이 가지도 있는 FK로 연결된 또다른 테이블의 정보를 쿼리 할려면 어떻게 해야하나요?

leftJoinAndSelect로 계속 이어서 쓰시면 됩니다.

.leftJoinAndSelect('image.ooo', 'ooo')

말씀해주신대로 해보는데 잘 되지 않아서 다시 문의 드립니다.

우선 Entity는 먼저 말씀드린대로

Parts.ts

@Entity("Parts", { schema: "workplaces" })
export class Parts {

    @PrimaryColumn('varchar', { name: "partsNumber", unique: true, nullable: false })
    partsNumber: string;

    @OneToMany(() => Images, (images) => images.parts)
    images: Images[];

    @ManyToOne(() => Parts, (mainParts) => mainParts.alternative, { onDelete: "SET NULL", onUpdate: "CASCADE" })
    mainParts: Parts;

    @OneToMany(() => Parts, (alternative) => alternative.parts)
    @JoinColumn([{ name: "alternative", referencedColumnName: "partsNumber" }])
    alternative: Parts[];
}

Images.ts

@Entity("Images", { schema: "workplaces" })
export class Images {

    @PrimaryGeneratedColumn({ type: "int", name: "imageID" })
    id: number;

    @Column('varchar', { name: 'image', nullable: true })
    image: string;

    @Column('varchar', { name: 'partsNumber', nullable: true })
    partsNumber: string;

    @ManyToOne(() => Parts, (parts) => parts.images, { onDelete: "SET NULL", onUpdate: "CASCADE" })
    @JoinColumn([{ name: 'partsNumber', referencedColumnName: 'partsNumber' }])
    parts: Parts;

}

이렇게 Entities를 구성하여 Part는 self referencing과 Images는 One To Many로 관계설정이 되어 있습니다.

그리고 앞에서 문의 했던대로

const data = await this.PartsRepository.createQueryBuilder("partsList")
            .leftJoinAndSelect("partsList.alternative", "alternative")
            .leftJoinAndSelect("partsList.mainParts", "mainParts")
            .leftJoinAndSelect("partsList.images", "images").leftJoinAndSelect("partsList.parts", "parts")
            .where("partsList.partsNumber = :partsNumber", { partsNumber })
            .getOne() 

로 query를 작성하면,

{
    "partsNumber": "testpart123",
    "images": [
        {
            "id": 1,
            "image": "https://www.testParts.com/thumbnail/testpart123.jpg",
            "partsNumber": "testpart123"
        }
    ],
    "mainPart": [],
    "alternative": [
        {
            "partsNumber": "alternativePart123"
        }
    ]
}

이렇게 출력됩니다. 하지만 원하는 결과는 사실

{
    "partsNumber": "testpart123",
    "images": [
        {
            "id": 1,
            "image": "https://www.testParts.com/thumbnail/testpart123.jpg",
            "partsNumber": "testpart123"
        }
    ],
    "mainPart": [],
    "alternative": [
        {
            "partsNumber": "alternativePart123",
            "images": [
                {
                    "id": 2,
                    "image": "https://www.testParts.com/thumbnail/alternativePart123.jpg",
                    "partsNumber": "alternativePart123"
                }
            ],
        }
    ]
}

이렇게 Self-referencing 된 Parts 타입 데이터 안에도 One To Many로 관계지어진 images 데이터를 표기하고 싶은데 잘 되지 않아서 문의드렸습니다.

.leftJoinAndSelect("partsList.alternatives.images", "images") 이런식의 의미인데 코드로 어떻게 작성해야 될지 모르겠네요.

말씀해주신대로 .leftJoinAndSelect("images.xxx", "xxx")로 어떻게 해야되는지 추가해봤는데 제대로 출력되지 않습니다. 제가 Entities 구성을할때 추가해줘야할 것을 빼 먹은 것이 있는건가요?

그냥 계속 이어서 쓰면 되는데 어떤 점이 어려우신건지를 모르겠습니다. partsList.alternative, alternative(alias) 하셨으면 alias 활용해서 alternative.images, altImages(alias) 하시면 되죠.

.leftJoinAndSelect("alternative.images", "altImages")

정말 빠른 답변 감사합니다. 어디선가 db 리뉴얼될때 오류가 있었었나 다시 리셋하고 재생성하니 오류없이 작동하는군요 ;; 감사합니다

좋은 하루 되세요!