Friedrich Siever

15. Januar 2024

Nest.js und TypeORM: Einblick in Datenbankrelationen

von: Friedrich Siever | Last Updated: 15.01.24

Einführung

Dieser Beitrag gibt eine umfassende Einführung in die möglichen Entitätsverbindungen in einer Nest.js-Anwendung mit SQL-Datenbank und TypeORM. Das Verständnis dieser Verbindungsarten ist entscheidend für die effektive Gestaltung von Datenbankbeziehungen.

Verbindungsarten

Es existieren drei Hauptverbindungsarten zwischen Entitäten, die auf der Anzahl der Elemente auf jeder Seite der Verbindung basieren:

  1. One-to-One-Relationship (1:1):

    • Beispiel: Nutzer (User) und Profil (Profile) Entitäten.
    • Jedes Profil ist immer mit einem bestimmten Nutzer verbunden.
  2. One-to-Many-Relationship (1:N):

    • Beispiel: Kommentare und Beiträge.
    • Ein Blogbeitrag kann viele Kommentare haben, aber ein Kommentar ist nur mit einem Beitrag verbunden.
  3. Many-to-Many-Relationship (N:N):

    • Beispiel: Lehrer und Unterrichtsfächer.
    • Ein Lehrer kann mehrere Unterrichtsfächer unterrichten, und ein Fach kann von verschiedenen Lehrern unterrichtet werden.

Diese Verbindungsarten bieten eine strukturierte Grundlage für die Gestaltung von Datenbankbeziehungen in Nest.js mit TypeORM. Durch das Verständnis dieser Konzepte kannst du effektiv entscheiden, welche Art von Verbindung für deine Anwendungslogik am besten geeignet ist.

Die Definition von Verbindungen in Nest.js ermöglicht es dir, deine Entitäten und alle verbundenen Entitäten bequem zu laden.

One-to-Many-Beziehung

Einrichtung

Schauen wir uns an, wie wir eine One-to-Many-Beziehung in Nest.js definieren können. In unserem kleinen Beispiel mit Messen nehmen wir an, dass Messeauftritte geladene Gäste haben. Zunächst müssen wir eine Entität für die Gäste erstellen, nennen wir sie “Guest”.

Erstelle dazu im Ordner deiner Ausstellungen (exhibition) eine Datei mit dem Namen guest.entity.ts. Nichts Besonderes.

import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Exhibition } from "./exhibitions.entity";

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

  @Column()
  name: string;

  @ManyToOne(() => Exhibition, (exhibition) => exhibition.guests)
  exhibition: Exhibition;
}

Auf der anderen Seite erweitern wir unsere Ausstellungs-Entität (exhibition) wie folgt:

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Guest } from "./guest.entity";

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  starts: Date;
  @Column()
  ends: Date;

  @Column()
  address: string;

  @OneToMany(() => Guest, (guest) => guest.exhibition)
  guests: Guest[];
}

Du erkennst Folgendes: Wenn wir den One-to-Many-Decorator auf der einen Seite verwenden, ist der One-to-Many-Decorator auf der anderen Seite Pflicht. Ohne diese Verknüpfung wird es nicht funktionieren.

Das funktioniert zwar schon, hat aber den Nachteil, dass es Gäste geben kann, die keinem Event zugeordnet sind. Hier kommt der dritte Parameter des ManyToOne-Decorators zum Einsatz.

@Entity()
export class Guest {
  //...
  @ManyToOne(() => Exhibition, (exhibition) => exhibition.guests, {
    nullable: false,
  })
  exhibition: Exhibition;
}

Auf diese Weise kann keine ‘Kind-Ressource’ erstellt werden, ohne die ‘Eltern-Ressource’ anzugeben.

Wenn du dir nun deine Datenbank anschaust, vorausgesetzt du hast synchronize: true, wirst du feststellen, dass wir eine Spalte in der Gäste-Tabelle angelegt haben, die die IDs der Ausstellungen enthalten soll. Um diese Spalte umzubenennen, kannst du den @JoinColumn-Decorator verwenden.

//...
  @ManyToOne(() => Exhibition, (exhibition) => exhibition.guests, {
    nullable: false,
  })
  @JoinColumn({
    name: 'exhibition_id',
  })
  exhibition: Exhibition;
//...

