TypeORM

TypeORMとは

TypeScript(他にもNodeJSなど対応)で使えるORM(オブジェクト関係マッピング

ORMとは

オブジェクトとデータベースの間の関係を定義(マッピング)することで、SQLを直に書かなくてもデータベースのアクセスができる

クイックスタート

①$npm i typeorm -g TypeORMをグローバルにインストール

②$typeorm init --name MyProject --databse mysql 今回はプロジェクト名をMyProject、データベースはmysqlを使う設定

③以下がファイル構成

MyProject
├── src              // typescriptのコードを書く場所
│   ├── entity       // エンティティフォルダ
│   │   └── User.ts  // サンプルのエンティティファイル
│   ├── migration    // マイグレーションの保存場所
│   └── index.ts     // アプリケーションの開始地点
├── .gitignore       // gitignoreファイル
├── ormconfig.json   // ORMとデータベースの接続設定
├── package.json     //ノードモジュールの依存関係
├── README.md        // readmeファイル
└── tsconfig.json    // typescriptのコンパイラーオプション

※エンティティとは実体のことでデータを指す

④$cd MyProject 作成したプロジェクトのディレクトリに移動

⑤$npm instal 必要なnode modulelをインストール

⑥"ormconfig.json"を編集 データベースの接続先の環境に合わせてormconfig.jsonの設定を行う。ほとんどの場合はhost, username, passwprd,database,portの設定をすれば接続出来る。設定の一例は以下の通り。

{
   "type": "mysql",
   "host": "localhost",
   "port": 3306,
   "username": "root",
   "password": "",
   "database": "test",
   "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"
   }

6.npm start アプリケーションを実行

設定したデータベースを確認するとサンプルデータが確認出来る

モデルの作成

TypORMでデータベースにテーブルを作成するには、モデルを使う。 例としてentityフォルダにPhoto.tsファイルを作成する。

Photo.ts

export class Photo {
    id: number;
    name: string;
    description: string;
    filename: string;
    views: number;
}

エンティティの作成

Photoモデルにエンティティを作成。エンティティを操作することで、データベースへデータの更新・追加・削除・読み込みを行う。

・エンティティを作成する為に@Entityデコレータを使用 ・データベースの追加は@Columnデコレータを使用して行う ・エンティティには少なくても1つの主キーが必要となる ・主キーの設定は@PrimaryColumnデコレータを使用 ・データ型はデコレータの()の中に設定

Photo.ts

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class Photo{

    @PrimaryGeneratedColumn()
    id: number;

    @Column({
        length: 100
    })
    name: string;

    @Column("text")
    description: string;
    
    @Column()
    filename: string;
    
    @Column()
    view: number;

    @Column()
    isPublished: boolean;
}

Entity Managerによるデータの操作

テーブルを作成しデータをPhotoに追加・変更・削除・読み込みする為にEntity Managerを使う

追加

connection.manager.saveでphotoに設定したデータをPhotoデータに追加

index.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.views = 1;
    photo.isPublished = true;

    await connection.manager.save(photo);
    console.log("Photo has been saved");

}).catch(error => console.log(error));

読み込み

connection.manager.findでデータの読み込みを行なっている

index.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let savedPhotos = await connection.manager.find(Photo);
    console.log("All photos from the db: ", savedPhotos);

}).catch(error => console.log(error));

Repositoryによるデータの操作

複数のエンティティを使用する場合はEntityManagerの代わりにRepositoryを使用する

追加/読み込み

connection.getRepositoryでデータを追加し、await photoRepository.findでデータを読み込みを行う。

Photo.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.views = 1;
    photo.isPublished = true;

    let photoRepository = connection.getRepository(Photo);

    await photoRepository.save(photo);
    console.log("Photo has been saved");

    let savedPhotos = await photoRepository.find();
    console.log("All photos from the db: ", savedPhotos);

}).catch(error => console.log(error));

更新

photoRepository.findOneで読み込み photoToUpdate.nameで内容を変更 photoRepository.save(photoToUpdate)でデータを更新

index.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToUpdate = await photoRepository.findOne(1);
    photoToUpdate.name = "Me, my friends and polar bears";
    await photoRepository.save(photoToUpdate);

}).catch(error => console.log(error));

削除

photoRepository.findOneでデータを読み込み photoRepository.remove(photoToRemove)で指定したデータを削除

index.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";

createConnection(/*...*/).then(async connection => {

    /*...*/
    let photoToRemove = await photoRepository.findOne(1);
    await photoRepository.remove(photoToRemove);

}).catch(error => console.log(error));

一対一リレーション

他のクラスとの一対一の関係を作成する。ここではentityフォルダにPhotoMetadata.tsを以下の様に作成する。

・@OneToOneデコレータを使用し一対一の関係を作成 ・@OneToOne(type => Photo)で関係を作りたいクラスを指定 ・@JoinColumnデコレータで単方向の関係を作成(関係の所有者側で指定する必要がある)

PhotoMetadata.ts

