Witajcie, mistrzowie tributów! Konsul Caesar.js ma dla was nowe wyzwanie - nauczycie się Query Builder, najpotężniejszego narzędzia do przeszukiwania tributów w całym rzymskim Imperium. To jak magiczna lupa, która pozwala znaleźć dokładnie to, czego szukasz!
Query Builder w TypeORM to jak doświadczony kartograf, który potrafi narysować dokładną mapę prowadzącą do każdego tributy. Zamiast pisać surowe zapytania SQL, używamy metod TypeScript, które są bardziej bezpieczne i czytelne.
1// Zamiast tego:
2const tributes = await connection.query(
3 'SELECT * FROM tributes WHERE value > $1 ORDER BY value DESC',
4 [1000]
5);
6
7// Używamy tego:
8const tributes = await tributeRepository
9 .createQueryBuilder('tribute')
10 .where('tribute.value > :minValue', { minValue: 1000 })
11 .orderBy('tribute.value', 'DESC')
12 .getMany();1// tribute.service.ts
2import { Injectable } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5import { Tribute } from './tribute.entity';
6
7@Injectable()
8export class TributeQueryService {
9 constructor(
10 @InjectRepository(Tribute)
11 private tributeRepository: Repository<Tribute>,
12 ) {}
13
14 // Znajdź tributy według wartości
15 async findByValueRange(minValue: number, maxValue: number): Promise<Tribute[]> {
16 return this.tributeRepository
17 .createQueryBuilder('tribute')
18 .where('tribute.value >= :minValue', { minValue })
19 .andWhere('tribute.value <= :maxValue', { maxValue })
20 .orderBy('tribute.value', 'DESC')
21 .getMany();
22 }
23
24 // Wyszukiwanie tekstowe
25 async searchByName(searchTerm: string): Promise<Tribute[]> {
26 return this.tributeRepository
27 .createQueryBuilder('tribute')
28 .where('tribute.name ILIKE :search', { search: `%${searchTerm}%` })
29 .orWhere('tribute.description ILIKE :search', { search: `%${searchTerm}%` })
30 .orderBy('tribute.collectedAt', 'DESC')
31 .getMany();
32 }
33
34 // Znajdź tributy odkryte w ostatnim czasie
35 async findRecentlyDiscovered(daysAgo: number): Promise<Tribute[]> {
36 const cutoffDate = new Date();
37 cutoffDate.setDate(cutoffDate.getDate() - daysAgo);
38
39 return this.tributeRepository
40 .createQueryBuilder('tribute')
41 .where('tribute.collectedAt >= :cutoffDate', { cutoffDate })
42 .orderBy('tribute.collectedAt', 'DESC')
43 .getMany();
44 }
45}1// Znajdź tributy z informacjami o właścicielach
2async findTributesWithOwners(): Promise<any[]> {
3 return this.tributeRepository
4 .createQueryBuilder('tribute')
5 .leftJoinAndSelect('tribute.legionariusze', 'legionariusze')
6 .leftJoinAndSelect('legionariusze.centurion', 'centurion')
7 .where('legionariusze.isActive = :isActive', { isActive: true })
8 .orderBy('tribute.value', 'DESC')
9 .getMany();
10}
11
12// Znajdź legionariuszów z ich tributami
13async findLegionarysWithTributes(): Promise<any[]> {
14 return this.legionariuszeRepository
15 .createQueryBuilder('legionariusze')
16 .leftJoinAndSelect('legionariusze.tributes', 'tribute')
17 .leftJoinAndSelect('legionariusze.centurion', 'centurion')
18 .where('legionariusze.tributeCount > :minTributes', { minTributes: 5 })
19 .orderBy('legionariusze.totalTributeValue', 'DESC')
20 .getMany();
21}
22
23// Inner join - tylko legioniści z centurionami
24async findLegionarysWithCenturions(): Promise<any[]> {
25 return this.legionariuszeRepository
26 .createQueryBuilder('legionariusze')
27 .innerJoinAndSelect('legionariusze.centurion', 'centurion')
28 .innerJoinAndSelect('legionariusze.tributes', 'tribute')
29 .orderBy('centurion.name', 'ASC')
30 .addOrderBy('legionariusze.name', 'ASC')
31 .getMany();
32}1// Statystyki tributów według typu
2async getTributeStatsByType(): Promise<any[]> {
3 return this.tributeRepository
4 .createQueryBuilder('tribute')
5 .select('tribute.type', 'type')
6 .addSelect('COUNT(*)', 'count')
7 .addSelect('AVG(tribute.value)', 'avgValue')
8 .addSelect('SUM(tribute.value)', 'totalValue')
9 .addSelect('MIN(tribute.value)', 'minValue')
10 .addSelect('MAX(tribute.value)', 'maxValue')
11 .groupBy('tribute.type')
12 .orderBy('totalValue', 'DESC')
13 .getRawMany();
14}
15
16// Ranking centurionów według wartości tributów legionu
17async getCenturionRanking(): Promise<any[]> {
18 return this.centurionRepository
19 .createQueryBuilder('centurion')
20 .leftJoin('centurion.legion', 'legionariusze')
21 .leftJoin('legionariusze.tributes', 'tribute')
22 .select('centurion.id', 'centurionId')
23 .addSelect('centurion.name', 'centurionName')
24 .addSelect('centurion.cohortName', 'cohortName')
25 .addSelect('COUNT(DISTINCT legionariusze.id)', 'legionSize')
26 .addSelect('COUNT(tribute.id)', 'totalTributes')
27 .addSelect('COALESCE(SUM(tribute.value), 0)', 'totalValue')
28 .groupBy('centurion.id')
29 .having('COUNT(DISTINCT legionariusze.id) > :minLegion', { minLegion: 2 })
30 .orderBy('totalValue', 'DESC')
31 .getRawMany();
32}
33
34// Statystyki miesięczne odkryć
35async getMonthlyDiscoveryStats(): Promise<any[]> {
36 return this.tributeRepository
37 .createQueryBuilder('tribute')
38 .select('EXTRACT(YEAR FROM tribute.collectedAt)', 'year')
39 .addSelect('EXTRACT(MONTH FROM tribute.collectedAt)', 'month')
40 .addSelect('COUNT(*)', 'discoveries')
41 .addSelect('SUM(tribute.value)', 'totalValue')
42 .addSelect('AVG(tribute.value)', 'avgValue')
43 .where('tribute.collectedAt >= :startDate', {
44 startDate: new Date('2023-01-01')
45 })
46 .groupBy('EXTRACT(YEAR FROM tribute.collectedAt)')
47 .addGroupBy('EXTRACT(MONTH FROM tribute.collectedAt)')
48 .orderBy('year', 'DESC')
49 .addOrderBy('month', 'DESC')
50 .getRawMany();
51}1// Złożone warunki logiczne
2async findTributesByComplexCriteria(criteria: {
3 minValue?: number;
4 types?: string[];
5 locations?: string[];
6 discoveredAfter?: Date;
7 ownerRanks?: string[];
8 excludeCursed?: boolean;
9}): Promise<Tribute[]> {
10 let query = this.tributeRepository
11 .createQueryBuilder('tribute')
12 .leftJoinAndSelect('tribute.legionariusze', 'legionariusze')
13 .leftJoinAndSelect('legionariusze.centurion', 'centurion');
14
15 // Dynamiczne dodawanie warunków
16 if (criteria.minValue) {
17 query = query.andWhere('tribute.value >= :minValue', {
18 minValue: criteria.minValue
19 });
20 }
21
22 if (criteria.types && criteria.types.length > 0) {
23 query = query.andWhere('tribute.type IN (:...types)', {
24 types: criteria.types
25 });
26 }
27
28 if (criteria.locations && criteria.locations.length > 0) {
29 query = query.andWhere('tribute.location IN (:...locations)', {
30 locations: criteria.locations
31 });
32 }
33
34 if (criteria.discoveredAfter) {
35 query = query.andWhere('tribute.collectedAt >= :discoveredAfter', {
36 discoveredAfter: criteria.discoveredAfter
37 });
38 }
39
40 if (criteria.ownerRanks && criteria.ownerRanks.length > 0) {
41 query = query.andWhere('legionariusze.rank IN (:...ranks)', {
42 ranks: criteria.ownerRanks
43 });
44 }
45
46 if (criteria.excludeCursed) {
47 query = query.andWhere('tribute.isCursed = :isCursed', {
48 isCursed: false
49 });
50 }
51
52 return query
53 .orderBy('tribute.value', 'DESC')
54 .getMany();
55}
56
57// Warunki z sub-queries
58async findLegionarysWithMostValuableTribute(): Promise<any[]> {
59 return this.legionariuszeRepository
60 .createQueryBuilder('legionariusze')
61 .leftJoinAndSelect('legionariusze.tributes', 'tribute')
62 .where(qb => {
63 const subQuery = qb.subQuery()
64 .select('MAX(t.value)')
65 .from(Tribute, 't')
66 .where('t.legionariuszeId = legionariusze.id')
67 .getQuery();
68 return `tribute.value = (${subQuery})`1 })
2 .orderBy('tribute.value', 'DESC')
3 .getMany();
4}1// Paginacja z Query Builder
2async findTributesWithPagination(
3 page: number = 1,
4 limit: number = 10,
5 sortBy: string = 'value',
6 sortOrder: 'ASC' | 'DESC' = 'DESC'
7) {
8 const queryBuilder = this.tributeRepository
9 .createQueryBuilder('tribute')
10 .leftJoinAndSelect('tribute.legionariusze', 'legionariusze');
11
12 // Dynamiczne sortowanie
13 if (sortBy === 'collectedAt' || sortBy === 'value' || sortBy === 'name') {
14 queryBuilder.orderBy(`tribute.${sortBy}`, sortOrder);
15 } else if (sortBy === 'ownerName') {
16 queryBuilder.orderBy('legionariusze.name', sortOrder);
17 }
18
19 const [tributes, total] = await queryBuilder
20 .skip((page - 1) * limit)
21 .take(limit)
22 .getManyAndCount();
23
24 return {
25 data: tributes,
26 pagination: {
27 page,
28 limit,
29 total,
30 totalPages: Math.ceil(total / limit),
31 hasNext: page * limit < total,
32 hasPrev: page > 1
33 }
34 };
35}
36
37// Top N wyników
38async getTopTributesByValue(limit: number = 10): Promise<Tribute[]> {
39 return this.tributeRepository
40 .createQueryBuilder('tribute')
41 .leftJoinAndSelect('tribute.legionariusze', 'legionariusze')
42 .orderBy('tribute.value', 'DESC')
43 .limit(limit)
44 .getMany();
45}1// Użycie funkcji SQL
2async getTributeDistanceFromPort(portLat: number, portLng: number): Promise<any[]> {
3 return this.tributeRepository
4 .createQueryBuilder('tribute')
5 .select('tribute.*')
6 .addSelect(
7 `SQRT(POW(tribute.latitude - :portLat, 2) + POW(tribute.longitude - :portLng, 2))`,
8 'distance'
9 )
10 .where('tribute.latitude IS NOT NULL')
11 .andWhere('tribute.longitude IS NOT NULL')
12 .orderBy('distance', 'ASC')
13 .setParameters({ portLat, portLng })
14 .getRawMany();
15}
16
17// Zaawansowane funkcje tekstowe
18async searchTributesFullText(searchTerm: string): Promise<Tribute[]> {
19 return this.tributeRepository
20 .createQueryBuilder('tribute')
21 .where(
22 `to_tsvector('english', tribute.name || ' ' || COALESCE(tribute.description, ''))
23 @@ plainto_tsquery('english', :searchTerm)`,
24 { searchTerm }
25 )
26 .orderBy(
27 `ts_rank(to_tsvector('english', tribute.name || ' ' || COALESCE(tribute.description, '')),
28 plainto_tsquery('english', :searchTerm))`,
29 'DESC'
30 )
31 .setParameter('searchTerm', searchTerm)
32 .getMany();
33}
34
35// Funkcje daty
36async getTributesByDateRange(
37 startDate: Date,
38 endDate: Date,
39 groupBy: 'day' | 'week' | 'month' = 'day'
40): Promise<any[]> {
41 const dateFormat = {
42 day: 'YYYY-MM-DD',
43 week: 'YYYY-"W"WW',
44 month: 'YYYY-MM'
45 };
46
47 return this.tributeRepository
48 .createQueryBuilder('tribute')
49 .select(`TO_CHAR(tribute.collectedAt, '${dateFormat[groupBy]}')`, 'period')
50 .addSelect('COUNT(*)', 'discoveries')
51 .addSelect('SUM(tribute.value)', 'totalValue')
52 .where('tribute.collectedAt BETWEEN :startDate AND :endDate', {
53 startDate,
54 endDate
55 })
56 .groupBy('period')
57 .orderBy('period', 'ASC')
58 .getRawMany();
59}1// Indeksy w zapytaniach
2async findTributesOptimized(type: string, minValue: number): Promise<Tribute[]> {
3 return this.tributeRepository
4 .createQueryBuilder('tribute')
5 .useIndex('IDX_tribute_type_value') // Użyj konkretnego indeksu
6 .where('tribute.type = :type', { type })
7 .andWhere('tribute.value >= :minValue', { minValue })
8 .orderBy('tribute.value', 'DESC')
9 .getMany();
10}
11
12// Batch processing dla dużych zbiorów
13async processTributesBatch(
14 batchSize: number = 1000,
15 processor: (tributes: Tribute[]) => Promise<void>
16): Promise<void> {
17 let offset = 0;
18 let hasMore = true;
19
20 while (hasMore) {
21 const batch = await this.tributeRepository
22 .createQueryBuilder('tribute')
23 .orderBy('tribute.id', 'ASC')
24 .skip(offset)
25 .take(batchSize)
26 .getMany();
27
28 if (batch.length === 0) {
29 hasMore = false;
30 } else {
31 await processor(batch);
32 offset += batchSize;
33 hasMore = batch.length === batchSize;
34 }
35 }
36}
37
38// Query z EXPLAIN dla analizy wydajności
39async explainQuery(type: string): Promise<any> {
40 const query = this.tributeRepository
41 .createQueryBuilder('tribute')
42 .where('tribute.type = :type', { type })
43 .orderBy('tribute.value', 'DESC');
44
45 // W środowisku dev można analizować plan zapytania
46 const sql = query.getSql();
47 console.log('Generated SQL:', sql);
48
49 return query.getMany();
50}1// Operacje transakcyjne
2async transferTribute(
3 tributeId: number,
4 fromLegionaryId: number,
5 toLegionaryId: number
6): Promise<void> {
7 await this.tributeRepository.manager.transaction(async manager => {
8 // Aktualizuj właściciela tributy
9 await manager
10 .createQueryBuilder()
11 .update(Tribute)
12 .set({ legionariusze: { id: toLegionaryId } })
13 .where('id = :tributeId', { tributeId })
14 .andWhere('legionariusze.id = :fromLegionaryId', { fromLegionaryId })
15 .execute();
16
17 // Zaktualizuj liczniki legionariuszów
18 await manager
19 .createQueryBuilder()
20 .update(Legionary)
21 .set({
22 tributeCount: () => 'tribute_count - 1',
23 totalTributeValue: () => `total_tribute_value - (
24 SELECT value FROM tributes WHERE id = ${tributeId}
25 )`
26 })
27 .where('id = :fromLegionaryId', { fromLegionaryId })
28 .execute();
29
30 await manager
31 .createQueryBuilder()
32 .update(Legionary)
33 .set({
34 tributeCount: () => 'tribute_count + 1',
35 totalTributeValue: () => `total_tribute_value + (
36 SELECT value FROM tributes WHERE id = ${tributeId}
37 )`
38 })
39 .where('id = :toLegionaryId', { toLegionaryId })
40 .execute();
41 });
42}1// tribute-query.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { getRepositoryToken } from '@nestjs/typeorm';
4import { TributeQueryService } from './tribute-query.service';
5import { Tribute } from './tribute.entity';
6
7describe('TributeQueryService', () => {
8 let service: TributeQueryService;
9 let mockRepository: any;
10
11 const mockQueryBuilder = {
12 where: jest.fn().mockReturnThis(),
13 andWhere: jest.fn().mockReturnThis(),
14 orWhere: jest.fn().mockReturnThis(),
15 orderBy: jest.fn().mockReturnThis(),
16 getMany: jest.fn(),
17 getRawMany: jest.fn(),
18 leftJoinAndSelect: jest.fn().mockReturnThis()
19 };
20
21 beforeEach(async () => {
22 mockRepository = {
23 createQueryBuilder: jest.fn(() => mockQueryBuilder)
24 };
25
26 const module: TestingModule = await Test.createTestingModule({
27 providers: [
28 TributeQueryService,
29 {
30 provide: getRepositoryToken(Tribute),
31 useValue: mockRepository
32 }
33 ]
34 }).compile();
35
36 service = module.get<TributeQueryService>(TributeQueryService);
37 });
38
39 describe('findByValueRange', () => {
40 it('should find tributes in value range', async () => {
41 const mockTributes = [
42 { id: 1, name: 'Golden Coin', value: 1500 }
43 ];
44
45 mockQueryBuilder.getMany.mockResolvedValue(mockTributes);
46
47 const result = await service.findByValueRange(1000, 2000);
48
49 expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('tribute');
50 expect(mockQueryBuilder.where).toHaveBeenCalledWith(
51 'tribute.value >= :minValue',
52 { minValue: 1000 }
53 );
54 expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
55 'tribute.value <= :maxValue',
56 { maxValue: 2000 }
57 );
58 expect(result).toEqual(mockTributes);
59 });
60 });
61});1// DOBRZE
2const tributes = await repository
3 .createQueryBuilder('tribute')
4 .leftJoinAndSelect('tribute.legionariusze', 'legionariusze')
5 .where('tribute.value > :value', { value: 1000 })
6 .getMany();
7
8// ŹLE
9const tributes = await repository
10 .createQueryBuilder()
11 .where('value > 1000')
12 .getMany();1// DOBRZE
2.where('tribute.name = :name', { name: userInput })
3
4// ŹLE (podatne na SQL injection)
5.where(`tribute.name = '${userInput}'`)1// Załaduj tylko potrzebne relacje
2.leftJoinAndSelect('tribute.legionariusze', 'legionariusze')
3.leftJoin('legionariusze.centurion', 'centurion') // Bez Select jeśli nie potrzebujemy danych
4.addSelect('centurion.name') // Dodaj tylko potrzebne polaTeraz jesteś prawdziwym mistrzem poszukiwań! Umiesz:
✓ Tworzyć złożone zapytania z Query Builder ✓ Używać agregacji i statystyk do analizy danych ✓ Optymalizować wydajność zapytań ✓ Implementować paginację i sortowanie ✓ Testować Query Builder w unit testach
Query Builder to twój najlepszy kompas w gąszczu danych - pozwala znaleźć dokładnie to, czego szukasz!