Wenn wir unsere Datenbank von Grund auf neu erstellen, ist der Einsatz des @JoinColumn-Decorators möglicherweise nicht zwingend erforderlich. Diese Funktionalitäten werden jedoch besonders nützlich, wenn wir eine bestehende Datenbank integrieren, die bereits ihre eigenen Konventionen zur Benennung von Feldern (Datenbankspalten) festgelegt hat.

Lesen von verbundenen Entitäten

Es ist Zeit für eine neue “playground”-Route, um zu testen, wie wir die verbundenen Daten auslesen können. Du kannst hierfür entweder einige Daten manuell eingeben oder meinen Seeder verwenden, der sich auf unsere Messedaten bezieht. Den Seeder für die Messen findest du im Beitrag zum Repository Pattern.

INSERT INTO guest (name, "exhibitionId") VALUES
  ('Annika Bremer', 1),
  ('Bert Hansen', 1),
  ('Anna Krömer', 1),
  ('Laura Stiehler', 1),
  ('Stefanie Mueller', 2),
  ('Robert Schulz', 3),
  ('Wolfgang Meyer', 4),
  ('Tom Mayerhofer', 5);

Nachdem du deine Daten in der Datenbank platziert hast, kannst du im Controller einfach eine neue Route erstellen. Falls du weitere Hintergrundinformationen und Kontext hierfür benötigst, keine Sorge. Alle wesentlichen Grundlagen stehen im Beitrag zu den Nest.js Constrollern.

// src/exhibitions/exhibitions.controller.ts
@Controller("/exhibitions")
export class ExhibitionsController {
  private readonly logger = new Logger(ExhibitionsController.name);

  constructor(
    @InjectRepository(Exhibition)
    private readonly repository: Repository<Exhibition>,
  ) {}

  @Get("/playground")
  async relationalQuerying() {
    return await this.repository.findOne({
      where: { id: 1 },
      loadEagerRelations: false,
    });
  }
}

Wenn du die “playground”-Route besuchst, wirst du feststellen, dass bisher keine relationalen Daten aus der Gästetabelle angezeigt werden. Ein erster Lösungsansatz besteht darin, das sogenannte “eager loading” in der Join-Eigenschaft der Ausstellungs-Entity zu verwenden. Hier ist der entsprechende Code:

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Guest } from "./guest.entity";

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

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  starts: Date;
  @Column()
  ends: Date;

  @Column()
  address: string;

  @OneToMany(() => Guest, (guest) => guest.exhibition, { eager: true })
  guests: Guest[];
}

Wenn alles wie erwartet funktioniert hat, wird deine API nun auf der Route /exhibitions/playground das folgende JSON zurückgeben.

{
  "id": 1,
  "name": "Künstlerische Wunder",
  "description": "Eine Erkundung zeitgenössischer Kunst und Kreativität.",
  "starts": "2022-03-10T13:00:00.000Z",
  "ends": "2022-03-10T17:00:00.000Z",
  "address": "Galerie Allee 123",
  "guests": [
    {
      "id": 11,
      "name": "Annika Bremer"
    },
    {
      "id": 12,
      "name": "Bert Hansen"
    },
    {
      "id": 13,
      "name": "Anna Krömer"
    },
    {
      "id": 14,
      "name": "Laura Stiehler"
    }
  ]
}

Das stellt zunächst einen Erfolg dar: Die relationalen Daten werden nun quasi automatisch geladen. Dennoch ist es wichtig, einen Hinweis zu beachten. Das pauschale Setzen aller Relationen auf “eager”, ohne einen sehr guten Grund dafür zu haben, kann schnell zu Performanceproblemen führen.

// src/exhibitions/exhibitions.controller.ts
@Controller("/exhibitions")
export class ExhibitionsController {
  private readonly logger = new Logger(ExhibitionsController.name);

  constructor(
    @InjectRepository(Exhibition)
    private readonly repository: Repository<Exhibition>,
  ) {}

  @Get("/playground")
  async relationalQuerying() {
    return await this.repository.findOne({
      where: { id: 1 },
      loadEagerRelations: false, // <-
    });
  }
}

Selbstverständlich ist es auch möglich, die Relationen andersherum zu handhaben. Falls du die Relation nicht auf “load eager” gesetzt hast, kannst du deinen Controller-Methoden die gewünschten Beziehungen in Form eines Arrays übergeben. Hier ein Beispiel:

