Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Relacje w bazie danych - więzy między legionariuszami

Witajcie, zarządcy danych! Konsul Caesar.js zauważył, że nasz legion rośnie, a tributy mnożą się w tempie balisty. Nadszedł czas, aby nauczyć się łączyć różne dane tak, jak legioniści łączą się w sojusze i kohorty!

Czym są relacje w świecie legionariuszów?

Wybraź sobie, że każdy legionariusz ma swojego centuriona, każdy cohort ma legion, a każdy tribut należy do konkretnego legionariusza. To wszystko to relacje - połączenia między różnymi obiektami w naszej rzymskim skarbcu danych.

TypeORM pozwala nam tworzyć cztery rodzaje relacji:

  • One-to-One (1:1) - jak legionariusz i jego mapa tributy
  • One-to-Many (1:N) - jak centurion i jego legion
  • Many-to-One (N:1) - jak wielu legionariuszów na jednej cohorcie
  • Many-to-Many (N:N) - jak legioniści i ich umiejętności

One-to-One: Legionariusz i jego osobista mapa tributy

Każdy prawdziwy legionariusz ma swoją osobistą, tajną mapę najcenniejszego tributy:

1// tribute-map.entity.ts
2import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
3import { Legionary } from './legionary.entity';
4
5@Entity('tribute_maps')
6export class TributeMap {
7 @PrimaryGeneratedColumn()
8 id: number;
9
10 @Column()
11 islandName: string;
12
13 @Column('decimal', { precision: 10, scale: 6 })
14 latitude: number;
15
16 @Column('decimal', { precision: 10, scale: 6 })
17 longitude: number;
18
19 @Column('text')
20 instructions: string;
21
22 @Column({ default: false })
23 isFound: boolean;
24
25 @OneToOne(() => Legionary, legionary => legionary.tributeMap)
26 @JoinColumn() // Kolumna z foreign key
27 legionary: Legionary;
28}
1// legionary.entity.ts (aktualizacja)
2import { OneToOne } from 'typeorm';
3import { TributeMap } from './tribute-map.entity';
4
5@Entity('legionaries')
6export class Legionary {
7 // ... poprzednie pola
8
9 @OneToOne(() => TributeMap, tributeMap => tributeMap.legionary)
10 tributeMap: TributeMap;
11}

One-to-Many: Centurion i jego legion

Jeden centurion może mieć wielu legionariuszów pod komendą:

1// centurion.entity.ts
2import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
3import { Legionary } from './legionary.entity';
4
5@Entity('centurions')
6export class Centurion {
7 @PrimaryGeneratedColumn()
8 id: number;
9
10 @Column({ length: 100 })
11 name: string;
12
13 @Column({ length: 100 })
14 legionName: string;
15
16 @Column('int')
17 yearsOfExperience: number;
18
19 @Column('decimal', { precision: 15, scale: 2 })
20 totalTributeValue: number;
21
22 @OneToMany(() => Legionary, legionary => legionary.centurion)
23 legion: Legionary[];
24}
1// legionary.entity.ts (aktualizacja)
2import { ManyToOne, JoinColumn } from 'typeorm';
3import { Centurion } from './centurion.entity';
4
5@Entity('legionaries')
6export class Legionary {
7 // ... poprzednie pola
8
9 @ManyToOne(() => Centurion, centurion => centurion.legion)
10 @JoinColumn({ name: 'centurion_id' })
11 centurion: Centurion;
12}

Many-to-Many: Barbarzyńcy i ich umiejętności

Barbarzyńcy mogą mieć wiele umiejętności, a każda umiejętność może być opanowana przez wielu legionariuszów:

