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
typeormto 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.
pgto popularny driver (sterownik) do PostgreSQL w środowisku Node.js.- TypeORM wymaga odpowiedniego drivera do połączenia się z bazą danych, a
pgzapewnia 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 poleidjest 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śćpostyw encjiUzytkownik, 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 klasyUzytkownik.
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,
) {}
async znajdzWszystkich(): Promise {
return this.uzytkownikRepo.find();
}
async znajdzJednego(id: number): Promise {
return this.uzytkownikRepo.findOneBy({ id });
}
async stworz(stworzUzytkownikDto: StworzUzytkownikDto): Promise {
const nowyUzytkownik = this.uzytkownikRepo.create(stworzUzytkownikDto);
return this.uzytkownikRepo.save(nowyUzytkownik);
}
async usun(id: number): Promise {
await this.uzytkownikRepo.delete(id);
}
}
-
find()– Zwraca listę wszystkich użytkowników. findOneBy({ id })– Znajduje użytkownika o określonymid.create()– Tworzy nowy obiekt encji na podstawie DTO.save()– Zapisuje nowy obiekt encji w bazie.delete()– Usuwa użytkownika na podstawieid.
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 encjiUzytkownik. 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 encjiUzytkownik. Jest to wymagane, ponieważ NestJS potrzebuje wiedzieć, że chcemy korzystać z repozytorium encjiUzytkownik. Dzięki temu serwis może wykonywać zapytania bezpośrednio na tej tabeli w bazie danych.private readonly uzytkownikRepo: Repository<Uzytkownik>: Deklaracjaprivate readonlyoznacza, żeuzytkownikRepojest prywatnym polem klasy i nie powinno być modyfikowane po inicjalizacji, aRepository<Uzytkownik>wskazuje typ tego pola jako repozytorium encjiUzytkownik.
Co oznacza ten kod?
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 klasaUzytkownikServicemoże być zależnością, a@InjectRepository(Uzytkownik)pozwala na wstrzyknięcie repozytorium dla klasyUzytkownik.Repozytorium encji
Uzytkownik: PoleuzytkownikRepojest repozytorium encjiUzytkownik, co oznacza, że ma dostęp do metod TypeORM, takich jakfind(),findOne(),save(),delete(). Dzięki temuUzytkownikServicemoż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 {
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.