import {Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class PhotoMetadata {

    @PrimaryGeneratedColumn()
    id: number;

    @Column("int")
    height: number;

    @Column("int")
    width: number;

    @Column()
    orientation: string;

    @Column()
    compressed: boolean;

    @Column()
    comment: string;

    @OneToOne(type => Photo)
    @JoinColumn()
    photo: Photo;
}

次にPhotoとPhotoMetadataにデータを追加する。

index.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {PhotoMetadata} from "./entity/PhotoMetadata";

createConnection(/*...*/).then(async connection => {

    // create a photo
    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.isPublished = true;

    // create a photo metadata
    let metadata = new PhotoMetadata();
    metadata.height = 640;
    metadata.width = 480;
    metadata.compressed = true;
    metadata.comment = "cybershoot";
    metadata.orientation = "portait";
    metadata.photo = photo; // this way we connect them

    // get entity repositories
    let photoRepository = connection.getRepository(Photo);
    let metadataRepository = connection.getRepository(PhotoMetadata);

    // first we should save a photo
    await photoRepository.save(photo);

    // photo is saved. Now we need to save a photo metadata
    await metadataRepository.save(metadata);

    // done
    console.log("Metadata is saved, and relation between metadata and photo is created in the database too");

}).catch(error => console.log(error));

上記の様に単方向のみの関係ではなくPhotoMetadataとPhotoとの関係を双方向にするのには以下の通り。

PhotoMetadata.ts

import {Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class PhotoMetadata {

    @PrimaryGeneratedColumn()
    id: number;

    @Column("int")
    height: number;

    @Column("int")
    width: number;

    @Column()
    orientation: string;

    @Column()
    compressed: boolean;

    @Column()
    comment: string;

    @OneToOne(type => Photo, photo => photo.metadata)
    @JoinColumn()
    photo: Photo;

}

Photo.ts

import { Entity, Column, PrimaryGeneratedColumn,OneToOne } from "typeorm"
import {PhotoMetadata} from "./PhotoMetadata";
@Entity()
export class Photo{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;
    
    @Column()
    filename: string;

    @Column()
    isPublished: boolean;

    @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo)
    metadata: PhotoMetadata;
}

一対多リレーション

一対一の場合、1つのレコードが他の1つのレコードに関連づけられていたが、一体多の場合は1つのレコードが他の複数のレコードに関連づけられる。

ここでは新しくAuthorクラスをentityフォルダに作成し(entityフォルダにAuthorファイルを作成)Photoクラスとindexを以下の様に設定する。

Author.ts

import {Entity, Column, PrimaryGeneratedColumn, OneToMany, JoinColumn} from "typeorm";
import {Photo} from "./Photo";

@Entity()
export class Author {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below
    photos: Photo[];
}

Photo.ts

import { Entity, Column, PrimaryGeneratedColumn,ManyToOne } from "typeorm"
import {Author} from "./Author";
@Entity()
export class Photo{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;
    
    @Column()
    filename: string;

    @Column()
    isPublished: boolean;

    @ManyToOne(type => Author, author => author.photos)
    author: Author;
}

index.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {Author} from "./entity/Author";

createConnection(/*...*/).then(async connection => {

    let photo = new Photo();
    photo.name = "Me and Bears";
    photo.description = "I am near polar bears";
    photo.filename = "photo-with-bears.jpg";
    photo.isPublished = true;

    let author = new Author();
    author.name = "Tom";
   
    let photoRepository = connection.getRepository(Photo);
    let metadataRepository = connection.getRepository(Author);
    
    await photoRepository.save(photo);
    await metadataRepository.save(author);
    
    console.log("Metadata is saved, and relation between metadata and photo is created in the database too");

}).catch(error => console.log(error));

多対多リレーション

複数のレコードが他のクラスの複数のレコードに関連づけられる。

index.ts

import {createConnection} from "typeorm";
import {Photo} from "./entity/Photo";
import {Album} from "./entity/Album";


createConnection(/*...*/).then(async connection => {

let album1 = new Album();
album1.name = "Bears";
await connection.manager.save(album1);

let album2 = new Album();
album2.name = "Me";
await connection.manager.save(album2);

let photo = new Photo();
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.albums = [album1, album2];
await connection.manager.save(photo);

const loadedPhoto = await connection
    .getRepository(Photo)
    .findOne(1, { relations: ["albums"] });
    
console.log(loadedPhoto);

}).catch(error => console.log(error));

Photo.ts

import { Entity, Column, PrimaryGeneratedColumn,ManyToMany } from "typeorm"
import {Album} from "./Album";
@Entity()
export class Photo{

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column()
    description: string;
    
    @Column()
    filename: string;

    @ManyToMany(type => Album, album => album.photos)
    albums: Album[];
}

Album.ts

import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable} from "typeorm";
import { Photo } from "./Photo"
@Entity()
export class Album {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @ManyToMany(type => Photo, photo => photo.albums)
    @JoinTable()
    photos: Photo[];
}

QueryBuilderを使用

QueryBuilderを使用して、ほとんどすべての複雑なSQLクエリを作成できます。 たとえば、これを行うことができます。

let photos = await connection
    .getRepository(Photo)
    .createQueryBuilder("photo")
    .innerJoinAndSelect("photo.metadata", "metadata")
    .leftJoinAndSelect("photo.albums", "album")
    .where("photo.isPublished = true")
    .andWhere("(photo.name = :photoName OR photo.name = :bearName)")
    .orderBy("photo.id", "DESC")
    .skip(5)
    .take(10)
    .setParameters({ photoName: "My", bearName: "Mishka" })
    .getMany();

このクエリは、「My」または「Mishka」という名前の公開された写真をすべて選択します。 位置5(ページ区切りオフセット)から結果を選択し、10の結果(ページ区切り制限)のみを選択します。 選択結果はidの降順で並べられます。 写真のアルバムは左結合され、それらのメタデータは内部結合されます。