TypeORM

A TypeORM egy objektum-relációs leképező eszköz, aminek segítségével TypeScript osztályokat különböző adatbázisokkal tudjuk anélkül használni, hogy konkrét adatbázis-kezelő specifikus parancsokat használnánk (TypeORM segítségével nem csak relációs adatbázist lehet kezelni, hanem NoSQL-t - pl. MongoDB-t - is).

Használatát egy példán keresztül érdemes megismerni.

Globális függőségek telepítése

A következő parancsot egyszer kell futtatni, ennek hatására rendszer szinten települnek a szükséges csomagok:

npm install -g typeorm

Projekt inicializálás

Projekt struktúra

A projektben a következő fájlstruktúra jött létre:

.
├── package.json
├── package-lock.json
├── README.md
├── src
│   ├── data-source.ts
│   ├── entity
│   │   └── User.ts
│   ├── migration
│   └── index.ts
└── tsconfig.json

Nézzünk bele a létrejött fájlok tartalmába!

A MySQL-ben hozzunk létre egy infrend2023_typeorm adatbázist, majd nyissuk meg a data-source.ts-t, és módosítsuk a beállításokat az alábbiak szerint:

import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User"

export const AppDataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "root",
    database: "infrend2023_typeorm",
    synchronize: true,
    logging: true,
    entities: [User],
    migrations: [],
    subscribers: [],
});

FIGYELEM! A belépési adatok (username / password) mindenkinél eltérőek lehetnek. Jelszó nélküli kapcsolódás esetén a jelszót tartalmazó sort törölni kell!

Projekt indítása

Futtassuk az npm start parancsot!

Ha hiba nélkül lefutott, akkor a PHPMyAdmin felületén megtekinthetjük a létrejött 1 sort az adatbázisban.

Entitások és relációk

A tábláink gyakran kapcsolatban vannak egymással. Három alap relációt különböztetünk meg:

One-to-Many példa

Adjunk hozzá két entitást az src/entity/ mappához, Dog.ts és Owner.ts elnevezéssel!

Az 1:N relációt mindkét osztályban jelölnünk kell, a ManyToOne és OneToMany dekorátorokkal.

// Dog.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { Owner } from "./Owner";

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

    @Column()
    name: string;

    @ManyToOne(type => Owner, owner => owner.dogs)
    owner: Owner;
}

Az osztály owner adattagja feletti dekorátor jelzi, hogy „több kutyának egy tulajdonosa is lehet”. Az első paraméter jelzi, hogy a reláció az Owner osztályra mutat. A második paraméter jelzi, hogy az Owner osztályban a dogs adattaggal lesz összekapcsolva.

// Owner.ts
import {Entity, PrimaryGeneratedColumn, Column, OneToMany} from "typeorm";
import { Dog } from "./Dog";

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

    @Column()
    name: string;

    @OneToMany(type => Dog, dog => dog.owner)
    dogs: Dog[];
}

Az Owner osztály dogs adattagja feletti dekorátor jelzi, hogy „egy gazdának több kutyája is lehet”. Az első paraméter jelzi, hogy a reláció a Dog osztályra mutat. A második paraméter jelzi, hogy a Dog osztályban az owner adattaggal lesz összekapcsolva.

Módosítsuk a data-source.ts fájlt, az entities tömbhöz adjuk hozzá az új entitásokat: entities: [Dog, Owner].

Módosítsuk továbbá az index.ts fájlt!

import { AppDataSource } from "./data-source"
import { Dog } from "./entity/Dog";
import { Owner } from "./entity/Owner";

AppDataSource.initialize().then(async () => {
    const dog1 = new Dog();
    dog1.name = 'Bodri';

    const dog2 = new Dog();
    dog2.name = 'Buksi';

    const owner = new Owner();
    owner.name = "Kovács Lajos";
    owner.dogs = [ dog1, dog2 ];

    await AppDataSource.manager.save(owner);
    console.log("Owner is saved.");

    AppDataSource.destroy();
}).catch(error => {
    console.log(error);
    AppDataSource.destroy();
});

