Friedrich Siever

11. Januar 2024

Nest.js - das Repository Pattern

von: Friedrich Siever | Last Updated: 13.01.24

Intro

Lass uns einen Überblick verschaffen, wo genau Repositories bei datenbankbezogenen Themen ansetzen.

  • Tabellenzeilen (Rows): Werden durch Entitäten repräsentiert.
  • Tabellen (Tables): Werden von generischen Repositories gemanagt. Die generische Repository-Klasse ist immer verfügbar und umfasst grundlegende Methoden wie findOne usw. Die meisten Operationen können erfolgreich durch die Nutzung eines generischen Repositories durchgeführt werden.

Repositories sind kein spezielles Datenbankthema. Es handelt sich eher um ein grundsätzliches Programmierparadigma, das besonders gut zu ORM (Object-Relational Mapping)-Systemen passt.

TypeORM hat daher eine generische Repository-Klasse implementiert, die das TypeScript-Generics-Feature nutzt und mit jeder Entität arbeiten kann. Diese Art von Repository werden wir in diesem Beitrag nutzen.

Diese Klasse enthält alle grundlegenden Methoden, die du zur Erstellung von CRUD-Applikationen benötigst. Hierzu gehören save(), find(), findOne() und remove().

Neben der generischen Repository-Klasse gibt es auch eine spezielle Repository-Klasse. Diese ist besonders hilfreich für komplexere Abfragen und wird eingesetzt, wenn Abfragen mehr als einmal im Code verwendet werden.

Repositories in der Praxis

Genug der Einleitung. Schauen wir uns das Ganze jetzt in der Praxis an, um dieses abstrakte Konzept greifbarer zu machen. In unserem grundlegenden Controller möchten wir natürlich sinnvolle Aktionen mit unserer Datenbank durchführen.

In Nest.js erstellen wir selten eigene Klassen. Stattdessen empfangen und nutzen wir sie als Mechanismus. Diesen Prozess bezeichnet man als Dependency Injection.

Um konkret zu erklären, welche Art von Daten wir mit unserem Repository bearbeiten wollen, verwenden wir den @InjectRepository-Decorator. Indem wir diesen Decorator mit dem Type Exhibition spezifizieren, teilen wir Nest.js mit, welche weiteren Klassen dieses Repository benötigt. Der Rest wird von Nest.js automatisch übernommen – pure Magie!

Es ist wichtig, Repositories mit diesem Decorator zu kennzeichnen. Das Argument für den Decorator ist die Entitätsklasse für das Repository.

Hier ein Beispiel aus unserem ExhibitionsController:

@Controller("/exhibitions")
// ...
export class ExhibitionsController {
  constructor(
    @InjectRepository(Exhibition)
    private readonly repository: Repository<Exhibition>,
  ) {}
  // ...
}

it diesem Ansatz kümmert sich Nest.js nahtlos um die Verwaltung und Bereitstellung des passenden Repositorys für unsere Exhibition-Entität. Ein einfacher und eleganter Weg, um mit Datenbankoperationen umzugehen!

Der Rest wird zu einem Kinderspiel. Schauen wir uns also an, wie wir mit Repository-Methoden all unsere Fälle in diesem Controller abhandeln können.

Vorher ist es wichtig, sich klar darüber zu sein, dass alle Repository-Methoden ein Promise zurückgeben müssen, wie es für alle Datenbankoperationen gilt.

Ein einfacher Controller, der mit deiner Datenbank interagiert, könnte also so aussehen:

import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
} from "@nestjs/common";
import { CreateExhibitionDto } from "./create-exhibition.dto";
import { Exhibition } from "./exhibitions.entity";
import { UpdateExhibitionDto } from "./update-exhibitions.dto";
import { Repository } from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";

@Controller("/exhibitions")
export class ExhibitionsController {
  constructor(
    @InjectRepository(Exhibition)
    private readonly repository: Repository<Exhibition>,
  ) {}

