Multiple connections

複数の接続を使用

複数のデータベースを使用する為に、異なる接続を作成します。以下の例ではdb1Connection,db2Connectionの2つのデータベースの接続を行っています。

import {createConnections} from "typeorm";

const connections = await createConnections([{
    name: "db1Connection",
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "db1",
    entities: [__dirname + "/entity/*{.js,.ts}"],
    synchronize: true
}, {
    name: "db2Connection",
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "root",
    password: "admin",
    database: "db2",
    entities: [__dirname + "/entity/*{.js,.ts}"],
    synchronize: true
}]);

この方法で任意の数のデータベースに接続することができます。各データベースに独自の構成やエンティティ、ORMスコープの設定ができます。

接続ごとに、新しい接続インスタンスが作成されます。 作成する接続ごとに名前を指定する必要があります。

接続オプションは、ormconfigファイルからロードすることもできます。 ormconfigファイルからすべての接続をロードできます。

import {createConnections} from "typeorm";

const connections = await createConnections();

または、名前で作成する接続(db2Connection)を指定できます。

import {createConnection} from "typeorm";

const connection = await createConnection("db2Connection");

接続を操作したり特定の接続を取得するには接続名を指定する必要があります。

import {getConnection} from "typeorm";

const db1Connection = getConnection("db1Connection");
// you can work with "db1" database now...

const db2Connection = getConnection("db2Connection");
// you can work with "db2" database now...

この方法の利点は、異なるログイン資格情報、ホスト、ポート、データベースタイプで複数の接続を構成できることです。 欠点は、複数の接続インスタンスを管理・操作をする必要があることです。

単一の接続で複数のデータベースを使用

複数の接続を作成せずに単一の接続で複数のデータベースを使用する場合は、使用するエンティティごとにデータベース名を指定できます。

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

@Entity({ database: "secondDB" })
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

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

@Entity({ database: "thirdDB" })
export class Photo {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    url: string;

}

UserエンティティはsecondDBデータベース内に作成され、PhotoエンティティはthirdDBデータベース内に作成されます。 他のすべてのエンティティは、デフォルトの接続データベースに作成されます。

別のデータベースからデータを選択する場合は、エンティティを提供するだけです。

const users = await connection
    .createQueryBuilder()
    .select()
    .from(User, "user")
    .addFrom(Photo, "photo")
    .andWhere("photo.userId = user.id")
    .getMany(); // userId is not a foreign key since its cross-database request

このコードは、次のSQLクエリを生成します:

SELECT 
  * 
FROM 
  "secondDB"."user" "user", 
  "thirdDB"."photo" "photo" 
WHERE 
  "photo"."userId" = "user"."id"

エンティティの代わりにテーブルのパスを指定することもできます。

const users = await connection
    .createQueryBuilder()
    .select()
    .from("secondDB.user", "user")
    .addFrom("thirdDB.photo", "photo")
    .andWhere("photo.userId = user.id")
    .getMany(); // userId is not a foreign key since its cross-database request

この機能は、MySQLおよびMSSQLlデータベースでのみサポートされています。

単一の接続で複数のスキーマを使用する

アプリケーションで複数のスキーマを使用できます。各エンティティにスキーマを設定するだけです。

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

@Entity({ schema: "secondSchema" })
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

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

@Entity({ schema: "thirdSchema" })
export class Photo {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    url: string;

}

UserエンティティはsecondSchemaスキーマ内に作成され、PhotoエンティティはthirdSchemaスキーマ内に作成されます。 他のすべてのエンティティは、デフォルトの接続スキーマに作成されます。

別のスキーマからデータを選択する場合は、エンティティを提供するだけです。

const users = await connection
    .createQueryBuilder()
    .select()
    .from(User, "user")
    .addFrom(Photo, "photo")
    .andWhere("photo.userId = user.id")
    .getMany(); // userId is not a foreign key since its cross-database request

このコードは、次のSQLクエリを生成します:

SELECT 
  * 
FROM 
  "secondSchema"."question" "question", 
  "thirdSchema"."photo" "photo" 
WHERE 
  "photo"."userId" = "user"."id"

エンティティの代わりにテーブルのパスを指定することもできます。

const users = await connection
    .createQueryBuilder()
    .select()
    .from("secondSchema.user", "user") // in mssql you can even specify a database: secondDB.secondSchema.user
    .addFrom("thirdSchema.photo", "photo") // in mssql you can even specify a database: thirdDB.thirdSchema.photo
    .andWhere("photo.userId = user.id")
    .getMany();

この機能は、PostgreSQLおよびMSSQLデータベースでのみサポートされています。 mssqlでは、たとえば次のようにスキーマとデータベースを組み合わせることができます。

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

@Entity({ database: "secondDB", schema: "public" })
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

}

Replication

TypeORMを使用して読み取り/書き込みレプリケーションをセットアップできます。 レプリケーション接続設定の例:

{
  type: "mysql",
  logging: true,
  replication: {
    master: {
      host: "server1",
      port: 3306,
      username: "test",
      password: "test",
      database: "test"
    },
    slaves: [{
      host: "server2",
      port: 3306,
      username: "test",
      password: "test",
      database: "test"
    }, {
      host: "server3",
      port: 3306,
      username: "test",
      password: "test",
      database: "test"
    }]
  }
}

すべてのスキーマの更新および書き込み操作は、マスターサーバーを使用して実行されます。 検索メソッドまたは選択クエリビルダーによって実行されるすべての単純なクエリは、ランダムスレーブインスタンスを使用しています。

クエリビルダーによって作成されたSELECTで明示的にマスターを使用する場合は、次のコードを使用できます。

const masterQueryRunner = connection.createQueryRunner("master");
try {
    const postsFromMaster = await connection.createQueryBuilder(Post, "post")
        .setQueryRunner(masterQueryRunner)
        .getMany();
} finally {
      await masterQueryRunner.release();
}

QueryRunnerによって作成された接続は、明示的に解放する必要があることに注意してください。

レプリケーションは、MySQLPostgreSQLSQL Serverでサポートされています。

{
  replication: {
    master: {
      host: "server1",
      port: 3306,
      username: "test",
      password: "test",
      database: "test"
    },
    slaves: [{
      host: "server2",
      port: 3306,
      username: "test",
      password: "test",
      database: "test"
    }, {
      host: "server3",
      port: 3306,
      username: "test",
      password: "test",
      database: "test"
    }],
    
    /**
    * If true, PoolCluster will attempt to reconnect when connection fails. (Default: true)
    */
    canRetry: true,

    /**
     * If connection fails, node's errorCount increases.
     * When errorCount is greater than removeNodeErrorCount, remove a node in the PoolCluster. (Default: 5)
     */
    removeNodeErrorCount: 5,

    /**
     * If connection fails, specifies the number of milliseconds before another connection attempt will be made.
     * If set to 0, then node will be removed instead and never re-used. (Default: 0)
     */
     restoreNodeTimeout: 0,

    /**
     * Determines how slaves are selected:
     * RR: Select one alternately (Round-Robin).
     * RANDOM: Select the node by random function.
     * ORDER: Select the first node available unconditionally.
     */
    selector: "RR"
  }
}