// Definition der Relation ohne load eager in der entity
@Entity()
export class Exhibition {
  // ..
  @OneToMany(() => Guest, (guest) => guest.exhibition)
  guests: Guest[];
}

// controller action
  @Get('/playground')
  async relationalQuerying() {
    return await this.repository.findOne({
      where: { id: 1 },
      relations: ['guests'],
    });
  }

Many-to-Many-Beziehung

Neben den One-to-One- und One-to-Many-Beziehungen ist die Many-to-Many-Beziehung eine weitere wichtige Form der Entitätsverknüpfung. Diese Art der Beziehung tritt auf, wenn eine Entität mit mehreren anderen Entitäten in Verbindung stehen kann und umgekehrt.

Einrichtung

Angenommen, wir haben die Entitäten Teacher und Subject. Ein Lehrer kann mehrere Unterrichtsfächer unterrichten, und ein Fach kann von verschiedenen Lehrern unterrichtet werden. Hier ist, wie du dies in Nest.js mit TypeORM umsetzen könntest:

Lehrer-Entität (teacher.entity.ts)

import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Subject } from "./subject.entity";

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

  @Column()
  name: string;

  @ManyToMany(() => Subject, (subject) => subject.teachers)
  subjects: Subject[];
}

Fach-Entität (subject.entity.ts)

import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
import { Teacher } from "./teacher.entity";

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

  @Column()
  name: string;

  @ManyToMany(() => Teacher, (teacher) => teacher.subjects)
  teachers: Teacher[];
}

In diesen Entitäten verwenden wir den ManyToMany-Decorator, um die Beziehung zu definieren. Jede Entität verweist auf die andere, und es entsteht eine bidirektionale Beziehung.

Verwendung

Nachdem die Entitäten eingerichtet sind, kannst du die Many-to-Many-Beziehung in deinen Controllern oder Services nutzen. Hier ein Beispiel:

// Lehrer und Fächer abrufen
const teacher = await teacherRepository.findOne({
  where: { id: 1 },
  relations: ["subjects"],
});

// Fächer und zugehörige Lehrer abrufen
const subject = await subjectRepository.findOne({
  where: { id: 1 },
  relations: ["teachers"],
});

Mit diesem Abschnitt hast du nun auch die Grundlagen für Many-to-Many-Beziehungen in Nest.js mit TypeORM abgedeckt. Viel Spaß beim Entdecken und Implementieren dieser leistungsstarken Verknüpfungsmöglichkeiten!

Fazit

Herzlichen Glückwunsch! Du hast nun einen fundierten Einblick in die Gestaltung von Datenbankrelationen in Nest.js mit TypeORM erhalten. Dieses Wissen ermöglicht es dir, effektiv und flexibel mit Entitätsverbindungen umzugehen und Datenbankbeziehungen in deiner Nest.js-Anwendung erfolgreich zu managen.

Die verschiedenen Verbindungsarten - One-to-One, One-to-Many und Many-to-Many - bieten eine strukturierte Grundlage für die Entwicklung von Datenbankbeziehungen, wodurch du deine Anwendungslogik effizient gestalten kannst.

Insbesondere die Definition von Verbindungen in Nest.js ermöglicht es dir, Entitäten und alle verbundenen Entitäten mühelos zu laden. Die vorgestellten Konzepte und praktischen Beispiele, wie die One-to-Many-Beziehung zwischen Ausstellungen und Gästen, veranschaulichen, wie diese Techniken in der Praxis angewendet werden können.

Abschließend sei darauf hingewiesen, dass das pauschale Setzen aller Relationen auf “eager” ohne guten Grund zu Performanceproblemen führen kann. Daher ist es ratsam, die gewünschten Relationen gezielt zu optimieren und dabei die Performance im Blick zu behalten.

Natürlich ist dies erst der Anfang, und es gibt noch viele weitere Konzepte zu entdecken, wie zum Beispiel das Cascade-Konzept. Diese Grundlagen legen jedoch den soliden Grundstein für dein vertieftes Verständnis relationaler Daten in Nest.js. Viel Erfolg beim Entdecken weiterer Möglichkeiten zur Verknüpfung von Entitäten!

0
0 Bewertungen

Jetzt selbst bewerten