2017/05/11

ORMをTypeScriptでサクッと実装するならtypeormがオススメ

久しぶりにTypeScriptを書いて型のある喜びを感じていた。そして以前使っていたSequelizeをTypeScriptで使ってみようと思ったのだが、深淵を覗いてしまったようだ。

Sequelizeは高機能で便利なのだが、これをTypeScriptで書こうとするとかなりツライそうだ。実際の使い方は以下の記事を参照ください。


TypeScriptで素直に書けるORMはないかな?と探したところ、typeormを見つけた。

typeormは、MySQLをはじめ、PostgreSQL、MariaDB、SQLite3、MS SQL Server、Oracle、WebSQL databaseと主要なデータベースをカバーしている。
また、ライブラリ自体がTypeScriptで書かれているため、デコレータなどを使ってサクッと実装することができる。

今回は、手っ取り早く試したいのでSQLite3で使ってみる。

typeormのインストール


まずはtypeormをインストールする。
$ npm i -S typeorm

# SQLite3を使うのでコチラもインストール
$ npm i -S sqlite3

今回のディレクトリ構成は、以下のようにする。
src
├── index.ts
├── store
|   └── index.ts
└── models
    ├── Task.ts
    └── TaskController.ts


  • index.ts: エントリーポイント
  • /store/index.ts: DBの設定をするクラス
  • /models/Task.ts: Taskテーブルを定義するクラス
  • /models/TaskController.ts: Taskテーブルを操作するクラス


アプリケーションが大きくなっていったら、models配下にHoge.ts + HogeController.tsのセットが増えていく感じ。



テーブルを定義する


/models/Task.tsにTaskテーブルの定義をする。
の前に、typeormではデコレータを使うので、tsconfig.jsonに以下の設定を追加しておく。
// tsconfig.json
{
    /* 略 */
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    /* 略 */
}

// ./models/Task.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

// テーブルのinterface(なくても大丈夫)
export interface ITask {
    id: number,
    title: string,
    is_done?: boolean
}

// テーブルの定義
@Entity()
export default class Task implements ITask {
    // タスクのID
    // int型で、自動インクリメントしてくれるカラム
    @PrimaryGeneratedColumn({ type: 'int', generated: true})
    id: number;

    // タスクのタイトル
    // string型のカラム
    @Column('string')
    title: string;

    // タスクの状態
    // boolean型のカラム 
    @Column('boolean', { default: false })
    is_done: boolean;
}

これでテーブルの定義は終わり。
@ColumnにはColumnOptionsが渡せるので、primaryやunique、generatedなどが指定できる。

以降、テーブルを操作するところではTaskクラス本体を使うので、ITaskインターフェースはあってもなくてもどっちでもよい。ただ、is_doneがnullableなインターフェースがほしかったので定義しているだけ。



データベースを設定する


次に./store/index.tsにデータベースを設定するためのクラスをつくる。
// ./store/index.ts
import { createConnection, ConnectionOptions, Connection } from 'typeorm';
import Task from '../models/Task';

export default class Store {
    private static _conn: Connection;

    // データベースの設定
    public static connectionOptions: ConnectionOptions = {
        driver: {
            type: 'sqlite',
            storage: 'tasks.db',
            database: 'Tasks'
        },
        entities: [
            // テーブルクラス
            Task
        ],
        autoSchemaSync: true
    };

    public static async createConnection() {
        if (!this._conn) {
            this._conn = await createConnection(this.connectionOptions);
        }
        return this._conn;
    }
}

1つのコネクションを使いまわしたいのでシングルトンっぽいクラスししてしまったのだが、改善の余地はありそう。
connectionOptionsには接続するデータベースの設定を書く。今回はSQLite3を使うので設定は少ない。

  • driver
    • type: データベースのタイプ
    • storage: sqlite3のデータベースファイルの格納先
    • database: データベース名
  • entities: テーブル定義したクラスを指定すると対応づくテーブルが作成される

createConnectionは、すでにコネクションがあればそれを返し、なければ新しく作って返している。typeorm.createConnectionはPromiseを返すのでasync/awaitを使っている。



テーブルを操作するコントローラをつくる


テーブル定義とデータベースの設定が終わったので、実際にデータの追加、取得、更新、削除の処理を実装していく。
テーブルを操作するために、./models/TaskController.tsにテーブルを操作するためのクラスをつくる。
// ./models/TaskController.ts
import Task from './Task';
import { ITask } from './Task';
import Store from '../store';

// レスポンス用のinterface
export interface ITaskOne {
    task: Task
}

export interface ITaskList {
    tasks: Task[]
}

export interface IPageInfo {
    total: number,
    offset: number
}

export interface ITaskListResponse extends ITaskList, IPageInfo {}

export interface IParameters {
    offset?: number,
    limit?: number
}