Dappsの開発

概要

Dapps開発のフローをご紹介します。

今回作成するDappsはオーナーが作成した本棚の本を、ゲストがトークンを支払うことで閲覧することが出来る電子図書館をイメージしたプロダクトです。

概念モデル設計

  • ユーザーにはオーナー(貸主)とゲスト(借主)がいる
  • オーナーは依託金を支払うことで本棚を作成することができる
  • 本棚を作成すると、オーナーは所有する本のデータを保存することができる
  • ゲストはオーナーが設定した額のトークを支払うことで、本を閲覧することができる
  • 期間が過ぎると依託金と支払われたトークンがオーナーに送られる

概念データモデル

f:id:adrenaline2017:20190707102533j:plain

ユーザーエンティティ

  • Ethereumアカウントアドレス
  • パスワード
  • 報酬額

シェルフエンティティ

  • ユーザーID
  • コントラクトアドレス
  • シャルフ名
  • トークン残高
  • シェルフの期限
  • アクティブ状態

ブックエンティティ

  • ブックID
  • ブックに紐付くシェルフID
  • 閲覧者ID
  • 閲覧者のアドレス

f:id:adrenaline2017:20190911112610j:plain

トランザクション

シェルフの作成

  • トランザクションハッシュ
  • 送金額
  • 送信者アドレス
  • 受信者アドレス
  • 成功の可否
  • シェルの作成者ID
  • シェルフID
  • 作成時刻

預託

  • トランザクションハッシュ
  • 送信者アドレス
  • 受信者アドレス
  • 送金額
  • 成功の可否
  • 承認の可否
  • 預託者ID
  • 預託先シェルフID
  • 預託時刻

報酬の支払

  • トランザクションハッシュ
  • 送信者アドレス
  • 受信者アドレス
  • 送金額
  • 成功の可否
  • 承認の可否
  • ブックID
  • 支払い先シェルフID
  • 支払時刻

返金

  • トランザクションハッシュ
  • 送金者アドレス
  • 受信者アドレス
  • 送金額
  • 成功の可否
  • 承認の可否
  • シェルフID
  • 返金先のオーナーID
  • 返金時刻

本の追加

  • オーナーID
  • ブックID
  • 題名
  • 価格
  • 詳細
  • 追加時刻

論理データモデル

f:id:adrenaline2017:20190707102554j:plain

物理データモデルの設計

userテーブル

import {Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn, UpdateDateColumn, OneToMany} from "typeorm";
import { Shelf } from "./Shelf";

@Entity()
export class User {

    @PrimaryGeneratedColumn({
        type: 'bigint',
        unsigned: true,
        comment: 'user id',
    })
    readonly id: string;

    @Index('eth_address_UNIQUE', {unique: true})
    @Column('varchar',{
        length: '255',
        nullable: false,
        comment: 'ethereum address',
    })
    eth_address: string;

    @Column('varchar',{
        length: '255',
        default: null,
        comment: 'password',
    })
    encrypted_password: string | null = null; 

    @Column('varchar',{
        length: '255',
        default: null,
        comment: 'solt',
    })
    salt: string | null = null;

    @Column('bigint', {
        unsigned: true,
        nullable: false,
        default: 0,
        comment: 'reword amount',
    })
    reword_amount: string;

    @CreateDateColumn({
        name: 'created_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: 'created date',
      })
      readonly createdAt?: Date;
    
      @UpdateDateColumn({
        name: 'updated_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: 'update date',
      })
      readonly updatedAt?: Date;

      @OneToMany((type) => Shelf, (shelf) => shelf.user)
      readonly shelfs?: Shelf[];
}

shelfテーブル

import { Entity, PrimaryGeneratedColumn, Index, Column, ManyToOne, JoinColumn, OneToMany } from "typeorm";
import { User } from "./User";
import { Book } from "./Book";

@Entity()
export class Shelf {

    @PrimaryGeneratedColumn({
        type: 'bigint',
        unsigned: true,
        comment: 'shelf id',
    })
    readonly id: string;

    @Index('event_code_UNIQUE', {unique: true})
    @Column('varchar', {
        length: '255',
        default: null,
        comment: 'event code'
    })
    event_code: string | null = null;

    @Column('bigint', {
        name: 'user_id',
        unsigned: true,
        nullable: false,
        comment: 'user id',
    })
    user_id: string

    @Column('varchar', {
        length: '255',
        default: null,
        comment: 'owner address',
    })
    owner_address: string | null = null;
    
    @Column('varchar', {
        length: '255',
        default: null,
        comment: 'transaction hash',
    })
    create_tx_hash: string | null = null;

    @Column('bigint', {
        unsigned: true,
        default: 0,
        comment: 'amount(wei)'
    })
    wei_balance: string;

    @Column('bigint', {
        default: null,
        comment: 'start date',
    })
    start_date: Date | null = null;

    @Column('bigint', {
        default: null,
        comment: 'end date',
    })
    end_date: Date | null = null;

    @ManyToOne((type) => User, (user) => user.shelfs)
    @JoinColumn({name: 'user_id', referencedColumnName: 'id'})
    readonly user?: User;

    @OneToMany((type) => Book, (book) => book.books)
      readonly books?: Book[];
}

bookテーブル

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Shelf } from './Shelf';

@Entity()
export class Book {

    @PrimaryGeneratedColumn({
        type: 'bigint',
        unsigned: true,
        comment: 'book id',
    })
    public id: string;

    @Column('bigint', {
        name: 'shelf_id',
        unsigned: true,
        nullable: false,
        comment: 'shelf id',
    })
    shelf_id: string;

    @Column('bigint', {
        unsigned: true,
        default: null,
        comment: 'borrower id'
    })
    borrower_id: string | null = null;

    @Column('varchar',{
        length: '255',
        nullable: false,
        comment: 'borrower address',
    })
    borrower_address: string;

    @CreateDateColumn({
        name: 'created_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: 'created date',
      })
      readonly createdAt?: Date;
    
    @UpdateDateColumn({
        name: 'updated_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: 'updated date',
    })
    readonly updatedAt?: Date;

    @ManyToOne((type) => Shelf, (shelf) => shelf.books)
    @JoinColumn({name: 'shelf_id', referencedColumnName: 'id'})
    readonly books?: Shelf[];
}

Paymantテーブル

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";

@Entity()
export class Payment {

