Praca z Bazą Danych w NestJS

Wprowadzenie

Praca z bazą danych jest kluczowym elementem wielu aplikacji serwerowych, a NestJS oferuje świetne narzędzia do tego, aby w prosty sposób skonfigurować połączenie z bazą oraz zarządzać danymi. W tym wpisie przyjrzymy się integracji NestJS z bazą danych, korzystając z TypeORM – popularnego narzędzia ORM, które umożliwia mapowanie obiektowo-relacyjne w aplikacjach opartych na TypeScript. Skonfigurujemy połączenie z bazą danych, stworzymy encje oraz wdrożymy operacje CRUD. Skupimy się na przykładzie z bazą PostgreSQL, ale podobne podejście można zastosować również dla MySQL.

Konfiguracja połączenia z bazą danych za pomocą TypeORM

Instalacja i konfiguracja TypeORM

Aby zacząć korzystać z TypeORM w NestJS, musimy zainstalować odpowiednie paczki. Załóżmy, że będziemy korzystać z bazy danych PostgreSQL. W terminalu wykonujemy poniższe polecenie:

				
					npm install @nestjs/typeorm typeorm pg

				
			

1. @nestjs/typeorm

  • Jest to paczka, która integruje framework NestJS z biblioteką TypeORM.
  • Zapewnia wtyczkę dla NestJS, która upraszcza konfigurowanie połączeń z bazą danych oraz wstrzykiwanie repozytoriów do serwisów.
  • Dzięki niej możemy korzystać z wygodnych narzędzi dostarczanych przez NestJS, takich jak TypeOrmModule, które automatycznie konfigurują i zarządzają połączeniami oraz encjami.

Główne funkcjonalności paczki @nestjs/typeorm:

  • Proste konfigurowanie połączenia z bazą danych za pomocą dekoratorów i modułów.
  • Możliwość wstrzykiwania repozytoriów do serwisów za pomocą dekoratora @InjectRepository().
  • Ścisła integracja z mechanizmami Dependency Injection (DI) używanymi w NestJS.

2. typeorm

  • typeorm to biblioteka ORM (Object Relational Mapping), która umożliwia mapowanie obiektów z języka TypeScript (lub JavaScript) na tabele baz danych.
  • ORM pomaga w pracy z bazą danych w sposób obiektowy, co oznacza, że operujemy na obiektach klas, a TypeORM zajmuje się tłumaczeniem operacji na odpowiednie zapytania SQL.
  • TypeORM wspiera różne typy baz danych, takie jak PostgreSQL, MySQL, MariaDB, SQLite, MS SQL Server i inne, dzięki czemu można łatwo migrować aplikację między różnymi bazami danych.

Główne funkcjonalności typeorm:

  • Tworzenie i zarządzanie encjami, które odpowiadają tabelom w bazie danych.
  • Obsługa relacji między encjami (np. OneToMany, ManyToOne).
  • Realizowanie operacji CRUD na bazie danych przy użyciu repozytoriów i Query Buildera.
  • Możliwość migracji bazy danych oraz synchronizacji schematów z bazą.

3. pg

  • Jest to paczka, która odpowiada za obsługę komunikacji z bazą danych PostgreSQL.
  • pg to popularny driver (sterownik) do PostgreSQL w środowisku Node.js.
  • TypeORM wymaga odpowiedniego drivera do połączenia się z bazą danych, a pg zapewnia funkcje potrzebne do nawiązywania połączenia, wykonywania zapytań, zarządzania transakcjami itp.

Główne funkcjonalności pg:

  • Umożliwia komunikację między aplikacją Node.js a serwerem PostgreSQL.
  • Jest wykorzystywana przez TypeORM jako warstwa komunikacyjna do wysyłania zapytań i odbierania odpowiedzi z bazy danych.