  @Get()
  async findAll() {
    return await this.repository.find();
  }

  @Get(":id")
  async findOne(@Param("id") id) {
    return await this.repository.findOneBy({ id });
  }

  @Post()
  async create(@Body() input: CreateExhibitionDto) {
    return await this.repository.save({
      ...input,
      starts: new Date(input.starts),
      ends: new Date(input.ends),
    });
  }

  @Patch(":id")
  async update(@Param("id") id, @Body() input: UpdateExhibitionDto) {
    const prevData = await this.repository.findOne(id);

    return await this.repository.save({
      ...prevData,
      ...input,
      starts: input.starts ? new Date(input.starts) : prevData.starts,
      ends: input.ends ? new Date(input.ends) : prevData.ends,
    });
  }

  @Delete(":id")
  @HttpCode(204)
  async remove(@Param("id") id) {
    const prevData = await this.repository.findOne(id);
    await this.repository.remove(prevData);
  }
}

Wir nähern uns dem Ende unserer Implementierung. Damit die reibungslose Injektion des Repositories funktioniert, müssen wir dem Modul mitteilen, dass es dieses benötigt.

// src/app.module.ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { Exhibition } from "./exhibitions.entity";
import { ExhibitionsController } from "./exhibitions.controller";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      // ...
    }),
    TypeOrmModule.forFeature([Exhibition]), // <-!!!
  ],

  controllers: [AppController, ExhibitionsController],
  providers: [AppService],
})
export class AppModule {}

Diese Konfiguration gewährleistet, dass das Repository für die spezifische Entität bereitgestellt wird, um von Nest.js für die Injektion verwendet zu werden.

Abfragen, Kriterien und Optionen

Der bisherige Code bietet noch Raum für Verbesserungen, insbesondere wenn es darum geht, praxistauglichen und produktionsreifen Code zu erstellen. Ein wesentlicher Aspekt dabei ist die Auseinandersetzung mit Abfragekriterien und -optionen im Repository. Diese ermöglichen das Filtern, Begrenzen und Sortieren der Ergebnisse. Um dies effektiv umzusetzen, sind Musterdaten vonnöten. Im Kontext meiner Messeanwendungen sehen diese beispielsweise wie folgt aus:

INSERT INTO exhibition (name, description, starts, ends, address) VALUES
  ('Artistic Marvels', 'An exploration of contemporary art and creativity.', '2022-03-10 14:00:00', '2022-03-10 18:00:00', 'Gallery Blvd 123'),
  ('Tech Innovations Expo', 'Discover the latest technological advancements and innovations.', '2022-03-15 10:30:00', '2022-03-15 17:30:00', 'Innovation Hub 456'),
  ('Nature''s Symphony', 'A visual journey celebrating the beauty of nature through art.', '2022-03-20 09:00:00', '2022-03-20 15:00:00', 'Nature Park 789'),
  ('Fantasy World Showcase', 'Immerse yourself in a world of fantasy and imagination.', '2022-03-25 12:00:00', '2022-03-25 20:00:00', 'Enchanted Castle 101'),
  ('Future of Space Exploration', 'Explore the future possibilities of space travel and interstellar adventures.', '2022-04-01 11:00:00', '2022-04-01 16:00:00', 'Space Center 202');

Um dies praktisch zu erleben, bietet es sich an, eine sogenannte “playground” Route im Controller zu erstellen.

Abfrage Bedingungen die where Property

@Controller("/exhibitions")
export class ExhibitionsController {
  constructor(
    @InjectRepository(Exhibition)
    private readonly repository: Repository<Exhibition>,
  ) {}

  // dein bisheriger Code

  @Get("/playground")
  async abfragen() {
    return await this.repository.find({
      where: { id: 3 },
    });
  }
}

Im obigen Beispiel nutzen wir die “where”-Property, die dem SQL-Code ähnlich ist, um Bedingungen zu definieren. Auf der Route http://localhost:3000/exhibitions/playground wird nur ein Array mit dem Objekt der Messe mit der ID 3 zurückgegeben. Dies ist eine einfache und effektive Methode.

