[JavaScript] 2014アニメのデータベースを作りたい その3 | DataMapper編

2014/12/17

こんにちは。きんくまです。

Promiseでの非同期処理の書き方もわかったので、実際にデータを取り込む手順を書いてみます。

参考書をみつつ、今回はDataMapperパターンでやってみたいと思います。

参考書

sqlite3のパッケージ

SQLite3をNode.jsでどうやって扱うかですが、検索かけたらこのパッケージが出ました。
扱いやすそうだったので、これに決定。

>> mapbox/node-sqlite3

データベース

今回からJSでそのまま書くと大変そうだったので、TypeScriptで書くことにしました。
最初に困ったのが内部モジュールと外部モジュールの扱いの違い。クライアント側だとずっと内部モジュールで良かったのですが、Nodeだとどうも外部モジュールを使うみたいでした。
なので、外部モジュールを使っています。

DatabaseManager.ts

import sqlite3 = require('sqlite3');
import fs = require('fs');

var singletonKey:string = "singletonKey";

class DatabaseManager{
    private static _instance:DatabaseManager;
    private static DB_PATH:string = './db_importer/bin/anime2014.db';
    private static DDL_PATH:string = "./db_importer/bin/ddl/anime2014_db.sql";
    private _database:sqlite3.Database;
    private _DDL:string[];

    static get sharedManager():DatabaseManager{
        if(DatabaseManager._instance == null){
            DatabaseManager._instance = new DatabaseManager(singletonKey);
        }
        return DatabaseManager._instance;
    }

    constructor(key:string){
        if(key !== singletonKey){
            throw new Error('singleton class');
        }
    }

    get database():sqlite3.Database{
        return this._database;
    }

    setup():Promise<(resolve, reject)=>void>{
        return new Promise((resolve, reject)=>{
            this.createDB()
                .then(()=>{
                    return this.createTable();
                }).then(()=>{
                    resolve();
                }).catch((err)=>{
                    reject(err);
                });
        });
    }

    createDB():Promise<(resolve, reject)=>void>{
        return new Promise((resolve, reject)=>{
            this._database = new sqlite3.Database(DatabaseManager.DB_PATH, (err)=>{
                if(err){
                    reject(err);
                }else{
                    resolve();
                }
            });
        });
    }

    createTable():Promise<(resolve, reject)=>void>{
        var ddlText = fs.readFileSync(DatabaseManager.DDL_PATH, 'utf8');
        ddlText = ddlText.replace(/\n/g, '');
        this._DDL = ddlText.split(';');
        for(var i = this._DDL.length - 1; i >= 0; i--){
            if(this._DDL[i].length <= 1){
                this._DDL.splice(i, 1);
            }
        }
        return new Promise((resolve, reject)=>{
            var len = this._DDL.length;
            this._database.serialize(()=>{
                this._DDL.forEach((ddl, idx)=>{
                    if(idx == len - 1){
                        this._database.run(ddl, (err)=>{
                            if(err){
                                reject(err);
                            }else{
                                resolve();
                            }
                        })
                    }else{
                        this._database.run(ddl);
                    }
                });
            });
        });
    }

    closeDB():Promise<(resolve, reject)=>void>{
        return new Promise((resolve, reject)=>{
            this._database.close((err)=>{
                if(err){
                    reject(err);
                }else{
                    resolve();
                }
            });
        });
    }
}

export = DatabaseManager;

sqliteのファイルを作ったり、テーブル作ったりするのは全部非同期でPromise返してます。

ドメインオブジェクト

DataMapperの特徴は以下の2つが切り離されてます
・ドメインオブジェクト
・データベースの入出力

反対にActiveRecordパターンはセットになってます。

で参考書のプログラムをみると、この2種類にそれぞれ親クラスを用意して、共通になるものはそこに入れるようにしてました。
(ちなみにこの本には全部のソースコードは載っていなくて、全て断片的なのです。むー)

DomainObject.ts