Pierwszym krokiem jest skonfigurowanie połączenia z bazą danych w głównym module aplikacji (AppModule). Przykład poniżej pokazuje, jak skonfigurować TypeORM do pracy z PostgreSQL:

				
					import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UzytkownikModule } from './uzytkownik/uzytkownik.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'user',
      password: 'password',
      database: 'my_database',
      entities: [__dirname + '/' + '../**/*.entity{.ts,.js}'],
      synchronize: true, // Synchronizuje schemat bazy danych na podstawie encji (tylko w środowisku deweloperskim)
    }),
    UzytkownikModule,
  ],
})
export class AppModule {}

				
			

W powyższej konfiguracji podajemy dane dostępowe do bazy (host, port, nazwa użytkownika, hasło, nazwa bazy). Flaga synchronize: true pozwala TypeORM na automatyczne synchronizowanie schematu bazy danych. Pamiętaj jednak, aby w środowisku produkcyjnym ustawić tę flagę na false, by uniknąć niechcianych zmian w schemacie bazy danych.

Tworzenie encji i relacji

Encje są odpowiednikiem tabel w bazie danych. W NestJS z TypeORM, encje są definiowane jako klasy TypeScript, które mogą zawierać kolumny oraz relacje.

Przykładowa encja użytkownika (uzytkownik.entity.ts) może wyglądać tak:

				
					import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Uzytkownik {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  imie: string;

  @Column()
  nazwisko: string;

  @Column()
  email: string;

  @Column()
  haslo: string;
}

				
			
  • @Entity() – Dekorator oznaczający, że klasa jest encją.
  • @PrimaryGeneratedColumn() – Oznacza, że pole id jest kluczem głównym i jego wartość będzie generowana automatycznie.
  • @Column() – Dekorator określający, że dana właściwość jest kolumną w bazie danych.

Jeśli mamy więcej encji, możemy utworzyć między nimi relacje. Na przykład dodajmy encję Post, która może mieć relację z użytkownikiem:

				
					import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import { Uzytkownik } from './uzytkownik.entity';

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  tytul: string;

  @Column()
  tresc: string;

  @ManyToOne(() => Uzytkownik, (uzytkownik) => uzytkownik.posty)
  uzytkownik: Uzytkownik;
}

				
			

Dekorator @ManyToOne() wskazuje na relację między encjami – w tym przypadku Post ma relację z Uzytkownik. Dzięki temu możemy zdefiniować, że jeden użytkownik może mieć wiele postów.

Aby zrozumieć tę deklarację, przyjrzyjmy się jej szczegółowo, krok po kroku:

Ogólna Idea

Dekorator @ManyToOne oznacza relację „wiele do jednego” między encją Post a encją Uzytkownik.

Relacja „wiele do jednego” (ManyToOne) oznacza, że wiele obiektów jednej encji (w tym przypadku wiele postów) może być powiązanych z jednym obiektem drugiej encji (w tym przypadku jednym użytkownikiem).

W praktyce oznacza to, że:

  • Jeden użytkownik może napisać wiele postów.
  • Każdy post jest przypisany tylko do jednego użytkownika.

Wyjaśnienie Składni

1. @ManyToOne(() => Uzytkownik, (uzytkownik) => uzytkownik.posty)

  • @ManyToOne(): To dekorator TypeORM, który definiuje relację typu „wiele do jednego”.
  • (() => Uzytkownik): Jest to funkcja zwracająca klasę Uzytkownik, co wskazuje na to, do której encji ta relacja się odnosi. Jest to tzw. „lazy function” – TypeORM wymaga takiego sposobu odwoływania się do encji, aby uniknąć problemów z cyklicznymi odniesieniami.
  • (uzytkownik) => uzytkownik.posty: Jest to tzw. „inverse side” relacji, czyli odwrotna strona relacji. W tym przypadku wskazuje na właściwość posty w encji Uzytkownik, która przechowuje listę wszystkich postów napisanych przez danego użytkownika. Dzięki temu TypeORM wie, że istnieje relacja między tymi dwoma stronami, i może zarządzać nią dwukierunkowo.