Zu Illustrationszwecken wird hier auch der entsprechende SQL-Code gezeigt:

SELECT * FROM exhibition WHERE exhibition.id = 3

An dieser Stelle gibt es eine Vielzahl von Möglichkeiten zu entdecken, die uns von TypeORM bereitgestellt werden. Die Dokumentation zu den Optionen von TypeORM ist ausgezeichnet und bietet detaillierte Informationen. Im Folgenden werfen wir einen Blick auf eine Auswahl von Beispielen. Modifiziere gerne deine Abfragefunktion entsprechend und teste sie mit Postman oder Insomnia gegen deine API.

MoreThan()

Um alle Exhibitions mit einer ID größer als 2 abzurufen, kannst du den MoreThan()-Operator verwenden:

  async abfragen() {
    return await this.repository.find({
      where: { id: MoreThan(2) },
    });
  }

Die generierte SQL-Abfrage lautet:

SELECT * FROM exhibition WHERE exhibition.id > 2

Diese Abfrage gibt ein Array von Objekten zurück, deren ID größer als 2 ist, was den Exhibitions mit den IDs 3, 4, 5 entspricht (sofern du meine Beispieldaten verwendet hast).

Und Operatoren in der where Bedingung

TypeORM ermöglicht die Verwendung von logischen UND-Operatoren in der where-Bedingung. In diesem Beispiel versuchen wir herauszufinden, welche Messeveranstaltungen nach dem 20.03.2022 begonnen haben und eine ID größer als 2 haben.

 @Get('/playground')
 async abfragen() {
   return await this.repository.find({
     where: { id: MoreThan(2), starts: MoreThan(new Date('2022-03-20')) },
   });
 }

Die entsprechende SQL-Abfrage sieht wie folgt aus:

SELECT * FROM exhibition WHERE exhibition.id > 2 AND exhibition.starts > '2022-03-20'

Logische ODER-Operationen mit der where-Property

Die where-Property ermöglicht auch die Verwendung von logischen ODER-Operatoren. In diesem Beispiel geben wir alle Ergebnisse der vorherigen Abfrage zurück und fügen zusätzlich alle Messen hinzu, deren Beschreibung das Wort “zeitgenössisch” enthält.

  async abfragen() {
    return await this.repository.find({
      where: [
        { id: MoreThan(2), starts: MoreThan(new Date('2022-03-20')) },
        { description: Like('%zeitgenössisch%') },
      ],
    });
  }

Achte darauf, die Like-Funktion aus TypeOrm zu importieren, da sie dem bekannten SQL LIKE-Statement entspricht. Die SQL-Abfrage zum obigen Beispiel lautet:

SELECT * FROM exhibition WHERE exhibition.id > 2
        AND exhibition.starts > '2022-03-20'
        OR exhibition.description LIKE '%zeitgenössisch'

Die Array-Schreibweise wird auch genutzt, wenn unterschiedliche Entitäten mit verschiedenen Alternativen abgefragt werden sollen.

  @Get('/playground')
  async abfragen() {
    return await this.repository.find({
      where: [
        { id: 2 },
        { id: 5 },
      ],
    });
  }

Nun gibt unsere API die Messen mit den IDs 2 und 5 zurück. Dies wird durch die folgende SQL-Abfrage erreicht:

SELECT * FROM exhibitions WHERE id = 5 OR id = 10

Limits mit der take-Property

Die Verwendung der take-Property in TypeORM entspricht dem SQL-Limit-Statement.

async abfragen() {
  return await this.repository.find({
    where: [
      { id: MoreThan(2), starts: MoreThan(new Date('2022-03-20')) },
      { description: Like('%zeitgenössisch%') },
    ],
    take: 2,
  });
}

Die von SQL ausgeführte Abfrage lautet:

