User Tools

Site Tools


tanszek:oktatas:informatikai_rendszerek_epitese:type_orm

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));
tanszek/oktatas/informatikai_rendszerek_epitese/type_orm.txt · Last modified: 2022/03/23 09:42 by 127.0.0.1