2. uzytkownik: Uzytkownik;

  • uzytkownik: To nazwa właściwości, która przechowuje referencję do użytkownika, do którego przypisany jest dany post. W praktyce, jeśli chcemy wiedzieć, kto napisał post, możemy uzyskać tę informację, odwołując się do tej właściwości.
  • : Uzytkownik: To typ tej właściwości, który wskazuje, że jest to instancja klasy Uzytkownik.

Podstawowe operacje CRUD

Tworzenie repozytoriów

Aby pracować z danymi w bazie, TypeORM wykorzystuje repozytoria. Repozytoria pozwalają na interakcję z encjami, takie jak ich tworzenie, odczytywanie, aktualizacja oraz usuwanie. Aby korzystać z repozytorium, musimy je zarejestrować w module.

W module UzytkownikModule rejestrujemy nasze repozytorium:

				
					import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UzytkownikController } from './uzytkownik.controller';
import { UzytkownikService } from './uzytkownik.service';
import { Uzytkownik } from './uzytkownik.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Uzytkownik])],
  controllers: [UzytkownikController],
  providers: [UzytkownikService],
})
export class UzytkownikModule {}

				
			

Dekorator TypeOrmModule.forFeature([Uzytkownik]) pozwala na wstrzyknięcie repozytorium Uzytkownik do serwisu.

Implementacja metod CRUD

Repozytoria TypeORM dostarczają gotowe metody, takie jak find(), findOne(), save(), remove(), które pomagają nam tworzyć funkcje CRUD.

Przykładowy serwis (uzytkownik.service.ts) może wyglądać następująco:

				
					import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Uzytkownik } from './uzytkownik.entity';
import { StworzUzytkownikDto } from './dto/stworz-uzytkownik.dto';

@Injectable()
export class UzytkownikService {
  constructor(
    @InjectRepository(Uzytkownik)
    private readonly uzytkownikRepo: Repository<Uzytkownik>,
  ) {}

  async znajdzWszystkich(): Promise<Uzytkownik[]> {
    return this.uzytkownikRepo.find();
  }

  async znajdzJednego(id: number): Promise<Uzytkownik> {
    return this.uzytkownikRepo.findOneBy({ id });
  }

  async stworz(stworzUzytkownikDto: StworzUzytkownikDto): Promise<Uzytkownik> {
    const nowyUzytkownik = this.uzytkownikRepo.create(stworzUzytkownikDto);
    return this.uzytkownikRepo.save(nowyUzytkownik);
  }

  async usun(id: number): Promise<void> {
    await this.uzytkownikRepo.delete(id);
  }
}

				
			
  •  find() – Zwraca listę wszystkich użytkowników.
  • findOneBy({ id }) – Znajduje użytkownika o określonym id.
  • create() – Tworzy nowy obiekt encji na podstawie DTO.
  • save() – Zapisuje nowy obiekt encji w bazie.
  • delete() – Usuwa użytkownika na podstawie id.

Dekorator @Injectable()

Dekorator @Injectable() w NestJS oznacza, że klasa UzytkownikService może być wstrzykiwana jako zależność do innych klas, które jej potrzebują. Dzięki temu serwis UzytkownikService może być używany przez kontroler (controller) lub inny serwis, który z niego korzysta.

  • @Injectable() wskazuje, że dana klasa jest serwisem, który może być „wstrzyknięty” w inne miejsca aplikacji. Innymi słowy, NestJS wie, że instancja tej klasy może być utworzona przez mechanizm wstrzykiwania zależności (Dependency Injection), dzięki czemu serwis jest łatwo dostępny w aplikacji.

Konstruktor i dekorator @InjectRepository()

