==== 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));