TypeORM - ORM, którego szukałam całe życie - podstawy

Już w poprzednim poście wspomniałam o TypeORM, ale nie odpuszczę dopóki nie podzielę się z wami szczęściem :) Odstawcie Sequelize, spuście wzrok z Mongoose, nadchodzi TypeORM!

Ej, czekaj, a co to jest ORM?

Jeśli wiesz, to może spokojnie poszukać następnego nagłówka, a jeśli nie to zdecydowanie jeden akapit nie wystarczy, żeby ogarnąć temat. ORM to skrót od Object-Relational Mapping, a koncepcja jest prosta: obiektowo odwzorować strukturę bazy danych oraz udostępnić przystępny interfejs do komunikacji.

Upraszczając: nie musisz wykonywać zapytań SQL tylko posługujesz się funkcjami udostępnionymi przez ORM. Jest to wycinek możliwości, ale jednocześnie główny cel.

Myślę, że poniższy przykład wyjaśni więcej niż tysiąc słów, więc czytajcie dalej :)

Dlaczego TypeORM?

Przede wszystkim to kop... produkt zainspirowany popularnymi rozwiązania takimi jak Hibernate,  Doctrine czy Entity Framework. Nie ma co wynajdywać koła na nowo za każdym razem: to co sprawdza się od wielu lat, przenosimy zarówno na front jak i JavaScriptowy backend.

Znana, prosta składnia to kolejna zaleta. W całości napisany jest w TypeScriptcie, chociaż oferuje wsparcie dla wszystkich wersji EMCAScript. Z powodzeniem używam go w projektach komercyjnych. Ma bardzo duże wsparcie jeśli chodzi o bazy, obsługuje migracje (włącznie z automatycznym generowaniem), listenery, mnóstwo platform (łącznie z frontem oraz mobilkami), nie boi się kilku typów i wielu połączeń z bazami naraz w dowolnej konfiguracji, kaskady działają cudownie i jest pełne wsparcie dla najpopularniejszych wzorców projektowych. A to tylko część jego możliwości!

Instalacja

Korzystając z managera paczek zainstalujmy TypeORM

npm install typeorm --save.

Musimy również zainstalować npm install reflect-metadata --save.

Oraz sterownik, ponieważ będziemy dzisiaj korzystać z bazy plikowej SQLite: npm install sqlite3 --save, żeby nie bawić się w stawianie bazy w tutorialu.

Po instalacji możemy skorzystać z wizarda TypeORM, ale niestety nadpisze on parę plików w istniejącym projekcie, więc zrobimy to ręcznie.

Po pierwsze plik konfiguracyjny! Jeśli w folderze głównym będzie istniał plik ormconfig.json, każde połączenie bez zdefiniowanego obiektu konfiguracyjnego będzie korzystało z tych danych

//ormconfig.json
{
    "type": "sqlite",
    "database": "mydb.sql",
    "synchronize": true,
    "entities": [
        "src/entity/**/*.ts"
    ]
}

Obsługa sqllite ma bardzo prosty plik konfiguracyjny, bowiem nie musimy podawać adresu, użytkownika ani hasła potrzebnego do połączenia.

Połączenie z bazą danych

Jak większość połączeń z bazą danych, te też odbywa się asynchronicznie. Mamy dostępną metodę do tworzenia połączenia, które przechowuje się w instancji biblioteki i jest to zalecana metoda pracy z TypeORM. Nie jest to jednak typowe połączenie tylko pula połączeń, której z racji na charakter aplikacji - strona internetowa - nie będziemy nigdy zamykać. Oczywiście TypeORM obsługuje również standardowe połączenia o czym można poczytać w dokumentacji.

Jak wygląda połączenie:

import {Story} from "../entity/Story";
import {Chapter} from "../entity/Chapter";
import {Author} from "../entity/Author";       

const connection = createConnection({
   type: "sqlite",
   database: "mydb.sql",
   synchronize: true,
   entities: [
       Story,
       Chapter,
       Author
      ]
   })
   .then((connection) => {/*tu magia sie dzieje */}) 
   .catch((error) => {console.log(error)} );

Wolę sobie podać encje ręcznie, szczególnie w czasie developmentu, gdy mam w repo jeszcze kilka z których obecnie nie korzystam :) więc przeniosłam config z .ormconfig tutaj, zdefiniowałam encje i jeszcze złapałam to w catcha.