    @PrimaryGeneratedColumn({
        type: 'bigint',
        unsigned: true,
        comment: 'deposit id',
    })
    readonly id?: string;
    
    @Column('varchar',{
        length: '255',
        default: null,
        comment: 'transaction hash',
    })
    tx_hash: string | null = null;

    @Column('varchar',{
        length: '255',
        nullable: false,
        default: '0x0',
        comment: 'sender',
    })
    sender: string;

    @Column('varchar',{
        length: '255',
        nullable: false,
        default: '0x0',
        comment: 'receiver',
    })
    receiver: string;

    @Column('bigint',{
        unsigned: true,
        default: 0,
        comment: 'amount(wei)',
    })
    wei_amount: string;

    @Column('tinyint',{
        default: 0,
        comment: 'success flag',
    })
    success: boolean;

    @Column('tinyint',{
        default: 0,
        comment: 'confirmed flag',
    })
    confirmed: boolean;

    @Column('bigint', {
        unsigned: true,
        nullable: false,
        comment: 'shelf id',
    })
    shelf_id: string;

    @CreateDateColumn({
        name: 'created_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: '作成日',
      })
      readonly createdAt?: Date;
    
    @UpdateDateColumn({
        name: 'updated_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: '更新日',
    })
    readonly updatedAt?: Date;
}

Depositテーブル

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";

@Entity()
export class Deposit {

    @PrimaryGeneratedColumn({
        type: 'bigint',
        unsigned: true,
        comment: 'deposit id',
    })
    readonly id?: string;
    
    @Column('varchar',{
        length: '255',
        default: null,
        comment: 'transaction hash',
    })
    tx_hash: string | null = null;

    @Column('varchar',{
        length: '255',
        nullable: false,
        default: '0x0',
        comment: 'sender',
    })
    sender: string;

    @Column('varchar',{
        length: '255',
        nullable: false,
        default: '0x0',
        comment: 'receiver',
    })
    receiver: string;

    @Column('bigint',{
        unsigned: true,
        default: 0,
        comment: 'amount(wei)',
    })
    wei_amount: string;

    @Column('tinyint',{
        default: 0,
        comment: 'success flag',
    })
    success: boolean;

    @Column('tinyint',{
        default: 0,
        comment: 'confirmed flag',
    })
    confirmed: boolean;

    @Column('bigint', {
        unsigned: true,
        nullable: false,
        comment: 'shelf id',
    })
    shelf_id: string;

    @CreateDateColumn({
        name: 'created_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: '作成日',
      })
      readonly createdAt?: Date;
    
    @UpdateDateColumn({
        name: 'updated_at',
        type: 'timestamp',
        default: () => 'CURRENT_TIMESTAMP',
        precision: 0,
        comment: '更新日',
    })
    readonly updatedAt?: Date;
}

スマートコントラクトの設計

機能の洗い出し

ShelfFactoryコントラクトとShelfコントラクトの各機能を洗い出す。

  • ShelfFactoryコントラクト 本棚を作成する機能

  • Shelfコントラクト 本棚への預託機能 オーナーへの返金機能 本棚の活性化・非活性化機能

Transaction

Transactionの作成と使用

Connectionを使用してTransactionを作成します。

import {getConnection} from "typeorm";

await getConnection().transaction(async transactionalEntityManager => {
    
});

EntityManagerを使用してTransactionを作成します。

import {getManager} from "typeorm";

await getManager().transaction(async transactionalEntityManager => {
    
});

Transactionを使用する際には非同期で実行する必要があります。

import {getManager} from "typeorm";

await getManager().transaction(async transactionalEntityManager => {
    await transactionalEntityManager.save(users);
    await transactionalEntityManager.save(photos);
    // ...
});

Transactionで作業する際には、getManagerからグローバルマネージャーを使用する場合に問題が発生します。transactionalEntityManagerなどのエンティティマネージャを使用して下さい。 また、グローバルマネージャーを使用してクエリを実行するクラスを使用することもできませんので、提供されたトランザクションエンティティマネージャーを使用して実行する必要があります。

Transactionの分離レベル

トランザクションの分離レベルの指定は、最初にパラメーターとして設定することで実行できます。

import {getManager} from "typeorm";

await getManager().transaction("SERIALIZABLE", transactionalEntityManager => {
    
});

MySQL、Postgres、SQL Serverでは標準の分離レベル(READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE)をサポートしています。

SQliteはデフォルトでSERIALIZABLEに設定していますが、共有キャッシュモードが有効になっている場合は、READ UNCOMMITTEDを使用できます。

Oracleは、READ COMMITTEDおよびSERIALIZABLEのみをサポートします。

トランザクションデコレータ

Transactionデコレータには@Transaction@TransactionManager@ TransactionRepositoryがあります。

@Transaction すべての実行を単一のデータベーストランザクションにラップ

@ TransactionManager トランザクション内でクエリを実行するために使用する必要があるトランザクションエンティティマネージャを提供

@Transaction()
save(@TransactionManager() manager: EntityManager, user: User) {
    return manager.save(user);
}

分離レベルを設定する場合

@Transaction({ isolation: "SERIALIZABLE" })
save(@TransactionManager() manager: EntityManager, user: User) {
    return manager.save(user);
}

@TransactionManagerが提供するマネージャーを使用するか@TransactionRepositoryを使用して、トランザクションリポジトリ(内部でトランザクションエンティティマネージャを使用している)を挿入することもできます。

@Transaction()
save(user: User, @TransactionRepository(User) userRepository: Repository<User>) {
    return userRepository.save(user);    
}

組み込みリポジトリとカスタムリポジトリの両方を使用できます。

Repository TreeRepository MongoRepositoryなど

@TransactionRepository(Entity) entityRepository:Repository <Entity>
@TransactionRepository()customRepository:CustomRepository)

QueryRunner

QueryRunnerは、単一のデータベースの接続を提供します。 トランザクションは、クエリランナーを使用してコントロールされます。 単一のトランザクションは単一のクエリランナーからのみ確立できます。 クエリランナーインスタンスを手動で作成し、それを使用してトランザクションの状態を手動でコントロールできます。

import {getConnection} from "typeorm";

// 接続を取得し、新しいクエリランナーを作成
const connection = getConnection();
const queryRunner = connection.createQueryRunner();

// 新しいクエリランナーを使用して実際のデータベース接続を確立する
await queryRunner.connect();

// クエリランナーで任意のクエリを実行できるようになりました。次に例を示します。
await queryRunner.query("SELECT * FROM users");

// クエリランナーによって作成された接続で動作するエンティティマネージャーにアクセスすることもできます。
const users = await queryRunner.manager.find(User);

// 新しいトランザクションを開く:
await queryRunner.startTransaction();