SELECT * FROM exhibition WHERE exhibition.id > 2
        AND exhibition.starts > '2022-03-20'
        OR exhibition.description LIKE '%zeitgenössisch'
LIMIT 2

Wenn du bereits Erfahrung mit SQL hast, wirst du vermutlich wissen, dass LIMIT häufig zusammen mit OFFSET verwendet wird. Auch das ist dank TypeORM typsicher und ziemlich einfach. In TypeORM verwendest du die skip-Property. Probiere es am besten als Übung selbst aus. Wenn du nicht weiterkommst, schaue in die oben verlinkten TypeORM-Docs.

Order

Um deine Datensätze zu sortieren, kannst du die order-Property verwenden.

async abfragen() {
    return await this.repository.find({
      where: [
        { id: MoreThan(2), starts: MoreThan(new Date('2022-03-20')) },
        { description: Like('%zeitgenössisch%') },
      ],
      take: 2,
      order: {
        id: 'DESC',
      },
    });
  }

Im vorherigen Beispiel haben wir die Datensätze so sortiert, dass die höchste ID zuerst in der API zurückgegeben wird. Wenn du die Reihenfolge umkehren möchtest, gib einfach ‘ASC’ für die order-Eigenschaft der id an.

Zur Vollständigkeit hier auch die auszuführende Abfrage:

SELECT * FROM exhibition WHERE exhibition.id > 2
        AND exhibition.starts > '2022-03-20'
        OR exhibition.description LIKE '%zeitgenössisch'
ORDER BY exhibition.id DESC
LIMIT 2

Die select Eigenschaft

Selbstverständlich kannst du auch festlegen, welche Felder der Entität überhaupt zurückgegeben werden sollen. Dies erfolgt mithilfe der select Eigenschaft. Wenn wir beispielsweise nur die Felder id und Starttermin zurückgeben möchten, sieht dies wie folgt aus:

  async abfragen() {
    return await this.repository.find({
      select: ['id', 'starts'],
      where: [
        { id: MoreThan(2), starts: MoreThan(new Date('2022-03-20')) },
        { description: Like('%zeitgenössisch%') },
      ],
      take: 2,
      order: {
        id: 'DESC',
      },
    });
  }

Hier ist erneut die SQL-Abfrage für dich.

SELECT id, name FROM exhibition WHERE exhibition.id > 2
        AND exhibition.starts > '2022-03-20'
        OR exhibition.description LIKE '%zeitgenössisch'
ORDER BY exhibition.id DESC
LIMIT 2

Fazit

Fazit Die Integration des Repository-Patterns in Nest.js eröffnet eine elegante Möglichkeit zur effizienten Datenbankverwaltung in TypeScript. Durch die Verwendung von generischen und speziellen Repositories bietet Nest.js eine benutzerfreundliche und leistungsstarke Lösung für CRUD-Operationen.

Das Repository-Pattern erlaubt eine klare Trennung der Verantwortlichkeiten zwischen der Darstellungsschicht und der Datenbankverwaltung. Die generischen Repositories stellen grundlegende Methoden bereit, während spezielle Repositories für komplexere Abfragen eingesetzt werden können.

Die nahtlose Integration von Repositories in Nest.js, unterstützt durch Dependency Injection, macht die Verwaltung und Bereitstellung von Datenbankoperationen zu einem kinderleichten Prozess. Dies fördert nicht nur die Code-Qualität, sondern erleichtert auch die Skalierbarkeit und Wartung von Nest.js-Anwendungen.

Insgesamt bietet das Repository-Pattern in Verbindung mit Nest.js eine effiziente und gut strukturierte Lösung für moderne TypeScript-Anwendungen. Die klare Abgrenzung der Datenbankverantwortlichkeiten trägt dazu bei, den Entwicklungsprozess zu optimieren und robuste, skalierbare Anwendungen zu erstellen.

0
0 Bewertungen

Jetzt selbst bewerten