1// skill.entity.ts
2import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
3import { Legionary } from './legionary.entity';
4
5@Entity('skills')
6export class Skill {
7 @PrimaryGeneratedColumn()
8 id: number;
9
10 @Column({ length: 50, unique: true })
11 name: string;
12
13 @Column('text')
14 description: string;
15
16 @Column({ type: 'enum', enum: ['Basic', 'Intermediate', 'Advanced', 'Master'] })
17 difficulty: string;
18
19 @ManyToMany(() => Legionary, legionary => legionary.skills)
20 legionaries: Legionary[];
21}
1// legionary.entity.ts (aktualizacja)
2import { ManyToMany, JoinTable } from 'typeorm';
3import { Skill } from './skill.entity';
4
5@Entity('legionaries')
6export class Legionary {
7 // ... poprzednie pola
8
9 @ManyToMany(() => Skill, skill => skill.legionaries)
10 @JoinTable({
11 name: 'legionariusze_skills', // Nazwa tabeli pośredniej
12 joinColumn: { name: 'legionariusze_id', referencedColumnName: 'id' },
13 inverseJoinColumn: { name: 'skill_id', referencedColumnName: 'id' }
14 })
15 skills: Skill[];
16}

Załadowanie relacji - wyszukiwanie połączonych tributów

Eager Loading - automatyczne ładowanie

1// Automatyczne ładowanie przy każdym zapytaniu
2@OneToMany(() => Legionary, legionary => legionary.centurion, { eager: true })
3legion: Legionary[];

Lazy Loading - ładowanie na żądanie

1// Ładowanie tylko gdy potrzebne
2@OneToMany(() => Legionary, legionary => legionary.centurion)
3legion: Promise<Legionary[]>;
4
5// Użycie:
6const centurion = await centurionRepository.findOne({ where: { id: 1 } });
7const legion = await centurion.legion; // Dopiero teraz ładuje dane

Explicit Loading - jawne ładowanie z relations

1// legionary.service.ts
2@Injectable()
3export class LegionaryService {
4 constructor(
5 @InjectRepository(Legionary)
6 private legionariuszeRepository: Repository<Legionary>,
7 @InjectRepository(Centurion)
8 private centurionRepository: Repository<Centurion>,
9 ) {}
10
11 async findLegionaryWithAllData(id: number): Promise<Legionary> {
12 return await this.legionariuszeRepository.findOne({
13 where: { id },
14 relations: {
15 centurion: true,
16 tributeMap: true,
17 skills: true
18 }
19 });
20 }
21
22 async findCenturionWithLegion(id: number): Promise<Centurion> {
23 return await this.centurionRepository.findOne({
24 where: { id },
25 relations: {
26 legion: {
27 skills: true, // Załaduj również umiejętności każdego legionariusza
28 tributeMap: true
29 }
30 }
31 });
32 }
33
34 async findLegionarysWithSkills(): Promise<Legionary[]> {
35 return await this.legionariuszeRepository.find({
36 relations: ['skills', 'centurion'],
37 order: {
38 name: 'ASC',
39 skills: {
40 name: 'ASC'
41 }
42 }
43 });
44 }
45}

Query Builder - zaawansowane wyszukiwanie tributów

1@Injectable()
2export class AdvancedLegionaryService {
3 constructor(
4 @InjectRepository(Legionary)
5 private legionariuszeRepository: Repository<Legionary>,
6 ) {}
7
8 async findLegionarysWithMostTributes(): Promise<Legionary[]> {
9 return await this.legionariuszeRepository
10 .createQueryBuilder('legionariusze')
11 .leftJoinAndSelect('legionary.centurion', 'centurion')
12 .leftJoinAndSelect('legionary.skills', 'skill')
13 .where('legionary.tributeCount > :minTributes', { minTributes: 10 })
14 .orderBy('legionary.tributeCount', 'DESC')
15 .limit(10)
16 .getMany();
17 }
18
19 async findCenturionsWithLargeLegion(): Promise<Centurion[]> {
20 return await this.centurionRepository
21 .createQueryBuilder('centurion')
22 .leftJoinAndSelect('centurion.legion', 'legionariusze')
23 .having('COUNT(legionary.id) >= :minLegionSize', { minLegionSize: 5 })
24 .groupBy('centurion.id')
25 .orderBy('COUNT(legionary.id)', 'DESC')
26 .getMany();
27 }
28
29 async findSkillStatistics() {
30 return await this.legionariuszeRepository
31 .createQueryBuilder('legionariusze')
32 .leftJoin('legionary.skills', 'skill')
33 .select('skill.name', 'skillName')
34 .addSelect('COUNT(legionary.id)', 'legionariuszeCount')
35 .addSelect('AVG(legionary.tributeCount)', 'avgTributes')
36 .groupBy('skill.name')
37 .orderBy('legionariuszeCount', 'DESC')
38 .getRawMany();
39 }
40}