try {
    
    // このトランザクションでいくつかの操作を実行します。
    await queryRunner.manager.save(user1);
    await queryRunner.manager.save(user2);
    await queryRunner.manager.save(photos);
    
    // トランザクションをコミット
    await queryRunner.commitTransaction();
    
} catch (err) {
    
    // エラーがあるため、変更をロールバックできます
    await queryRunner.rollbackTransaction();
    
} finally {
    
    // 手動で作成されたクエリランナーをリリースする必要があります。
    await queryRunner.release();
}

QueryRunnerでトランザクションを制御するには3つの方法があります。

  • startTransaction

クエリランナーインスタンス内で新しいトランザクションを開始します。

  • commitTransaction

クエリランナーインスタンスを使用して行われたすべての変更をコミットします。

  • rollbackTransaction

クエリランナーインスタンスを使用して行われたすべての変更をロールバックします。

QueryBuilder

QueryBuilderとは

typeormの機能の一つで複雑なSQLクエリを簡単に作成して実行するための機能です。

QueryBuilderの例

const firstUser = await connection
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .getOne();

これは以下のSQLクエリを構築します。

SELECT 
    user.id as userId, 
    user.firstName as userFirstName, 
    user.lastName as userLastName
FROM users user
WHERE user.id = 1

Userのインスタンスを返します。

User {
    id: 1,
    firstName: "Timber",
    lastName: "Saw"
}

クエリを作成する方法

クエリービルダーで作成する3つの方法。

connectionを使用

import {getConnection} from "typeorm";

const user = await getConnection()
    .createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne();

EntityManagerを使用

import {getManager} from "typeorm";

