Table of Contents
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 (a TypeORM-mel nem csak relációs adatbázist lehet kezelni, hanem NoSQL-t is, pl. MongoDB-t).
Használatát egy példán keresztül érdemes megmutatni.
Telepítés
Hozzunk létre egy projekt könyvtárat és belépve (cd /path/to/ORMExample/) adjuk ki a következő parancsokat:
Az első parancsnál minden kérdésre nyomjunk entert.
npm init npm install typeorm@0.2.45 --save
Létrejön lokálisan a node_modules/ könyvtár és a package.json, valamint a package-lock.json fájlok.
Telepítsük a kiegészítőit:
npm install reflect-metadata --save npm install @types/node --save npm install typescript --save npm link typescript
Telepítsük a választott adatbázis driver-ét:
npm install sqlite3 --save
Kiegészítés
Ha MySQL-t vagy MariaDB-t használunk:
npm install mysql --save
Ha PostgreSQL-t használunk:
npm install pg --save
Ha Microsoft SQL Servert használunk:
npm install mssql --save
Ha MongoDB-t használunk:
npm install mongodb --save
Projekt létrehozása
Adjuk ki a következő parancsot (a node_modules\.bin könyvtár gyűjti a végrehajtható állományokat, nézzük meg jelenleg mik vannak benne):
.\node_modules\.bin\typeorm init
A következő fájlstruktúra jött létre:
├──> src │ ├──> entity │ │ └──> User.ts │ ├──> migration │ └──> index.ts ├──> node_modules ├──> ormconfig.json ├──> package.json ├──> package-lock.json └──> tsconfig.json
- src - a TypeScript forráskódot tartalmazza
- index.ts - Az alkalmazás belépési pontja. Ez a .ts állomány indul el futtatáskor
- entity - Ez a könyvtár tartalmazza az adatbázis modelleket
- migration - ez a könyvtár tartalmazza az adatbázis migrációs szkripteket. Ez azért kell, mert minden DB szerkezeti módosításnál ebben lesznek megadva azok a DB specifikus parancsok amik megvalósítják a konkrét struktúrákat vagy azok változtatását
- node_modules - a helyi modulok, az alkalmazás által használt minden komponens, amit letöltünk
- ormconfig.json - TypeORM konfigurációs állománya
- tsconfig.json - TypeScript compiler beállítások
Töltsük le az UwAmp elnevezésű hordozható WAMP szervert: https://www.uwamp.com/file/UwAmp.zip Ezt ki kell csomagolni egy könyvtárba és elindítani az uwamp.exe-t.
Nyissuk meg az ormconfig.json-t, és módosítsuk a mysql elérést az alábbiak szerint:
{ "type": "mysql", "host": "localhost", "port": 3306, "username": "root", "password": "root", "database": "teszt", "synchronize": true, "logging": false, "entities": [ "src/entity/**/*.ts" ], "migrations": [ "src/migration/**/*.ts" ], "subscribers": [ "src/subscriber/**/*.ts" ], "cli": { "entitiesDir": "src/entity", "migrationsDir": "src/migration", "subscribersDir": "src/subscriber" } }
Hozzunk létre a MySQL-ben egy “teszt” nevű adatbázist, nyissuk meg a http://localhost/phpmyadmin vagy http://localhost/mysql lapot:
- alapértelmezett felhasználó: root
- alapértelmezett jelszó: root
Futtassuk a következő parancsot:
npm start
Ha hiba nélkül lefutott, akkor az UwAmp PHPMyAdmin felületén megtekinthetjük a létrejött 1 sort az adatbázisban.
A folytatáshoz töröljük le a teszt adatbázisban létrejött táblát, majd az ormconfig.json fájlban a “synchronize” legyen false:
"synchronize": false,
Futtassuk le még egyszer a npm start parancsot, most látható hogy a tábla nem jött létre.
Adatbázis migráció
Az adatbázis módosításainak nyomonkövetésére a migráció nyújt lehetőséget. Nyissuk meg a package.json fájlt és a “scripts” részt módosítsuk az alábbi módon:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "ts-node src/index.ts", "typeorm-migration-generate": "ts-node ./node_modules/typeorm/cli.js migration:generate -n ", "typeorm-migration-run": "ts-node ./node_modules/typeorm/cli.js migration:run", "typeorm-migration-revert": "ts-node ./node_modules/typeorm/cli.js migration:revert" },
Az adatbázis első változatát a következő paranccsal lehet létrehozni (a user_tabla egy szabadon választott név, ez azonosítja az első változatot):
npm run typeorm-migration-generate user_tabla
Az src/migration alkönyvtárban létrejött állomány tartalma ez lesz:
import {MigrationInterface, QueryRunner} from "typeorm"; export class userTabla1615018618129 implements MigrationInterface { name = 'userTabla1615018618129' public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query("CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `firstName` varchar(255) NOT NULL, `lastName` varchar(255) NOT NULL, `age` int NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB"); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query("DROP TABLE `user`"); } }
A typeorm legenerálta a tábla létrehozásához és eldobásához szükséges két függvényt. Ezzel még nincs továbbra sem létrehozva a valódi tábla, ezért a következő paranccsal alkalmazzuk a migrációt.
npm run typeorm-migration-run
Ezzel létrejött a két tábla:
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'teszt' AND `TABLE_NAME` = 'migrations' query: CREATE TABLE `teszt`.`migrations` (`id` int NOT NULL AUTO_INCREMENT, `timestamp` bigint NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB query: SELECT * FROM `teszt`.`migrations` `migrations` ORDER BY `id` DESC 0 migrations are already loaded in the database. 1 migrations were found in the source code. 1 migrations are new migrations that needs to be executed. query: START TRANSACTION query: CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `firstName` varchar(255) NOT NULL, `lastName` varchar(255) NOT NULL, `age` int NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB query: INSERT INTO `teszt`.`migrations`(`timestamp`, `name`) VALUES (?, ?) -- PARAMETERS: [1615018618129,"userTabla1615018618129"] Migration userTabla1615018618129 has been executed successfully. query: COMMIT
Mint látható, egy migrations nevű tábla is létrejött ami tartalmazza az aktuális db változat nevét.
Módosítsuk a User.ts-t a src/entity könyvtárban. Adjuk hozzá egy email mezőt:
@Column() email: string;
Ezután az index.ts-ben a 11. sor után szúrjunk be egy email címet:
user.email = 'eee@eee.com';
Majd npm start után természetesen hibát kapunk, mert nincs átvezetve a db-be a módosítás.
QueryFailedError: ER_BAD_FIELD_ERROR: Unknown column 'email' in 'field list'
Futtassuk a következő sort:
npm run typeorm-migration-generate user_email
Ezzel létrejön egy új fájl a src/migration/ könyvtárban.
import {MigrationInterface, QueryRunner} from "typeorm"; export class userEmail1615054071901 implements MigrationInterface { name = 'userEmail1615054071901' public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query("ALTER TABLE `user` ADD `email` varchar(255) NOT NULL"); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query("ALTER TABLE `user` DROP COLUMN `email`"); } }
Ha nem akarunk minden módosításkor migrációt készíteni (a próbálgatási időszakban) akkor érdemes a ormconfig.json-ben visszaállítani a “synchronize”: true-t. Tegyük is meg a következő példák előtt.
Entitások - Relációk
A gyakorlatban a tábláink kapcsolatban vannak egymással. Három alaprelációt különböztetünk meg:
- one-to-one - egy az egy reláció, ahol két táblát úgy kapcsolunk össze, hogy 1 sor csak 1 sornak felelhet meg mindkét táblában. Ilyen ha pl. Országok és a Fővárosok táblákat tekintjük, mivel egy országnak egy egy adott fővárosa lehet és minden főváros csak 1 országnak lehet a fővárosa.
- one-to-many - egy/több reláció, egy adott sor, több sorral is össze van kapcsolva. Tekintsük a Kutyák és Gazdák táblát, ahol 1 kutyának csak 1 gazdája van, de egy gazdának több kutyája is lehet.
- many-to-one - ugyanaz mint az előző csak fordított relációban.
- many-to-many - több/több reláció, egy adott sor, több sorral is össze van kötve és fordítva. Ilyen pl. a Felhasználók és Szerepkörök tábla, ahol egy felhasználónak több szerepköre is lehet és egy szerepkör több felhasználóhoz is tartozhat.
Az ORM-ek, így a TypeORM is a következő fogalmakat/módszereket használja a relációk használatánál.
- eager - a forrás entitás betölti a relációkhoz tartozó összes adatot. Ez azt jelenti, hogy ha a kutyák és gazdákra gondolunk, akkor a gazda betöltésekor a kutyái is betöltődnek. Ez természetesen bonyolultabb relációknál is érvényes. Ezért kell kézzel megadni, mert nagyobb adatbázison nem feltétlenül akarjuk betölteni automatikusan a kapcsolt adatot.
- cascade - A cél entitás frissül vagy hozzáadódik automatikusan, ha a forrás változik. (példa alapján érthető lesz később)
- onDelete - A cél entitás törlődik, ha a forrást törlik.
One-to-Many példa
Adjunk hozzá két entitást a src/entity/ mappához: Dog.ts és Owner.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; }
A Dog.ts owner adattagja feletti dekorátor jelzi, hogy “Több kutyának egy tulajdonosa lehet”. Az első paraméter azt jelenti, hogy a reláció az Owner objektumra mutat. A második paraméter jelzi, hogy az owner osztályban a dogs adattaggal lesz összekapcsolva.
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.ts dogs adattagja feletti dekorátor jelzi, hogy “Egy gazdának több kutyája lehet”. Az első paraméter azt jelenti, hogy a reláció az Dog objektumra mutat. A második paraméter jelzi, hogy az dog osztályban az owner adattaggal lesz összekapcsolva.
Módosítsuk továbbá az index.ts-t.
import "reflect-metadata"; import { createConnection } from "typeorm"; import { Dog } from "./entity/Dog"; import { Owner } from "./entity/Owner"; createConnection().then(async connection => { const owner = new Owner(); owner.name = "owner1"; const dog = new Dog(); dog.name = 'Bodri'; owner.dogs = [dog]; await connection.manager.save(owner); console.log("done."); }).catch(error => console.log(error));
Futtassuk le a kódot és nézzük meg 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öjjön a kutya is, az Owner.ts-ben módosítsuk a relációt:
@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. Viszont mi történik 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 ezt a sort, és futtassuk az alkalmazást:
await connection.manager.remove(owner);
A hibaüzenet azt jelenti, hogy nem tudja letörölni a gazdát, mert ekkor a kutya táblában olyan idegen kulcs maradna ami nem mutat egyetlen tulajdonosra sem.
QueryFailedError: ER_ROW_IS_REFERENCED_2: Cannot delete or update a parent row: a foreign key constraint fails (`teszt`.`dog`, CONSTRAINT `FK_2cd931b431fa086ee81e43ec5da` FOREIGN KEY (`ownerId`) REFERENCES `owner` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION)
Ezt megoldhatjuk úgy, hogy a kutya tulajdonosát null-ra állítjuk és utána törlünk, de automatikusan is az alábbi módon:
@ManyToOne(type => Owner, owner => owner.dogs, { onDelete: 'CASCADE', }) owner: Owner;
Azaz “kaszkádolt törlést” definiálunk a kapcsolatnál.
Many-to-Many példa
Az ORM-ek erőssége akkor lesz érezhetőbb, ha például a több-több reláció esetén a kapcsolótáblát is létrehozzuk.
Klasszikus példa a user-roles reláció.
Hozzuk létre a User.ts és Role.ts fájlokat a következő tartalommal:
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm"; @Entity() export class Role { @PrimaryGeneratedColumn() id: number; @Column() name: string; }
Látható, hogy a Role.ts forrásában semmi újdonság nincs.
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 User.ts alapján látható hogyan lehet több-több kapcsolatot megadni, most rögtön cascade típusúra definiáltuk a kapcsolatot.
Nézzük a index.ts példát, majd futtassuk le a kódot (npm start). Előtte nem árt az adatbázist kipucolni, törölni az összes táblát.
import "reflect-metadata"; import { createConnection } from "typeorm"; import { Role } from "./entity/Role"; import { User } from "./entity/User"; createConnection().then(async connection => { 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 connection.manager.save(user); console.log("done."); }).catch(error => console.log(error));