class DomainObject{
    id:number;
}

export = DomainObject;

ドメインオブジェクトの親クラスDomainObjectはidだけが載っているシンプルなクラス

Work.ts

import DomainObject = require('./DomainObject');

class Work extends DomainObject{
    name:string;
    title:string;
    summary:string;
    private _monthFrom:string;
    monthFromTime:number;
    private _monthTo:string;
    monthToTime:number;
    episodes:number;

    get monthFrom():string{
        return this._monthFrom;
    }

    set monthFrom(value:string){
        this._monthFrom = value;
        this.monthFromTime = this.parseMonthStrToInt(this._monthFrom);
    }

    get monthTo():string{
        return this._monthTo;
    }
    set monthTo(value:string){
        this._monthTo = value;
        this.monthToTime = this.parseMonthStrToInt(this._monthTo, true);
    }

    constructor(){
        super();
    }

    private parseMonthStrToInt(monthText:String, isMonthTo:boolean = false):number{
        var monthArr:string[] = monthText.split('-');
        var year = parseInt(monthArr[0], 10);
        var month = parseInt(monthArr[1], 10);

        //次の月の1st dayから1秒マイナスすると、前の月の終わりの日の23時59分59秒
        if(isMonthTo){
            month++;
            if(month > 12){
                month = 1;
                year++;
            }
        }

        var date = new Date(year, month - 1, 1, 0, 0, 0);
        var time = date.getTime();
        if(isMonthTo){
            time = time - 1000;
        }
        return time;
    }
}

export = Work;

作品情報を表すWorkクラスは長そうなんですが、開始・終了月を表す計算がつらつら書いてあるだけで、大事なのは上の方でプロパティとして列挙されているところです。これは、DBのテーブルの列に対応してます。

Mapper

データベースと直接やりとりするのがMapperです。
参考書だと親はAbstractMapperって名前だったのですが、別に抽象じゃないし、、とか思いBaseMapperって名前に変えました。

BaseMapper.ts

import DomainObject = require('./DomainObject');
import sqlite3 = require('sqlite3');

class BaseMapper{
    protected loadedMap:{ [key:string]:DomainObject; } = {};

    constructor(){

    }

    protected baseFind(id:number, database:sqlite3.Database):Promise<(resolve, reject)=>void>{
        return new Promise((resolve, reject) => {
            var result:DomainObject = this.loadedMap[id];
            if(result){
                resolve(result);
                return;
            }
            database.get(this.findStatement(), {$id:id}, (err, row)=>{
                if(err){
                    reject(err);
                }else{
                    resolve(this.load(row));
                }
            })
        });
    }

    protected load(row:any):DomainObject{
        if(row == null){
            return null;
        }
        var id:number = row.id;
        var result:DomainObject = this.doLoad(id, row);
        this.loadedMap[id] = result;
        return result;
    }

    protected findStatement():string{
        throw new Error('You must implement findStatement');
    }

    protected doLoad(id:number, row:any):DomainObject{
        throw new Error('You must implement doLoad');
    }

    protected insertStatement():string{
        throw new Error('You must implement insertStatement');
    }

    protected doInsert(subject:DomainObject, statement:sqlite3.Statement){
        throw new Error('You must implement doInsert');
    }

    insert(subject:DomainObject, database:sqlite3.Database):Promise<(resolve, reject)=>void>{
        return new Promise((resolve, reject)=>{
            var insertStmt:sqlite3.Statement = database.prepare(this.insertStatement());
            this.doInsert(subject, insertStmt);
            insertStmt.finalize((err)=>{
                if(err){
                    reject(err);
                }else{
                    resolve();
                }
            })
        });
    }
}

export = BaseMapper;

loadedMapっていうプロパティがあって、findで検索したあとそこに入れておきます。で、もう一回同じidで検索がかけられたとき、loadedMap内にキャッシュされていればそこから拾って直接DBを見に行かずスピードアップする処理になってました。