Konstruktor klasy UzytkownikService ma za zadanie wstrzyknąć repozytorium (Repository) encji Uzytkownik. W ten sposób serwis ma bezpośredni dostęp do repozytorium TypeORM, co pozwala na wykonywanie operacji CRUD (tworzenie, odczytywanie, aktualizowanie, usuwanie) na danych użytkowników.

  • Konstruktor: Konstruktor jest metodą, która jest uruchamiana podczas tworzenia instancji klasy. W tym przypadku konstruktor przyjmuje parametr uzytkownikRepo, który jest repozytorium encji Uzytkownik. Dzięki Dependency Injection, NestJS automatycznie wstrzykuje odpowiednie repozytorium, które jest zarządzane przez TypeORM.

  • @InjectRepository(Uzytkownik): Dekorator @InjectRepository() jest używany, aby wstrzyknąć konkretne repozytorium TypeORM dla encji Uzytkownik. Jest to wymagane, ponieważ NestJS potrzebuje wiedzieć, że chcemy korzystać z repozytorium encji Uzytkownik. Dzięki temu serwis może wykonywać zapytania bezpośrednio na tej tabeli w bazie danych.

  • private readonly uzytkownikRepo: Repository<Uzytkownik>: Deklaracja private readonly oznacza, że uzytkownikRepo jest prywatnym polem klasy i nie powinno być modyfikowane po inicjalizacji, a Repository<Uzytkownik> wskazuje typ tego pola jako repozytorium encji Uzytkownik.

Co oznacza ten kod?

  1. Wstrzykiwanie zależności: Mechanizm Dependency Injection (DI) w NestJS automatycznie dostarcza zależności (np. repozytoria, serwisy) do klasy, która ich potrzebuje. Dzięki @Injectable(), NestJS wie, że klasa UzytkownikService może być zależnością, a @InjectRepository(Uzytkownik) pozwala na wstrzyknięcie repozytorium dla klasy Uzytkownik.

  2. Repozytorium encji Uzytkownik: Pole uzytkownikRepo jest repozytorium encji Uzytkownik, co oznacza, że ma dostęp do metod TypeORM, takich jak find(), findOne(), save(), delete(). Dzięki temu UzytkownikService może bezpośrednio operować na danych użytkowników w bazie.

Obsługa błędów

Obsługa błędów jest kluczowa w aplikacjach produkcyjnych. W NestJS możemy obsługiwać błędy, używając wbudowanych narzędzi takich jak HttpException.

Przykład obsługi błędu w metodzie serwisu:

				
					import { HttpException, HttpStatus } from '@nestjs/common';

async znajdzJednego(id: number): Promise<Uzytkownik> {
  const uzytkownik = await this.uzytkownikRepo.findOneBy({ id });
  if (!uzytkownik) {
    throw new HttpException('Użytkownik nie znaleziony', HttpStatus.NOT_FOUND);
  }
  return uzytkownik;
}

				
			

W powyższym przykładzie, jeśli użytkownik o danym id nie istnieje, rzucamy wyjątek HttpException z kodem statusu HTTP 404 - Not Found.

Podsumowanie

NestJS, w połączeniu z TypeORM, stanowi doskonałą platformę do pracy z bazami danych, umożliwiając szybkie tworzenie aplikacji serwerowych. W tym wpisie skonfigurowaliśmy połączenie z bazą danych PostgreSQL, nauczyliśmy się tworzyć encje oraz wdrożyliśmy operacje CRUD przy użyciu repozytoriów TypeORM.

Dzięki TypeORM i NestJS możemy tworzyć aplikacje, które są łatwe w utrzymaniu, modularne i gotowe do skalowania. Skorzystanie z repozytoriów ułatwia zarządzanie danymi, a TypeORM automatycznie tłumaczy nasze operacje na odpowiednie zapytania SQL. Zachęcam do dalszej praktyki i rozwijania umiejętności pracy z bazami danych w NestJS – to naprawdę potężne narzędzie do budowy aplikacji back-endowych.