Opcja synchronize to jedna z tych opcji, która dodaje release'om na produkcję mnóstwo adrenaliny. Jest bardzo niebezpieczna, ale przydatna podczas developmetu, ponieważ znika potrzeba migracji. Jeśli modele danych nie zgadzają się z bazą, to... tworzy bazę na nowo :)

    public static async init(): Promise<void | Connection> {
        const connection = createConnection()
        .catch((error) => {console.log(error)} );

        return await connection;
    }

A u góry pełna funkcja, która wywołana w głównym pliku będzie tworzyła pule połączeń podczas uruchamiania aplikacji.

Definiowanie modeli

ORM sam tworzy strukturę bazy, jeśli mu pozwolimy. Ba! W przypadku bazy plikowej nawet sam plik. Ale żeby wiedział co ma dokładnie stworzyć musimy mu zamodelować dane.

Dzisiejszy przypadek jest bardzo prosty:

  • Istnieje opowiadanie (Story), które ma rozdziały i jednego autora.
  • Istnieje autor, który ma wiele opowiadań
  • Istnieje rozdział, który należy do jednego opowiadania

Zaraz zauważycie, ale w encji właściwości opisujemy za pomocą zdefiniowanych w TypeORM dekoratorów :)

//story.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany, ManyToOne } from "typeorm";
import { Author } from "./Author";
import { Chapter } from "./Chapter";

@Entity()
export class Story extends BaseEntity {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;

    @OneToMany(type => Chapter, chapter => chapter.story)
    chapters: Chapter[];
    
    @ManyToOne(type => Author, author => author.stories)
    author: Author
}
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne } from "typeorm";
import { Story } from "./Story";

@Entity()
export class Chapter extends BaseEntity {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;

    @Column()
    content: string;

    @ManyToOne(type => Story, story => story.chapters)
    story: Story;
}
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany } from "typeorm";
import { Story } from "./Story";

@Entity()
export class Author extends BaseEntity {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @OneToMany(type => Story, story => story.author)
    stories: Story[]
}

Myślę, że składnia jest bardzo prosta. Jeśli klasa ma być encją w bazie to oznaczamy ją @Entity, jeśli kolumną to @Column(). Relacje oznaczamy @OneToMany @ManyToOne oraz @ManyToMany (tutaj wymagany jest dekorator @JoinTable, który pokaże właściciela).

Operacje na otwartej bazie :)

W tej chwili po odpaleniu aplikacji powinna utworzyć się pusta baza danych. Można ją wypełnić na wiele sposobów odpowiadających ulubionym wzorcom:

const me = new Author();
me.name = "Kamila";
me.save();

Albo używając repozytorium (najpopularniejsze wyjście):

import {getManager, getRepository} from "typeorm";
import { Author } from './entity/Author';
const me = new Author();
me.name = "Kamila";
getRepository(Author).save(me);

Istnieje jeszcze zapis za pomocą managera, ale to już zostawiam dociekliwym, dokumentacja jest bardzo przystępna :)

W ten sposób można zapisać każdy osobno, ale istnieje jeszcze bardzo fajny sposób, który pozwala nam zapisywać powiązane relacje... kaskadowo. Jest to bardzo niebezpieczne, ale jednocześnie - przy dobrym przemyśleniu odpowiednio małej aplikacji - niezwykle wygodne :)

Najpierw uzupełnimy model Story, który będziemy zapisywać tak, aby wkładał do bazy również zależności. Nie trzeba kaskadować wszystkich właściwości, ale w tym przypadku jest to bardzo wygodne:

//Story.ts
@OneToMany(type => Chapter, chapter => chapter.story, {cascade: true})
chapters: Chapter[];

@ManyToOne(type => Author, author => author.stories, {cascade: true})
author: Author

Teraz możemy wykonać zapis:

//indext.ts
const me = new Author();
me.name = "Kamila";
const myStory = new Story();
myStory.name = "Przygody kota";
myStory.description = "Kocia historia";
myStory.author = me;

const firstChapter = new Chapter();
firstChapter.name = "Początki";
firstChapter.description = "Poczatki kociej przygody";
firstChapter.content = "Dawno, dawno temu za górami i lasami...";
myStory.chapters = [firstChapter];
getRepository(Story).save(myStory);

Podsumowanie

Dzisiaj poznaliśmy podstawy TypeORM, mam nadzieje, że się podobało. Oczywiście jeden wpis to za mało, z pewnością możecie liczyć na kolejne z tej serii. Kawałek po kawałku będziemy składać aplikację internetową :)

Jak zwykle czekam na uwagi i komentarze, te pod wpisem z NodeJS były tak dobre, że najprawdopodobniej zastosuję je w obecnym projekcie. Dziękuję :)

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)