const user = await getManager()
    .createQueryBuilder(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne();

Repositoryを使用

import {getRepository} from "typeorm";

const user = await getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .getOne();

5つのタイプ

SELECT,INSERT,UPDATE,DELETE,Relationを使用することができる。

1.SELECT

import {getConnection} from "typeorm";

const user = await getConnection()
    .createQueryBuilder()
    
    .select("user")
    
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne();

2.INSERT

import {getConnection} from "typeorm";

await getConnection()
    .createQueryBuilder()
    
    .insert()
    
    .into(User)
    .values([
        { firstName: "Timber", lastName: "Saw" }, 
        { firstName: "Phantom", lastName: "Lancer" }
     ])
    .execute();

3.UPDATE

import {getConnection} from "typeorm";

await getConnection()
    .createQueryBuilder()
    
    .update(User)
    
    .set({ firstName: "Timber", lastName: "Saw" })
    .where("id = :id", { id: 1 })
    .execute();

4.DELETE

import {getConnection} from "typeorm";

await getConnection()
    .createQueryBuilder()
    
    .delete()
    
    .from(User)
    .where("id = :id", { id: 1 })
    .execute();

5.RelationQueryBuilder

関係固有の操作を構築して実行するために使用

値を取得する方法

データベースから一つ結果を取得するには「getOne」を使用する。

以下はIDまたは名前でユーザー

const timber = await getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id OR user.name = :name", { id: 1, name: "Timber" })
    .getOne();

データベースから複数の結果を取得するには「getMany」を使用。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .getMany();

selectを使用して取得できる結果には、エンティティーとロウの2種類がある。 ほとんどの場合はデータベースから実際のエンティティを選択する必要がありのでgetOneとgetManyを使用する。 しかし特定のデータを選択する場合、例えばすべてのユーザー写真の合計はデータの実体ではなく、ロウデータを取得する。取得するには、getRawOneとgetRawManyを使用する。

const { sum } = await getRepository(User)
    .createQueryBuilder("user")
    .select("SUM(user.photosCount)", "sum")
    .where("user.id = :id", { id: 1 })
    .getRawOne();
const photosSums = await getRepository(User)
    .createQueryBuilder("user")
    .select("user.id")
    .addSelect("SUM(user.photosCount)", "sum")
    .where("user.id = :id", { id: 1 })
    .getRawMany();

// result will be like this: [{ id: 1, sum: 25 }, { id: 2, sum: 13 }, ...]

エイリアスとは?

createQueryBuilder("user")の"user"がSQLエイリアスである。選択したデータを扱うとき以外はどこでもエイリアスを使用する。

createQueryBuilder("user")と下記は同等である。

createQueryBuilder()
    .select("user")
    .from(User, "user")

結果は以下のSQLlクエリのようになる。

SELECT ... FROM users user

usersがテーブル名でuserがこのテーブルに割り当てるエイリアスになる。

createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.name = :name", { name: "Timber" })

これにより以下のSQLクエリが生成される。

SELECT ... FROM users user WHERE user.name = 'Timber'

クエリービルダーを作成したときに割り当てたユーザーエイリアスを使用してusersテーブルを使用した。

1つのクエリービルダーは1つのエイリアスに限定されず、複数のエイリアスを持つことができる。各”select”はそれ自身の別名を持つことができ、それぞれそれ独自の別名を持つ複数のテーブルから選択することができる。それぞれ独自の別名を持つ複数のテーブルを結合することができ、それらの別名を使用して選択しているテーブルにアクセスできる。

データをエスケープするためのパラメータの使用

where("user.name =:name"、{name: "Timber"})の {name: "Timber"}はSQLインジェクションのためのパラメータである。

where("user.name = '" + name + "")と書くこともできるが、SQLインジェクションのコードは安全ではない。安全な方法はインジェクション対策されたwhere("user .name =:name "、{name:" Timber "})と言う書き方である。ここで「:name」はパラメータ名であり値はオブジェクトで指定され:{name:" Timber "}と書きます。

.where("user.name = :name", { name: "Timber" })

上記は下記の短縮である。

.where("user.name = :name")
.setParameter("name", "Timber")

※異なる値に同じパラメーター名を使用しない。値は複数回設定するたびに上書きされる

特殊な展開構文を使用して値の配列を指定し、それらをSQLステートメント内の値のリストに変換することもできる。

.where("user.name IN (:...names)", { names: [ "Timber", "Cristal", "Lina" ] })

これは次のようになる。

WHERE user.name IN ('Timber', 'Cristal', 'Lina')

WHERE式の追加

WHERE式は以下の通り。

createQueryBuilder("user")
    .where("user.name = :name", { name: "Timber" })

次のSQLクエリが生成される。

SELECT ... FROM users user WHERE user.name = 'Timber'

存在するWHERE式にANDを追加。

createQueryBuilder("user")
    .where("user.firstName = :firstName", { firstName: "Timber" })
    .andWhere("user.lastName = :lastName", { lastName: "Saw" });

次のSQLクエリが生成される。

SELECT ... FROM users user WHERE user.firstName = 'Timber' AND user.lastName = 'Saw'

既存のWHERE式にORを追加できる。

createQueryBuilder("user")
    .where("user.firstName = :firstName", { firstName: "Timber" })
    .orWhere("user.lastName = :lastName", { lastName: "Saw" });

次のSQLクエリが生成される。

SELECT ... FROM users user WHERE user.firstName = 'Timber' OR user.lastName = 'Saw'

大括弧を使用して、複雑なWHERE式を既存のWHEREに追加できる。

createQueryBuilder("user")
    .where("user.registered = :registered", { registered: true })
    .andWhere(new Brackets(qb => {
        qb.where("user.firstName = :firstName", { firstName: "Timber" })
          .orWhere("user.lastName = :lastName", { lastName: "Saw" })
    }))

次のSQLクエリが生成される。

SELECT ... FROM users user WHERE user.registered = true AND (user.firstName = 'Timber' OR user.lastName = 'Saw')

必要なだけAND式とOR式を組み合わせることができる。「.where」を複数回使用すると、それ以前のすべてのWHERE式がオーバーライドされる。

※orWhereには注意。AND式とOR式の両方を含む複雑な式を使用する場合は、それらが偽りなくスタックされることに注意する。 場合によっては、代わりにwhere文字列を作成しorWhereを使用しないようにする必要がある。

HAVING式の追加

HAVING式を追加は以下の通り。

createQueryBuilder("user")
    .having("user.name = :name", { name: "Timber" })

次のSQLクエリが生成される。

SELECT ... FROM users user HAVING user.name = 'Timber'

既存のHAVING式にANDを追加も可能。

createQueryBuilder("user")
    .having("user.firstName = :firstName", { firstName: "Timber" })
    .andHaving("user.lastName = :lastName", { lastName: "Saw" });

次のSQLクエリが生成される。

SELECT ... FROM users user HAVING user.firstName = 'Timber' AND user.lastName = 'Saw'

既存のHAVING式にORを追加できる。

createQueryBuilder("user")
    .having("user.firstName = :firstName", { firstName: "Timber" })
    .orHaving("user.lastName = :lastName", { lastName: "Saw" });

次のSQLクエリが生成される。

SELECT ... FROM users user HAVING user.firstName = 'Timber' OR user.lastName = 'Saw'

必要なだけAND式とOR式を組み合わせることができる。「.having」を複数回使用すると、それまでのすべてのHAVING式が上書きされる。

ORDER BY式を追加する

ORDER BY式を追加するのは以下の通り。

createQueryBuilder("user")
    .orderBy("user.id")

次のSQLクエリが生成される。

SELECT ... FROM users user ORDER BY user.id

昇順から降順に変更できる。

createQueryBuilder("user")
    .orderBy("user.id", "DESC")
    
createQueryBuilder("user")
    .orderBy("user.id", "ASC")

複数の注文基準を追加できる。

createQueryBuilder("user")
    .orderBy("user.name")
    .addOrderBy("user.id");

order-byフィールドのマップを使うこともできる。

createQueryBuilder("user")
    .orderBy({
        "user.name": "ASC",
        "user.id": "DESC"
    });

.orderByを複数回使用すると、それまでのすべてのORDER BY式がオーバーライドされる。

GROUP BY式の追加

GROUP BY式の追加は以下の通り。

createQueryBuilder("user")
    .groupBy("user.id")

次のSQLクエリが生成される。

SELECT ... FROM users user GROUP BY user.id

グループ化基準をさらに追加するには「addGroupBy」を使用する。

createQueryBuilder("user")
    .groupBy("user.name")
    .addGroupBy("user.id");

.groupByを複数回使用すると、それまでのすべてのGROUP BY式が上書きされる。

LIMIT式の追加

LIMIT式の追加は以下の通り。

createQueryBuilder("user")
    .limit(10)

次のSQLクエリが生成される。

SELECT ... FROM users user LIMIT 10

SQLクエリの結果はデータベースの種類(SQLmySQL、Postgresなど)によって異なる。

※結合または副照会で複雑な照会を使用している場合、LIMITは期待どおりに機能しない可能性がある。 ページネーションを使用している場合は、代わりにtakeを使用することをお勧めする。

OFFSET式の追加

OFFSET式の追加は以下の通り。

createQueryBuilder("user")
    .offset(10)

次のSQLクエリが生成される。

SELECT ... FROM users user OFFSET 10

SQLクエリの結果はデータベースの種類(SQLmySQL、Postgresなど)によって異なる。

※結合またはサブクエリを含む複雑なクエリを使用していると、期待したとおりにOFFSETが機能しないことがある。 ページネーションを使用している場合は、代わりにskipを使用することをお勧めする。

テーブル結合

次のエンティティがある場合。

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

@Entity()
export class User {
    
    @PrimaryGeneratedColumn()
    id: number;
    
    @Column()
    name: string;
    
    @OneToMany(type => Photo, photo => photo.user)
    photos: Photo[];
}
import {Entity, PrimaryGeneratedColumn, Column, ManyToOne} from "typeorm";
import {User} from "./User";

@Entity()
export class Photo {
    
    @PrimaryGeneratedColumn()
    id: number;
    
    @Column()
    url: string;
    
    @ManyToOne(type => User, user => user.photos)
    user: User;
}

ユーザー「Timber」に彼の写真をすべてロードしたいとする。

const user = await createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo")
    .where("user.name = :name", { name: "Timber" })
    .getOne();

次のような結果になる。

{
    id: 1,
    name: "Timber",
    photos: [{
        id: 1,
        url: "me-with-chakram.jpg"
    }, {
        id: 2,
        url: "me-with-trees.jpg"
    }]
}

「leftJoinAndSelect」が自動的に「Timber」の写真をすべてロードした。 最初の引数はロードしたいリレーション、2番目の引数はこのリレーションのテーブルに割り当てるエイリアスである。この別名は、クエリービルダーのどこでも使用できる。例えば、削除されていないすべてのTimberの写真をとる場合は以下の通り。

const user = await createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo")
    .where("user.name = :name", { name: "Timber" })
    .andWhere("photo.isRemoved = :isRemoved", { isRemoved: false })
    .getOne();

次のSQLクエリが生成される。

SELECT user.*, photo.* FROM users user 
    LEFT JOIN photos photo ON photo.user = user.id
    WHERE user.name = 'Timber' AND photo.isRemoved = FALSE

「where」を使用し結合式に条件を追加することもできる。

const user = await createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo", "photo.isRemoved = :isRemoved", { isRemoved: false })
    .where("user.name = :name", { name: "Timber" })
    .getOne();

次のSQLクエリが生成される。

SELECT user.*, photo.* FROM users user 
    LEFT JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
    WHERE user.name = 'Timber'

inner joinとleft join

LEFT JOINの代わりにINNER JOINを使いたい場合はinnerJoinAndSelectを使用する。

const user = await createQueryBuilder("user")
    .innerJoinAndSelect("user.photos", "photo", "photo.isRemoved = :isRemoved", { isRemoved: false })
    .where("user.name = :name", { name: "Timber" })
    .getOne();

これは以下のSQLと同等です。

SELECT user.*, photo.* FROM users user 
    INNER JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
    WHERE user.name = 'Timber'

LEFT JOINとINNER JOINの違いは、写真がない場合はINNER JOINがユーザーを返さない。 写真がない場合でもLEFT JOINの場合はユーザーを返す。

選択せずに結合

選択せずにデータを結合するにはleftJoinまたはinnerJoinを使用する。

const user = await createQueryBuilder("user")
    .innerJoin("user.photos", "photo")
    .where("user.name = :name", { name: "Timber" })
    .getOne();

次のSQLクエリが生成される。

SELECT user.* FROM users user 
    INNER JOIN photos photo ON photo.user = user.id
    WHERE user.name = 'Timber'

彼が写真を持っているならば「Timber」を選択しますが、写真は返さない。

いくつかのエンティティまたはテーブルを結合

関係のあるエンティティやテーブルだけでなく、他の関係のないエンティティまたはテーブルも結合することができる。

const user = await createQueryBuilder("user")
    .leftJoinAndSelect(Photo, "photo", "photo.userId = user.id")
    .getMany();
const user = await createQueryBuilder("user")
    .leftJoinAndSelect("photos", "photo", "photo.userId = user.id")
    .getMany();

結合とマッピング機能

ユーザーエンティティにprofilePhotoを追加すると、QueryBuilderを使用して任意のデータをそのプロパティにマッピングできます。

export class User {    
    /// ...
    profilePhoto: Photo;
    
}
const user = await createQueryBuilder("user")
    .leftJoinAndMapOne("user.profilePhoto", "user.photos", "photo", "photo.isForProfile = TRUE")
    .where("user.name = :name", { name: "Timber" })
    .getOne();

これでTimberのプロフィール写真が読み込まれ、それがuser.profilePhotoに設定される。 一つのエンティティをロードしてマップする場合は、leftJoinAndMapOneを使用する。複数のエンティティを読み込んでマップしたい場合は、leftJoinAndMapManyを使用する。

生成されたクエリを取得する

QueryBuilderで生成されたSQLクエリを取得したい場合には「getSql」を使用する。

const sql = createQueryBuilder("user")
    .where("user.firstName = :firstName", { firstName: "Timber" })
    .orWhere("user.lastName = :lastName", { lastName: "Saw" })
    .getSql();

デバッグ目的では「printSql」を使用する。

const users = await createQueryBuilder("user")
    .where("user.firstName = :firstName", { firstName: "Timber" })
    .orWhere("user.lastName = :lastName", { lastName: "Saw" })
    .printSql()
    .getMany();

ストリーミング結果データ

ストリームを返す「stream」を使うことができる。 ストリーミングによって生データが返されるので、エンティティの変換を手動で処理する必要がある。

const stream = await getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .stream();

ページネーションの使用

ほとんどの場合、アプリケーションを開発するときにページ付け機能が必要である。 アプリケーションにページネーション、ページスライダ、または無限スクロールコンポーネントがある場合に使用する。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo")
    .take(10)
    .getMany();

これにより、最初の10人のユーザーに写真が付く。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo")
    .skip(10)
    .getMany();

これにより、最初の10人のユーザー以外の写真が付く。それらの方法を組み合わせることができる。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo")
    .skip(5)
    .take(10)
    .getMany();

これは最初の5人のユーザーをスキップし、それらの後に10人のユーザーを取得する。

takeとskipはlimitとoffsetを使っているように見えるかもしれないが、結合やサブクエリを使用したより複雑なクエリが発生した場合に、limitおよびoffsetは期待どおりに機能しない可能性がある。その場合にtake and skipを使うとこれらの問題を防ぐことができる。

セットロック

QueryBuilderは、楽観的ロックと悲観的ロックの両方をサポートしている。 悲観的読み取りロックを使用する場合は次の方法を使用する。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .setLock("pessimistic_read")
    .getMany();

悲観的書き込みロックを使用するには、次の方法を使用する。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .setLock("pessimistic_write")
    .getMany();

悲観的書き込みロックを使用するには、次の方法を使用する。楽観的ロックを使用するには、次の方法を使用する。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .setLock("optimistic", existUser.version)
    .getMany();

楽観的ロックは@Versionおよび@UpdatedDateデコレータの両方と連携して機能する。

部分選択

一部のエンティティプロパティのみを選択したい場合は、次の構文を使用できる。

const users = await getRepository(User)
    .createQueryBuilder("user")
    .select([
        "user.id",
        "user.name"
    ])
    .getMany();

これはユーザーのIDと名前のみを選択する。

サブクエリを使用する

サブクエリは以下のように使用する。 FROM、WHERE、およびJOIN式でサポートされている。

const qb = await getRepository(Post).createQueryBuilder("post");
const posts = qb
    .where("post.title IN " + qb.subQuery().select("user.name").from(User, "user").where("user.registered = :registered").getQuery())
    .setParameter("registered", true)
    .getMany();

同じことをするためのもっとエレガントな方法

const posts = await connection.getRepository(Post)
    .createQueryBuilder("post")
    .where(qb => {
        const subQuery = qb.subQuery()
            .select("user.name")
            .from(User, "user")
            .where("user.registered = :registered")
            .getQuery();
        return "post.title IN " + subQuery;
    })
    .setParameter("registered", true)
    .getMany();

あるいは、別のクエリービルダーを作成してその生成されたSQLを使用することもできる。

const userQb = await connection.getRepository(User)
    .createQueryBuilder("user")
    .select("user.name")
    .where("user.registered = :registered", { registered: true });

const posts = await connection.getRepository(Post)
    .createQueryBuilder("post")
    .where("post.title IN (" + userQb.getQuery() + ")")
    .setParameters(userQb.getParameters())
    .getMany();

FROMにサブクエリを作成できる。

const userQb = await connection.getRepository(User)
    .createQueryBuilder("user")
    .select("user.name", "name")
    .where("user.registered = :registered", { registered: true });

const posts = await connection
    .createQueryBuilder()
    .select("user.name", "name")
    .from("(" + userQb.getQuery() + ")", "user")
    .setParameters(userQb.getParameters())
    .getRawMany();

またはより洗練された構文を使用する

const posts = await connection
    .createQueryBuilder()
    .select("user.name", "name")
    .from(subQuery => {
        return subQuery
            .select("user.name", "name")
            .from(User, "user")
            .where("user.registered = :registered", { registered: true });
    }, "user")
    .getRawMany();

サブクエリ選択を「2番目から」として追加する場合は、addFromを使用する。

SELECTステートメントでサブクエリ選択を使用することもできます。

const posts = await connection
    .createQueryBuilder()
    .select("post.id", "id")
    .addSelect(subQuery => {
        return subQuery
            .select("user.name", "name")
            .from(User, "user")
            .limit(1);
    }, "name")
    .from(Post, "post")
    .getRawMany();

隠れたカラム

照会しているモデルに「select:false」列を持つ列がある場合、その列から情報を取得するために「addSelect関数」を使用する必要がある。

次のエンティティがあるとする。

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

@Entity()
export class User {
    
    @PrimaryGeneratedColumn()
    id: number;
    
    @Column()
    name: string;

    @Column({select: false})
    password: string;
}

標準の検索またはクエリを使用しても、モデルのパスワードプロパティは表示されない。 ただし、次のようにしたとすると。

const users = await connection.getRepository(User)
    .createQueryBuilder()
    .select("user.id", "id")
    .addSelect("user.password")
    .getMany();

クエリでプロパティパスワードを取得できる。

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の降順で並べられます。 写真のアルバムは左結合され、それらのメタデータは内部結合されます。

Repository API

Repository APIとは

エンティティ(テーブル)の検索、挿入、更新、削除などを行うもの

manager

このリポジトリで使用されるEntityを操作するオブジェクト。

const manager = repository.manager;

metadata

このリポジトリによって管理されるエンティティのメタデータ

const metadata = repository.metadata;

queryRunner

EntityManagerが使用するクエリランナー。 EntityManagerのトランザクションインスタンスでのみ使用。

const queryRunner = repository.queryRunner;

target

このリポジトリによって管理されるターゲットとなるエンティティクラス。エンティティマネージャのトランザクションインスタンスでのみ使用される。

const target = repository.target;

createQueryBuilder

SQLクエリを構築するために使用するクエリビルダーを作成する。

const users = await repository
    .createQueryBuilder("user")
    .where("user.name = :name", { name: "John" })
    .getMany();

hasId

指定されたエンティティのプライマリ列プロパティが定義されているかどうかを確認する。

if (repository.hasId(user)) {
    // ... do something
 }

getId

指定されたエンティティのプライマリのプロパティの値を取得する。エンティティに複合主キーがある場合、返される値は主列の名前と値を持つオブジェクトになる。

const userId = repository.getId(user); // userId === 1

create

ユーザーの新しいインスタンスを作成する。オプションで、新しく作成されたユーザーオブジェクトに書き込まれるユーザープロパティを持つオブジェクトリテラルを受け入れる。

const user = repository.create(); // same as const user = new User();
const user = repository.create({
    id: 1,
    firstName: "Timber",
    lastName: "Saw"
}); // same as const user = new User(); user.firstName = "Timber"; user.lastName = "Saw";

merge

複数のエンティティを単一のエンティティにマージする。

const user = new User();
repository.merge(user, { firstName: "Timber" }, { lastName: "Saw" }); // same as user.firstName = "Timber"; user.lastName = "Saw";

preload

指定されたプレーンなJavaScriptオブジェクトから新しいエンティティを作成する。エンティティがデータベースにすでに存在する場合、それ(およびそれに関連するすべて)をロードし、すべての値を特定のオブジェクトの新しい値で置き換え、新しいエンティティを返す。新しいエンティティは、実際には、新しいオブジェクトからすべてのプロパティが置き換えられた、データベースからロードされたエンティティ。

const partialUser = {
    id: 1,
    firstName: "Rizzrak",
    profile: {
        id: 1
    }
};
const user = await repository.preload(partialUser);
// user will contain all missing data from partialUser with partialUser property values:
// { id: 1, firstName: "Rizzrak", lastName: "Saw", profile: { id: 1, ... } }

save

指定されたエンティティまたはエンティティの配列を保存する。エンティティがすでにデータベースに存在する場合、更新される。エンティティがデータベースに存在しない場合、挿入される。指定されたすべてのエンティティを単一のトランザクションで保存する。未定義のプロパティはすべてスキップされるため、部分更新もサポートする。保存されたエンティティを返す。

await repository.save(user);
await repository.save([
    category1,
    category2,
    category3
]);

remove

指定されたエンティティまたはエンティティの配列を削除する。単一のトランザクションで指定されたすべてのエンティティを削除する(エンティティの場合、マネージャーはトランザクションではない)。削除されたエンティティを返す。

await repository.remove(user);
await repository.remove([
    category1,
    category2,
    category3
]);

insert

新しいエンティティまたはエンティティの配列を挿入する。

await repository.insert({
    firstName: "Timber",
    lastName: "Timber"
});


await manager.insert(User, [{ 
    firstName: "Foo", 
    lastName: "Bar" 
}, { 
    firstName: "Rizz", 
    lastName: "Rak" 
}]);

update

指定された更新オプションまたはエンティティIDによってエンティティを部分的に更新。

await repository.update({ firstName: "Timber" }, { firstName: "Rizzrak" });
// executes UPDATE user SET firstName = Rizzrak WHERE firstName = Timber

await repository.update(1, { firstName: "Rizzrak" });
// executes UPDATE user SET firstName = Rizzrak WHERE id = 1

delete

エンティティID、ID、または指定された条件でエンティティを削除する。

await repository.delete(1);
await repository.delete([1, 2, 3]);
await repository.delete({ firstName: "Timber" });

count

指定されたオプションに一致するエンティティをカウントする。ページネーションに便利。

const count = await repository.count({firstName: "Timber"};

increment

与えられたオプションに一致するエンティティの提供された値によっていくつかの列を増やす。

await manager.increment(User、{firstName: "Timber"}"age"3;

decrement

指定されたオプションに一致する提供された値によって一部の列をデクリメントする。

await manager.decrement(User、{firstName: "Timber"}"age"3;

find

指定されたオプションに一致するエンティティを検索する。

const timbers = await repository.find({firstName: "Timber"};

findAndCount

指定された検索オプションに一致するエンティティを検索する。また、指定された条件に一致するすべてのエンティティをカウントするが、ページネーション設定を無視する(スキップしてテイクオプション)。

const [timbers, timbersCount] = await repository.findAndCount({ firstName: "Timber" });

findByIds

IDで複数のエンティティを検索する。

const users = await repository.findByIds([123];

findOne

一部のIDまたは検索オプションに一致する最初のエンティティを検索する。

const user = await repository.findOne(1;
const timber = await repository.findOne({firstName: "Timber"};

findOneOrFail

一部のIDまたは検索オプションに一致する最初のエンティティを検索する。 一致するものがない場合、返されたプロミスを拒否する。

const user = await repository.findOneOrFail(1;
const timber = await repository.findOneOrFail({firstName: "Timber"};

query

生のSQLクエリを実行する。

const rawData = await repository.query( `SELECT * FROM USERS`;

clear

指定されたテーブルからすべてのデータをクリアする(切り捨て/削除する)。

await repository.clear();

プライベートネットでの動作確認

概要

gethで構築したプライベートネットワークを使い、作成したコントラクトが動作するか確認してみたいと思います。

設定

gethの設定

Homebrewを使いgethをインストールします

$ brew tap ethereum/ethereum
$ brew install ethereum

gethのプライベートネットを起動するにはgenesis.jsonファイルを設定する必要があります。

まずプライベートネットのデータを保管するディレクトリを作成し移動します。

$ mkdir ~/private-net && cd private-net

次にprivate-netディレクトリ内にgenesis.jsonファイルとpasswordファイルを用意します。

$ touch genesis.json
$ touch password

各ファイルの内容は以下の通りです。 genesis.json

{
  "config": {
    "chainId": 15,
    "homesteadBlock": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0
  },
  "coinbase"   : "0x0000000000000000000000000000000000000000",
  "difficulty" : "0x1",
  "extraData"  : "",
  "gasLimit"   : "0x2fefd8",
  "nonce"      : "0x0000000000000042",
  "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",
  "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
  "timestamp"  : "0x00",
  "alloc": {
    "945cd603a6754cb13c3d61d8fe240990f86f9f8a": { "balance": "500000000000000000000000000" },
    "66b4e7be902300f9a15d900822bbd8803be87391": { "balance": "500000000000000000000000000" },
    "104f0d848da3f760dddadc56fc4ab78305110dba": { "balance": "500000000000000000000000000" },
    "addfaa808c59581f04cdadfc0be28ebfb520e839": { "balance": "500000000000000000000000000" },
    "450a8a99bf5ad49db301f6068c619de2400de6f7": { "balance": "500000000000000000000000000" }
  }
}

password

blah
blah
blah
blah
blah

プライベートネットワークの設定

まずはプライベートネットワークの初期化を行いprivate-netディレクトリにgethディレクトリを作成します。

$ geth --datadir ~/private-net --nodiscover --maxpeers 0 init ~/private-net/genesis.json

次にgethのプライベートネットを起動します。 gethを起動するたびに下記のスクリプトを入力するのは大変なので、geth-start.shファイルを用意し、起動用のシェルスクリプトを作成します。

geth-start.sh

geth --datadir ~/private-net --networkid 15 --nodiscover --maxpeers 0 --mine --minerthreads 1 --rpc --rpcaddr  0.0.0.0 --rpccorsdomain "*" --rpcvhosts "*" --rpcapi "eth,web3,pesonal,net" --ipcpath ~/private-net/geth.ipc --ws --wsaddr  0.0.0.0  --wsapi "eth,web3,personal,net" --wsorigins "*" --unlock 0,1,2,3,4 --password ~/private-net/password --allow-insecure-unlock
$ bash geth-start.sh

プライベートネットを起動するとマイニングが始まります。現在開いてるターミナルにはマイニングの状況がリアルタイムに表示されます。別にもう一つターミナルを立ち上げgeth.ipcファイル(private-netディレクトリにあります)にアタッチしgethコンソールを起動します。

$ geth attach geth.ipc

起動

作成したコントラクト(ここではRoomFactoryというコントラクト用いますが、任意の動作確認したいコントラクトに置き換えて下さい)をtruffleを使いgethで作成したプライベートネットワークにデプロイします。

動作確認する際にABIとコントラクトアドレスが必要になるので、画面で確認出来る様にmigrationファイルを下記の様にします。

deploy_room_factory.js

const RoomFactory = artifacts.require('../contracts/RoomFactory.sol')

module.exports = deployer => {
    deployer.deploy(RoomFactory).then(instance => {
        console.log('ABI:', JSON.stringify(instance.abi))
    })
}

RoomFactoryコントラクトをデプロイします。

$ truffle migrate --rest

すると下記の様な結果が得られます。

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x5fd63c3dc91f64c3ee8963cab14f020d17bb469c079603b564984574e3de4094
  Migrations: 0xf5ad7542173e8944d1ae16b4394eaa34cfda4814
Saving artifacts...
Running migration: 2_deploy_room_factory.js
  Replacing RoomFactory...
  ... 0x38d654c7e25bf331ab43de2de054bf303bf77b7abb9fe6f59e0db704eebf1828
  RoomFactory: 0xcc6cc4d996ec212a1b047b83a4f039e52783bab8
ABI: [{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"na
me":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonp
ayable","type":"function"},{"constant":false,"inputs":[],"name":"destroy","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"p
ause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"state
Mutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable",
"type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"}],"name":"destroyAndSend","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"
anonymous":false,"inputs":[{"indexed":true,"name":"_creator","type":"address"},{"indexed":false,"name":"_room","type":"address"},{"indexed":false,"name":"_depositedValue","type":"uint256"}],"n
ame":"RoomCreated","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"anonymous":false,"inputs":[{"
indexed":true,"name":"previousOwner","type":"address"}],"name":"OwnershipRenounced","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"inde
xed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"constant":false,"inputs":[],"name":"createRoom","outputs":[],"payable":true,"stateMutability":"pa
yable","type":"function"}]
Saving artifacts...

RoomFactoryに続く'0xcc6cc4d996ec212a1b047b83a4f039e52783bab8'がコントラクトアドレスになります。またABI:に続く配列のがABIになります。

動作確認

gethのプライベートネットワークにおけるコントラクトの動作確認を行います。動作確認をする際にはマイニングの作業を止める必要がありますので、先程立ち上げたgethコンソールのターミナルを開きマニングをストップさせます。

> miner.stop() 

次にコントラクトオブジェクトを定義します。 先程調べたコントラクトのABIを変数abiに定義します。

> abi = [{"constant":false,"..."type":"function"}]

同様にRoomFactoryのコントラクトアドレスをaddressに定義します。

> address = '0xcc6cc4d996ec212a1b047b83a4f039e52783bab8'

大義したabiとaddressを使いRoomFactoryコントラクトオブジェクトを定義します。

> roomFactory = eth.contract(abi).at(address)

それでは定義したコントラクトオブジェクトを使ってコントラクトの状態呼び出しと変更を行います。

ガスを消費しない状態の呼び出し(今回はpaused関数)にはcallを使用します。

> roomFactory.paused.call()
false

ガスを消費する状態の変更(今回はcreteRoom関数)にはtransactionを使用します。

> roomFactory.createRoom.sendTransaction({from: eth.accounts[0], gas: 100000, value: web3.toWei(0.1, 'ether')})
"0xa1a1239be3c65a4726842bc50b31c628326bb4197d7ff56bcc276e75797108f1"

マイニングを再度開始しトランザクションをブロックに取り込みます。

> miner.start(1)

少し時間を置いてから下記の方法でトランザクションの確認をします。

>eth.getTransaction('0xa1a1239be3c65a4726842bc50b31c628326bb4197d7ff56bcc276e75797108f1')