Backbone.js で HTML5 Web SQL Database を使うための backbone-webdatabase を作ってみた

Backbone.Model の fetch や save はサーバーと通信するけど、データをサーバーではなくローカルにだけ保存したいときがある。例えば PhoneGap でスタンドアロンアプリを作るときとか。

localStorage にアクセスするライブラリは、backbone-localstorage っていうのが Backbone.js のリポジトリ内にあるけど、Web SQL Database にアクセスするライブラリは見つからなかった。使いたいのは Web SQL Database なんだけどな。

ネットで探してみたけど見つからなかったので、自作してみた。

/**
 * データベース名
 */
Backbone.DATABASE_NAME = "backbone";

/**
 * データベースのバージョン
 */
Backbone.DATABASE_VERSION = "0.1";

/**
 * データベースの表示名
 */
Backbone.DATABASE_DISPLAY_NAME = "backbone";

/**
 * データベースの予測サイズ
 */
Backbone.DATABASE_SIZE = 1024 * 1024;

/**
 * Web SQL Database 上のテーブルを表します。
 *
 * @class
 * @param name テーブル名
 * @param columns 列の定義
 * @example
 *  var table = new Table("user", {
 *      name: "TEXT",
 *      email: "TEXT"
 *  });
 */
var Table = function(name, columns) {
    this.name = name;
    this.columns = columns;
};
/**
 * データベースを開きます。
 */
Table.openDatabase = function() {
    return openDatabase(
        Backbone.DATABASE_NAME,
        Backbone.DATABASE_VERSION,
        Backbone.DATABASE_DISPLAY_NAME,
        Backbone.DATABASE_SIZE);
};
/**
 * データベースを開いて SQL を実行します。
 *
 * @param sql 実行する SQL
 * @param params プレースフォルダに渡すパラメータ
 * @param options オプション
 * @example
 *  Table.executeSql("SELECT * FROM users", [], {
 *      success: function(result) {
 *          alert(result);
 *      }
 *  });
 */
Table.executeSql = function(sql, params, options) {
    var options = options || {};
    var db = Table.openDatabase();
    db.transaction(function(tx) {
        // 発行する SQL とパラメータをログに出力
        console.log(sql);
        console.log(params);

        tx.executeSql(sql, params, function(tx, rs) {
            if (options.success) {
                options.success(rs);
            }
        }, function(tx, error) {
            if (options.error) {
                options.error(error);
            }
        });
    });
};
/**
 * テーブルを作成します。
 *
 * @param options オプション
 * @example
 *  table.createTable({
 *      success: function(result) {
 *          alert("テーブルの作成に成功しました。");
 *      }
 *  });
 */
Table.prototype.createTable = function(options) {
    var columnNames = "";
    _.each(this.columns, function(type, col) {
        columnNames += " , " + col + " " + type;
    });

    var sql = "CREATE TABLE IF NOT EXISTS "
            + this.name
            + " (id INTEGER PRIMARY KEY "
            + columnNames
            + " ) ";

    Table.executeSql(sql, [], options);
};
/**
 * テーブルを削除します。
 *
 * @param options オプション
 * @example
 *  table.dropTable({
 *      success: function(result) {
 *          alert("テーブルの削除に成功しました。");
 *      }
 *  });
 */
Table.prototype.dropTable = function(options) {
    var sql = "DROP TABLE IF EXISTS " + this.name;
    Table.executeSql(sql, [], options);
};
/**
 * モデルを1件取得します。
 */
Table.prototype.find = function(model, options) {
    var sql = "SELECT * FROM " + this.name + " WHERE id = ?";
    var params = [model.id];

    // success を差し替える
    if (options.success) {
        var originalSuccess = options.success;
        options.success = function(result) {
            var row = result.rows.item(0);
            originalSuccess(row);
        };
    }

    Table.executeSql(sql, params, options);
};
/**
 * モデルを複数件取得します。
 */
