==== 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 { 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 { 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 { await queryRunner.query("ALTER TABLE `user` ADD `email` varchar(255) NOT NULL"); } public async down(queryRunner: QueryRunner): Promise { 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));