export default class TaskController {
    constructor () {
    }

    // GET /tasks
    // 全件取得(offset, limitをパラメータに受け取ることも可能)
    static all(query: IParameters): Promise<ITaskListResponse> {

        return new Promise(async (resolve, reject) => {
            let tasks: Task[];

            try {
                const conn = await Store.createConnection();
                // Taskテーブルから全件取得(offset、limitも指定可能)
                tasks = await conn.entityManager.find(Task, {
                    alias: 'task',
                    offset: query.offset || 0,
                    limit: query.limit || 100
                });

            } catch (err) {
                reject({ code: 500, message: err.message });

            }

            if (tasks) {
                resolve({
                    tasks: tasks,
                    total: tasks.length,
                    offset: query.offset || 0
                });
            } else {
                reject({
                    code: 404,
                    message: 'タスクが見つかりませんでした'
                });
            }
        });
    }

    // GET /tasks/{id}
    // 指定したIDのタスクを取得する
    static get(id: number): Promise<ITaskOne> {
        return new Promise(async (resolve, reject) => {
            let result: Task;

            try {
                const conn = await Store.createConnection();
                // ID指定で1件だけ取得
                result = await conn.entityManager.findOneById(Task, id);

            } catch (err) {
                reject({ code: 500, message: err.message });

            }

            if (result) {
                resolve({ task: result });
            } else {
                reject({
                    code: 404,
                    message: '指定IDのタスクが見つかりませんでした'
                })
            }
        });
    }

    // POST /tasks
    // タスクを登録する
    static add(param: ITask): Promise<ITaskOne> {
        return new Promise(async (resolve, reject) => {
            let result: Task;

            const task = new Task();
            task.title = param.title;
            task.is_done = param.is_done || false;

            try {
                const conn = await Store.createConnection();
                // データの登録
                result = await conn.entityManager.persist(task);

            } catch (err) {
                reject({ code: 500, message: err.message });

            }

            resolve({ task: result });
        });
    }

    // PUT /tasks/{id}
    // 指定したIDのタスクを更新する
    static update(id: number, param: ITask): Promise<ITaskOne> {
        return new Promise(async (resolve, reject) => {
            let result: Task;

            try {
                const conn = await Store.createConnection();
                const repository = await conn.getRepository(Task);
                // ID指定で1件だけ取得
                const task = await repository.findOneById(id);

                if (!task) {
                    reject({
                        code: 404,
                        message: '指定IDのタスクが見つかりませんでした'
                    })
                }

                // 内容を更新する
                task.title = param.title || task.title;
                task.is_done = param.is_done || task.is_done;

                // 変更内容で更新する
                result = await repository.persist(task);

            } catch (err) {
                reject({ code: 500, message: err.message });

            }

            resolve({ task: result });
        });
    }

    // DELETE /tasks/{id}
    // 指定したIDのタスクを削除する
    static delete(id: number): Promise<ITaskOne> {
        return new Promise(async (resolve, reject) => {
            let result: Task;

            try {
                const conn = await Store.createConnection();
                const repository = await conn.getRepository(Task);
                // ID指定で1件だけ取得
                result = await repository.findOneById(id);

                if (!result) {
                    reject({
                        code: 404,
                        message: '指定IDのタスクが見つかりませんでした'
                    });
                }

                // データを削除する
                result = await repository.remove(result);

            } catch (err) {
                reject({ code: 500, message: err.message });

            }

            resolve({ task: result });
        });
    }
}

createConnectionでDBに接続し、entityManagerをつかってテーブルの操作ができる。
コードの中にentityManagerとgetRepositoryの2種類があるが、1回の処理で1回したDBにアクセスしないならentityManager、取得・更新のように2回以上DBにアクセスするたなgetRepositoryを使うのが良さそう。



実際に動かしてみる


実際に動かすために、エントリーポイントを実装する。
// index.ts
import Task from './models/TaskController';

function async demo() {
    // GET /tasks
    const getTasks = await Task.all({ offset: 0, limit: 10 });
    console.log(getTasks);

    // POST /tasks
    const postTasks = await Task.add({ title: 'test' });
    console.log(postTasks);

    // GET /tasks/{id}
    const getTasksById = await Task.get(1);
    console.log(getTasksById);

    // PUT /tasks/{id}
    const putTasks = await Task.update(1, { title: 'updated', is_done: true });
    console.log(putTasks);

    // DELETE /tasks/{id}
    const deleteTasks = await Task.delete(1);
    console.log(deleteTasks)
}

// 実行
demo();


こんな感じでサクッと実装できる。

この記事を書き終えそうなところで、sequelize-typescriptなるものを見つけてしまった…。



参考サイト





以上

written by @bc_rikko

0 件のコメント :

コメントを投稿