Kaskadowe operacje - automatyczne akcje

1@Entity('centurions')
2export class Centurion {
3 // ... inne pola
4
5 @OneToMany(() => Legionary, legionary => legionary.centurion, {
6 cascade: ['insert', 'update'], // Automatycznie zapisz/aktualizuj legionariuszów
7 onDelete: 'SET NULL' // Gdy centurion znika, legioniści zostają bez centuriona
8 })
9 legion: Legionary[];
10}
11
12@Entity('legionaries')
13export class Legionary {
14 // ... inne pola
15
16 @OneToOne(() => TributeMap, tributeMap => tributeMap.legionariusze, {
17 cascade: true, // Wszystkie operacje
18 onDelete: 'CASCADE' // Gdy legionariusz znika, mapa też
19 })
20 tributeMap: TributeMap;
21
22 @ManyToMany(() => Skill, skill => skill.legionaries, {
23 cascade: ['insert'] // Tylko przy dodawaniu nowych umiejętności
24 })
25 @JoinTable()
26 skills: Skill[];
27}

Operacje na relacjach

1@Injectable()
2export class RelationService {
3 constructor(
4 @InjectRepository(Legionary)
5 private legionariuszeRepository: Repository<Legionary>,
6 @InjectRepository(Centurion)
7 private centurionRepository: Repository<Centurion>,
8 @InjectRepository(Skill)
9 private skillRepository: Repository<Skill>,
10 ) {}
11
12 async assignLegionaryToCenturion(legionariuszeId: number, centurionId: number): Promise<Legionary> {
13 const legionariusze = await this.legionariuszeRepository.findOne({ where: { id: legionariuszeId } });
14 const centurion = await this.centurionRepository.findOne({ where: { id: centurionId } });
15 
16 legionary.centurion = centurion;
17 return await this.legionariuszeRepository.save(legionariusze);
18 }
19
20 async addSkillToLegionary(legionariuszeId: number, skillId: number): Promise<Legionary> {
21 const legionariusze = await this.legionariuszeRepository.findOne({
22 where: { id: legionariuszeId },
23 relations: ['skills']
24 });
25 const skill = await this.skillRepository.findOne({ where: { id: skillId } });
26 
27 if (!legionary.skills.some(s => s.id === skill.id)) {
28 legionary.skills.push(skill);
29 return await this.legionariuszeRepository.save(legionariusze);
30 }
31 
32 return legionariusze;
33 }
34
35 async removeSkillFromLegionary(legionariuszeId: number, skillId: number): Promise<Legionary> {
36 const legionariusze = await this.legionariuszeRepository.findOne({
37 where: { id: legionariuszeId },
38 relations: ['skills']
39 });
40 
41 legionary.skills = legionary.skills.filter(skill => skill.id !== skillId);
42 return await this.legionariuszeRepository.save(legionariusze);
43 }
44
45 async promoteToCapitan(legionariuszeId: number, legionName: string): Promise<Centurion> {
46 const legionariusze = await this.legionariuszeRepository.findOne({ where: { id: legionariuszeId } });
47 
48 const newCenturion = this.centurionRepository.create({
49 name: legionary.name,
50 legionName: legionName,
51 yearsOfExperience: 0,
52 totalTributeValue: legionary.totalTributeValue
53 });
54 
55 const centurion = await this.centurionRepository.save(newCenturion);
56 
57 // Usuń legionariusza z legionu
58 await this.legionariuszeRepository.remove(legionariusze);
59 
60 return centurion;
61 }
62}

Zaawansowane relacje z dodatkowymi danymi