Table.prototype.findAll = function(collection, options) {
    // SQL の組み立て
    var sql = "SELECT * FROM " + this.name;
    var params = [];
    if (collection.filterString) {
        sql += " WHERE " + collection.filterString;
        if (collection.filterParams) {
            params = collection.filterParams;
        }
    }
    if (collection.orderString) {
        sql += " ORDER BY " + collection.orderString;
    }
    
    // success を差し替える
    if (options.success) {
        var originalSuccess = options.success;
        options.success = function(result) {
            var models = [];
            for (var i = 0, len = result.rows.length; i < len; i++) {
                models.push(result.rows.item(i));
            }
            originalSuccess(models);
        };
    }

    Table.executeSql(sql, params, options);
};
/**
 * モデルを新しく作成します。
 */
Table.prototype.create = function(model, options) {
    // SQL を組み立てる
    var columnNames = "";
    var paramNames = "";
    var params = [];
    var first = true;
    _.each(this.columns, function(type, col) {
        if (!first) {
            columnNames += " , ";
            paramNames += " , ";
        }
        first = false;
        columnNames += col;
        paramNames += "?";
        var value = model.get(col);
        params.push(value);
    }, this);
    var sql = "INSERT INTO "
            + this.name
            + " ( "
            + columnNames
            + " ) VALUES ( "
            + paramNames
            + " ) ";

    // success を差し替える
    if (options.success) {
        var originalSuccess = options.success;
        options.success = function(result) {
            originalSuccess(model);
        };
    }

    // SQL を実行する
    Table.executeSql(sql, params, options);
};
/**
 * モデルを更新します。
 */
Table.prototype.update = function(model, options) {
    // SQL を組み立てる
    var columnNames = "";
    var params = [];
    var first = true;
    _.each(this.columns, function(type, col) {
        if (!first) {
            columnNames += " , ";
        }
        first = false;
        columnNames += col + " = ? ";
        var value = model.get(col);
        params.push(value);
    }, this);
    var sql = "UPDATE "
            + this.name
            + " SET "
            + columnNames
            + " WHERE id = ? ";
    params.push(model.id);
    
    // success を差し替える
    if (options.success) {
        var originalSuccess = options.success;
        options.success = function(result) {
            originalSuccess(model);
        };
    }
    
    // SQL を実行する
    Table.executeSql(sql, params, options);
};
/**
 * 指定したモデルを削除します。
 */
Table.prototype.destroy = function(model, options) {
    var sql = "DELETE FROM " + this.name + " WHERE id = ?";
    var params = [model.id];

    // success を差し替える
    if (options.success) {
        var originalSuccess = options.success;
        options.success = function(result) {
            originalSuccess(model);
        };
    }

    Table.executeSql(sql, params, options);
};

/**
 * オリジナルの sync メソッド。
 */
Backbone.originalSync = Backbone.sync;

Backbone.sync = function(method, model, options) {
    var table = model.table || model.model.prototype.table;
    switch (method) {
        case "read":
            if(model.id) {
                table.find(model, options);
            } else {
                table.findAll(model, options);
            }
            break;
        case "create":
            table.create(model, options);
            break;
        case "update":
            table.update(model, options);
            break;
        case "delete":
            table.destroy(model, options);
            break;
    }
}

とりあえず、自分が作ろうとしているアプリに必要な機能に絞って実装した。

使い方を簡単に説明すると、

var Book = Backbone.Model.extend({
    table: new Table("books", {
        title: "TEXT",
        author: "TEXT",
        price: "INTEGER"
    }
});

という風にすれば、fetch や save で Web SQL Database にアクセスするようになる。

不具合が見つかったら(多分)修正するけど、機能追加は必要に迫られるまでしないと思う。Fork、Pull Request は大歓迎。

というか、誰かがもっとちゃんとしたライブラリを作ってくれたらいいな。