Futtassuk le a kódot (npm start), és nézzük meg, hogy mi jött létre az adatbázisban!

Látható, hogy az ORM rendszer létrehozta a táblákat, a dog táblában az owner-re mutató id-vel. Láthatjuk még azt is, hogy az owner táblában 1 sor van, de a dog táblában nem jött létre semmi.

Azért, hogy létrejöjjenek a kutyákhoz tartozó rekordok is, az Owner.ts-ben módosítsuk az 1:N relációt leíró dekorátort:

@OneToMany(type => Dog, dog => dog.owner, { cascade: true })

A cascade: true engedélyezi, hogy a gazda létrehozásakor a kutya is létrejöjjön. Mi történik viszont törlés esetén, ha egy gazdát törlünk? Próbáljuk ki!

Az index.ts-ben a save() után rögtön tegyük be a következő sort, és futtassuk újra az alkalmazást (npm start):

await AppDataSource.manager.remove(owner);

A kapott hibaüzenet azt jelenti, hogy a TypeORM nem tudja letörölni a gazdát, mert a dog táblában van olyan idegen kulcs, ami a törlendő gazdára mutat.

Cannot delete or update a parent row: a foreign key constraint fails (`infrend2023_typeorm`.`dog`, CONSTRAINT `FK_2cd931b431fa086ee81e43ec5da` FOREIGN KEY (`ownerId`) REFERENCES `owner` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)

A problémát megoldhatjuk úgy, hogy a kutyák tulajdonosát a törlés előtt null-ra állítjuk, de az alábbi módon beállítható az is, hogy a gazdával együtt a kutyák is törlésre kerüljenek (azaz „kaszkádolt törlés” történjen):

// Dog.ts

@ManyToOne(type => Owner, owner => owner.dogs, { onDelete: 'CASCADE' })
owner: Owner;

Many-to-Many példa

Az ORM-ek erőssége akkor válik érezhetőbbé, ha több-több reláció esetén kapcsolótáblát is létre kell hoznunk.

Klasszikus példa a Felhasználó - Szerepkör reláció.

Az src/entity mappában hozzuk létre a Role.ts fájlt a következő tartalommal:

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

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

    @Column()
    name: string;
}

Látható, hogy ebben a kódban semmi újdonság nincs. Most hozzuk létre a User.ts fájlt!

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from "typeorm";
import { Role } from "./Role";

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

    @Column()
    name: string;

    @ManyToMany(type => Role, { cascade: true })
    @JoinTable()
    roles: Role[];
}

A fenti kódban látható, hogyan lehet több-több kapcsolatot létrehozni. A User osztály esetében most rögtön cascade típusú kapcsolatot definiáltunk a szerepkörökre nézve.

Az újonnan létrehozott entitásokra hivatkozzunk a data-source.ts fájl megfelelő pontján: entities: [User, Role, Dog, Owner].

Ezt követően írjuk át az index.ts fájl tartalmát a következőre:

import { AppDataSource } from "./data-source"
import { Role } from "./entity/Role";
import { User } from "./entity/User";

AppDataSource.initialize().then(async () => {
    const roleAdmin = new Role();
    roleAdmin.name = "admin";

    const roleUser = new Role();
    roleUser.name = "user";

    const user = new User();
    user.name = "administrator";
    user.roles = [roleAdmin, roleUser];

    await AppDataSource.manager.save(user);
    console.log("User is saved.");

    AppDataSource.destroy();
}).catch(error => {
    console.log(error);
    AppDataSource.destroy();
});

Futtassuk a kódot (npm start), majd a PHPMyAdmin felületén ellenőrizzük, hogy mi történt az adatbázisban:

Létrejött egy user és egy role tábla a felhasználók és a szerepkörök tárolásához, valamint létrejött egy user_roles_role elnevezésű kapcsolótábla is, mely a több-több kapcsolatot valósítja meg az entitások között.