あと今回は登録と検索ができればよいので、削除・更新メソッドは省いています。

WorkMapper.ts

import sqlite3 = require('sqlite3');

import BaseMapper = require('./BaseMapper');
import DomainObject = require('./DomainObject');
import Work = require('./Work');

class WorkMapper extends BaseMapper{

    constructor(){
        super();
    }

    static get COLUMNS():string{
        return "" +
                //"work_id as id, " +
                "work_title, " +
                "work_summary, " +
                "work_month_from, " +
                "work_month_from_time, " +
                "work_month_to, " +
                "work_month_to_time, " +
                "work_episodes";
    }

    protected findStatement():string{
        return "SELECT work_id as id, " + WorkMapper.COLUMNS + " FROM Works" + " WHERE id = $id";
    }

    protected insertStatement():string{
        return "INSERT INTO Works (" +
                    WorkMapper.COLUMNS +
                ") VALUES (" +
                    "$title, " +
                    "$summary, " +
                    "$monthFrom, " +
                    "$monthFromTime, " +
                    "$monthTo, " +
                    "$monthToTime, " +
                    "$episodes" +
                ");";
    }

    find(id:number, database:sqlite3.Database):Promise<(resolve, reject)=>void>{
        return this.baseFind(id, database);
    }

    protected doLoad(id:number, row:any):DomainObject{
        var work:Work = new Work();
        work.id = id;
        work.title = row.work_title;
        work.summary = row.work_summary;
        work.monthFrom = row.work_month_from;
        work.monthFromTime = row.work_month_from_time;
        work.monthTo = row.work_month_to;
        work.monthToTime = row.work_month_to_time;
        work.episodes = row.work_episodes;
        return work;
    }

    protected doInsert(subject:DomainObject, statement:sqlite3.Statement){
        var work:Work = <Work>subject;
        if(!work){
            throw new Error('subject is not type of Work');
        }
        statement.run({
            $title: work.title
            ,$summary: work.summary
            ,$monthFrom: work.monthFrom
            ,$monthFromTime: work.monthFromTime
            ,$monthTo: work.monthTo
            ,$monthToTime: work.monthToTime
            ,$episodes: work.episodes
        });
    }
}

export = WorkMapper;

WorkMapperは実際のSQLを組み立てるみたいなイメージかと。

使ってみる

/// <reference path="declarations/node-0.11.d.ts" />
/// <reference path="declarations/sqlite3.d.ts" />
/// <reference path="declarations/es6-promise.d.ts" />

import DatabaseManager = require('./models/DatabaseManager');
import Work = require('./models/Work');
import WorkMapper = require('./models/WorkMapper');

var dbManager = DatabaseManager.sharedManager;
var mapper = new WorkMapper();

dbManager.setup()
    .then(()=>{
        console.log('set up complete');
        var work1 = new Work();
        work1.title = "たいとる1ですぞ";
        work1.summary = "概要がはいりまする";
        work1.monthFrom = '2014-9';
        work1.monthTo = '2014-12';
        work1.episodes = 3;
        return mapper.insert(work1, dbManager.database);
    })
    .then(()=>{
        console.log('insert1 comp');
        var work2 = new Work();
        work2.title = "タイトル2です";
        work2.summary = "概要が入っちゃいます";
        work2.monthFrom = '2014-1';
        work2.monthTo = '2014-11';
        work2.episodes = 18;
        return mapper.insert(work2, dbManager.database);
    })
    .then(()=>{
        return mapper.find(1, dbManager.database);
    })
    .then((foundResult:any)=>{
        var resultWork:Work = <Work>(foundResult);
        console.log('result ', resultWork);
    })
    .then(()=>{
        return dbManager.closeDB();
    })
    .catch((err)=>{
        console.log(err);
    });

Promiseパターンを使っているので、非同期処理なんだけど同期的に書けてますね。
まだやることがのこっているので引き続きやってみようと思います。

LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る