Czasami potrzebujemy dodatkowych informacji w relacji many-to-many:

1// legionariusze-skill.entity.ts - tabela pośrednia z dodatkowymi danymi
2import { Entity, PrimaryColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
3import { Legionary } from './legionary.entity';
4import { Skill } from './skill.entity';
5
6@Entity('legionariusze_skills')
7export class LegionarySkill {
8 @PrimaryColumn()
9 legionariuszeId: number;
10
11 @PrimaryColumn()
12 skillId: number;
13
14 @Column({ type: 'int', default: 1 })
15 level: number; // 1-10
16
17 @Column({ type: 'date' })
18 learnedAt: Date;
19
20 @Column({ type: 'int', default: 0 })
21 practiceHours: number;
22
23 @ManyToOne(() => Legionary, legionary => legionary.legionariuszeSkills)
24 @JoinColumn({ name: 'legionariuszeId' })
25 legionary: Legionary;
26
27 @ManyToOne(() => Skill, skill => skill.legionariuszeSkills)
28 @JoinColumn({ name: 'skillId' })
29 skill: Skill;
30}
1// Aktualizacja entities
2@Entity('legionaries')
3export class Legionary {
4 // ... inne pola
5
6 @OneToMany(() => LegionarySkill, legionariuszeSkill => legionariuszeSkill.legionariusze)
7 legionariuszeSkills: LegionarySkill[];
8}
9
10@Entity('skills')
11export class Skill {
12 // ... inne pola
13
14 @OneToMany(() => LegionarySkill, legionariuszeSkill => legionariuszeSkill.skill)
15 legionariuszeSkills: LegionarySkill[];
16}

Wydajność relacji

Problem N+1

1// ZŁY przykład - N+1 zapytania
2async getBadCenturionsWithLegion(): Promise<any[]> {
3 const centurions = await this.centurionRepository.find(); // 1 zapytanie
4 
5 const result = [];
6 for (const centurion of centurions) {
7 const legion = await this.legionariuszeRepository.find({ 
8 where: { centurion: { id: centurion.id } } 
9 }); // N zapytań!
10 result.push({ centurion, legionCount: legion.length });
11 }
12 
13 return result;
14}
15
16// DOBRY przykład - jedno zapytanie
17async getGoodCenturionsWithLegion(): Promise<any[]> {
18 return await this.centurionRepository
19 .createQueryBuilder('centurion')
20 .leftJoinAndSelect('centurion.legion', 'legionariusze')
21 .getMany(); // 1 zapytanie
22}

Indeksowanie kluczy obcych

1@Entity('legionaries')
2@Index(['centurionId']) // Indeks na kluczu obcym
3export class Legionary {
4 @ManyToOne(() => Centurion, centurion => centurion.legion)
5 @JoinColumn({ name: 'centurion_id' })
6 centurion: Centurion;
7}

Praktyczne ćwiczenie

Spróbuj stworzyć nowe relacje:

  1. Quest (misja) - One-to-Many z Legionary
  2. Island - Many-to-Many z Legionary (odwiedzone wyspy)
  3. Tribute - Many-to-One z Legionary, One-to-One z TributeMap
1// Twój kod tutaj
2@Entity('quests')
3export class Quest {
4 // Zaimplementuj relacje
5}
6
7@Entity('islands')
8export class Island {
9 // Zaimplementuj relacje Many-to-Many
10}
11
12@Entity('tributes')
13export class Tribute {
14 // Połącz z Legionary i TributeMap
15}

Podsumowanie

Gratulacje! Teraz jesteś prawdziwym mistrzem relacji rzymskich! Umiesz:

Tworzyć wszystkie typy relacji między entities ✓ Ładować połączone dane na różne sposoby ✓ Używać Query Builder do złożonych zapytań ✓ Zarządzać kaskadowymi operacjamiOptymalizować wydajność relacyjnych zapytań

Teraz Twój rzymski legion może być w pełni zorganizowany, a tributy odpowiednio skatalogowane!

Vai a CodeWorlds