はじめに
公式にも「Web SQL Database」の扱い方がさらっと書いてありますが、実際のアプリに適用するにはちょっと……うーん。
#Monacaに限らず、DB系のサンプルは大抵そうですけどねw
「DBを触るところはできるだけ隠ぺいしたい」
「バージョン管理したいよね」
「クエリを各TBLごとに書くのめんどくさい」
……という訳で、何とかしてみました。
特徴
- SELECT, INSERT等、基本的なクエリは、テーブルクラス定義のみで処理OK
- DB定義もテーブルクラス宣言のみで基本的にはOK
- DB処理は非同期
- バージョン管理可能
- DB関連スクリプト は、ライブラリ依存なし。(単体でも使えます)
ベース
[monaca][onsenui][AngularJS][ui-router]onsenui/ui-router最小限プロジェクト
html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
<script src="components/loader.js"></script>
<script src="js/script.js"></script>
<script src="js/db.js"></script>
<script src="js/dbTable.js"></script>
<link rel="stylesheet" href="components/loader.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body ng-app="MyApp">
<ons-page>
<ons-toolbar>
<div class="center">DB</div>
</ons-toolbar>
<div style="margin: 10px;">
<ons-button ui-sref="person">人物</ons-button>
<ons-button ui-sref="url">URL</ons-button>
</div>
<div style="padding: 20px 10px;" ui-view>
<h1>使い方</h1>
<ul>
<li>各画面では、初期処理として、全件データ検索を行います。</li>
<li>画面上部の入力フォームにデータを入力して…
<ul>
<li>「登録」ボタンを押下すると、新規登録(INSERT)されます。</li>
<li>「検索」ボタンを押下すると、条件付き検索されます。</li>
</ul>
</li></li>
<li>検索結果の項目を押下すると、入力フォームに値が設定されます。
<ul>
<li>値を変更し、「登録」ボタンを押下すると、更新(UPDATE)されます。</li>
<li>「削除」ボタンを押下すると、削除(DELETE)されます。</li>
</ul>
</li>
</ul>
</div>
</ons-page>
<ons-template id="person.html">
<div ng-controller="PersonController">
<ons-input type="text" ng-model="person_sei" modifier="material" float placeholder="姓"></ons-input>
<ons-input type="text" ng-model="person_mei" modifier="material" float placeholder="名"></ons-input>
<ons-input type="date" ng-model="person_birthday" modifier="material" float placeholder="生年月日"></ons-input>
<ons-select ng-model="person_sex" modifier="material" >
<option value="1">男性</option>
<option value="2">女性</option>
<option value="0">どちらでもない</option>
</ons-select>
<ons-button ng-click="save();">登録</ons-button>
<ons-button ng-click="delete();">削除</ons-button>
<ons-button ng-click="search();">検索</ons-button>
[{{person_id}}]
<ul>
<li ng-repeat="r in results" ng-click="modify(r);">
{{r.col_id}}: {{r.col_sei}}, {{r.col_mei}}, {{r.col_birthday}}, {{r.col_sex}}
</li>
</ul>
</div>
</ons-template>
<ons-template id="url.html">
<div ng-controller="UrlController">
<ons-input type="text" ng-model="url_name" modifier="material" float placeholder="名称"></ons-input>
<ons-input type="url" ng-model="url_url" modifier="material" float placeholder="URL"></ons-input>
<ons-button ng-click="save();">登録</ons-button>
<ons-button ng-click="delete();">削除</ons-button>
<ons-button ng-click="search();">検索</ons-button>
[{{url_id}}]
<ul>
<li ng-repeat="r in results" ng-click="modify(r);">
{{r.col_id}}: {{r.col_name}}, {{r.col_url}}
</li>
</ul>
</div>
</ons-template>
</body>
</html>
javascript
画面コントロール用
var myApp = angular.module('MyApp', ['onsen', 'ui.router']);
myApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('person', {
url: '/person',
templateUrl: 'person.html'
})
.state('url', {
url: '/url',
templateUrl: 'url.html'
})
}]);
myApp.controller('PersonController', ['$scope', 'MyDBService', function($scope, MyDBService) {
MyDBService.test();
loadList();
// リスト読込処理
function loadList() {
// リスト取得
MyDBService.connect().then(
function(db) {
// PERSONテーブルを全件検索
return MyDBService.select(db, new PersonTableCls());
}
).then(
function(resultList) {
// 検索結果を画面に設定する
$scope.results = resultList;
// 一旦初期化
$scope.person_id = "";
$scope.person_sei = "";
$scope.person_mei = "";
$scope.person_birthday = "";
$scope.person_sex = "";
}
);
};
// 保存ボタン押下時処理
$scope.save = function() {
var data = new PersonTableCls($scope.person_id, $scope.person_sei, $scope.person_mei, $scope.person_birthday, $scope.person_sex);
console.log(data);
// 登録実行
MyDBService.connect().then(
function(db) {
return MyDBService.save(db, data);
}
).then(
function(result) {
console.log("result = "+ result);
if (result) {
// 保存成功したら、リスト再読み込み
loadList();
}
else {
// 保存失敗
ons.notification.alert("保存失敗");
}
}
);
}
// 削除ボタン押下時処理
$scope.delete = function() {
var data = new PersonTableCls($scope.person_id, $scope.person_sei, $scope.person_mei, $scope.person_birthday, $scope.person_sex);
console.log(data);
// 登録実行
MyDBService.connect().then(
function(db) {
return MyDBService.delete(db, data);
}
).then(
function(result) {
console.log("result = "+ result);
if (result) {
// 削除成功したら、リスト再読み込み
loadList();
}
else {
// 削除失敗
ons.notification.alert("保存失敗");
}
}
);
}
$scope.search = function() {
var condition = new PersonTableCls(null, $scope.person_sei, $scope.person_mei, $scope.person_birthday, $scope.person_sex);
console.log(condition);
// リスト取得
MyDBService.connect().then(
function(db) {
// PERSONテーブルを条件指定検索
return MyDBService.select(db, condition);
}
).then(
function(resultList) {
// 検索結果を画面に設定する
$scope.results = resultList;
// 一旦初期化
$scope.person_id = "";
$scope.person_sei = "";
$scope.person_mei = "";
$scope.person_birthday = "";
$scope.person_sex = "";
}
);
}
// リスト項目押下時
$scope.modify = function(r) {
// 入力フォームに値設定
$scope.person_id = r.col_id;
$scope.person_sei = r.col_sei;
$scope.person_mei = r.col_mei;
$scope.person_birthday = r.col_birthday;
$scope.person_sex = r.col_sex;
}
}]);
myApp.controller('UrlController', ['$scope', 'MyDBService', function($scope, MyDBService) {
MyDBService.test();
loadList();
// リスト読込処理
function loadList() {
// リスト取得
MyDBService.connect().then(
function(db) {
// URLテーブルを全件検索
return MyDBService.select(db, new UrlTableCls());
}
).then(
function(resultList) {
// 検索結果を画面に設定する
$scope.results = resultList;
// 一旦初期化
$scope.url_id = "";
$scope.url_name = "";
$scope.url_url = "";
}
);
};
// 保存ボタン押下時処理
$scope.save = function() {
var data = new UrlTableCls($scope.url_id, $scope.url_name, $scope.url_url);
console.log(data);
// 登録実行
MyDBService.connect().then(
function(db) {
return MyDBService.save(db, data);
}
).then(
function(result) {
console.log("result = "+ result);
if (result) {
// 保存成功したら、リスト再読み込み
loadList();
}
else {
// 保存失敗
ons.notification.alert("保存失敗");
}
}
);
}
// 削除ボタン押下時処理
$scope.delete = function() {
var data = new UrlTableCls($scope.url_id, $scope.url_name, $scope.url_url);
console.log(data);
// 登録実行
MyDBService.connect().then(
function(db) {
return MyDBService.delete(db, data);
}
).then(
function(result) {
console.log("result = "+ result);
if (result) {
// 削除成功したら、リスト再読み込み
loadList();
}
else {
// 削除失敗
ons.notification.alert("保存失敗");
}
}
);
}
$scope.search = function() {
var condition = new UrlTableCls(null, $scope.url_name, $scope.url_url);
console.log(condition);
// リスト取得
MyDBService.connect().then(
function(db) {
// URLテーブルを条件指定検索
return MyDBService.select(db, condition);
}
).then(
function(resultList) {
// 検索結果を画面に設定する
$scope.results = resultList;
// 一旦初期化
$scope.url_id = "";
$scope.url_name = "";
$scope.url_url = "";
}
);
}
// リスト項目押下時
$scope.modify = function(r) {
// 入力フォームに値設定
$scope.url_id = r.col_id;
$scope.url_name = r.col_name;
$scope.url_url = r.col_url;
}
}]);
DB操作用
myApp.service('MyDBService', ['$q', '$timeout', function($q, $timeout){
// サービスで取り扱うテーブル一覧(クラスで管理)
var TABLES = [
new PersonTableCls(),
new UrlTableCls()
];
// 初期化時に登録するデータ
var INITIALIZE = [
new PersonTableCls(null, "山田", "花子", "2000-12-25", 2),
new UrlTableCls(null, "モナカプレス", "https://press.monaca.io/")
];
// テストメソッド
this.test = function() {
for (var tbl of TABLES) {
// テーブルクラス
console.log(tbl);
// カラムリスト
console.log(tbl.getColumns());
// IDを除いたカラムリスト
console.log(tbl.getColumnsIgnoreId());
}
for (var data of INITIALIZE) {
console.log(data);
// 値リスト
console.log(data.getValues());
// IDを除いた値リスト
console.log(data.getValuesIgnoreId());
}
}
// DBへの接続
// DBに何らかの処理を行う際には、必ずこれを通す
this.connect = function () {
var deferred = $q.defer();
$timeout(function(){
console.log('Start connect');
var db = window.openDatabase("MyDatabase", "", "My Database", 1024 * 1024);
// バージョン管理付DB
var M = new Migrator(db);
// ver.1 (初回生成)
M.migration(1, function(tx){
console.log("create db ver.1");
// テーブル作成 ---------------
for (var id in TABLES) {
// リストにあるテーブルを作成していく
var tbl = TABLES[id];
// 一旦既存のがあったら削除する
var sql = 'DROP TABLE IF EXISTS '+ tbl.table_name;
console.log(sql);
tx.executeSql(sql);
// 改めてテーブルを作成する
sql = 'CREATE TABLE IF NOT EXISTS '+ tbl.table_name +' (col_id integer primary key autoincrement, '+ tbl.getColumnsIgnoreId().join(',') +')';
console.log(sql);
tx.executeSql(sql);
}
for (var data of INITIALIZE) {
console.log(data);
var params = data.getValuesIgnoreId();
// 値の数だけqueryを生成しておく
var query = [];
for (var p of params) {
query.push("?");
}
// 初期データを投入する
var sql = 'INSERT INTO '+ data.table_name +' ('+ data.getColumnsIgnoreId().join(',') +') VALUES ('+ query.join(',') +')';
console.log(sql);
tx.executeSql(sql, params);
}
});
M.doIt();
console.log('End connect');
deferred.resolve(db);
}, 0);
return deferred.promise;
};
// 検索条件を元にデータを返す
// db: conncectで取得したdbクラス
// condtion: BaseTableClsのサブクラス
this.select = function(db, condition) {
var deferred = $q.defer();
var resultList = [];
$timeout(function(){
db.readTransaction(
function(tx){
console.log("select 開始 TBL="+ condition.table_name);
var sql = 'SELECT * FROM '+ condition.table_name;
var columns = [];
var values = [];
// 条件オブジェクトで値が入っているものを条件として加える
for (var p in condition) {
if (p.toLowerCase().indexOf("col_") == 0 && condition[p]) {
columns.push(p);
values.push(condition[p]);
}
}
var whereSql = "";
if ( columns.length > 0 ) {
// 条件があった場合、WHERE句追加
whereSql = " WHERE ";
for (var i = 0; i < columns.length; i++) {
if (i > 0) {
// 1個以上の場合、AND追加
whereSql += " AND ";
}
whereSql += columns[i] + " = ?";
}
console.log(whereSql);
}
sql += whereSql;
console.log(sql);
tx.executeSql(sql , values
, function(tx, rs){
// 成功時
for (var i = 0; i < rs.rows.length; i++) {
var row = rs.rows.item(i);
// console.log(row);
// 検索条件のコンストラクタからオブジェクト再生成
var Const = condition.constructor;
var data = new Const();
// 検索結果をオブジェクトに設定
for (var r in row) {
data[r] = row[r];
}
console.log(data);
resultList.push(data);
}
console.log("resultList.length: "+ resultList.length);
deferred.resolve(resultList);
}, function(tx, err) {
// SELECT失敗時
console.error("select 失敗: TBL="+ condition.table_name);
console.error(err);
deferred.resolve(null);
});
},
function(err){
// トランザクション失敗時
console.error("select TRANSACTION 失敗: TBL="+ condition.table_name);
console.error(err);
},
function(){
// トランザクション成功時
console.log("select TRANSACTION 成功: TBL="+ condition.table_name);
}
);
}, 0);
return deferred.promise;
}
// IDを元に、追加または更新を実行する
// db: conncectで取得したdbクラス
// data: BaseTableClsのサブクラス
this.save = function(db, data) {
var deferred = $q.defer();
$timeout(function(){
db.transaction(
function(tx){
console.log("save 開始 TBL="+ data.table_name);
var params = data.getValuesIgnoreId();
var sql = "";
if (data.col_id) {
// IDがある場合、更新
var columns = data.getColumnsIgnoreId();
var sets = [];
for (var c of columns) {
sets.push(c +" = ? ");
}
sql = 'UPDATE '+ data.table_name +' SET '+ sets.join(',') + " WHERE col_id = "+ data.col_id;
}
else {
// IDがない場合、追加
// 値の数だけqueryを生成しておく
var query = [];
for (var p of params) {
query.push("?");
}
sql = 'INSERT INTO '+ data.table_name +' ('+ data.getColumnsIgnoreId().join(',') +') VALUES ('+ query.join(',') +')';
}
console.log(sql);
console.log(params);
tx.executeSql(sql, params
, function(tx, rs){
// 成功時
console.log("save executeSql 成功: TBL="+ data.table_name);
deferred.resolve(true);
}, function(tx, err) {
// 失敗時
console.error("save executeSql 失敗: TBL="+ data.table_name);
console.error(err);
});
},
function(err){
// 失敗時
console.error("save TRANSACTION 失敗: TBL="+ data.table_name);
console.error(err);
},
function(){
// 成功時
console.log("save TRANSACTION 成功: TBL="+ data.table_name);
}
);
}, 0);
return deferred.promise;
}
// IDを元に、削除を実行する
// db: conncectで取得したdbクラス
// data: BaseTableClsのサブクラス
this.delete = function(db, data) {
var deferred = $q.defer();
$timeout(function(){
db.transaction(
function(tx){
console.log("delete 開始 TBL="+ data.table_name);
var sql = 'DELETE FROM '+ data.table_name + " WHERE col_id = ?";
console.log(sql);
// IDのみを引数とし、SQL実行
tx.executeSql(sql, [data.col_id]
, function(tx, rs){
// 成功時
console.log("delete executeSql 成功: TBL="+ data.table_name);
deferred.resolve(true);
}, function(tx, err) {
// 失敗時
console.error("delete executeSql 失敗: TBL="+ data.table_name);
console.error(err);
});
},
function(err){
// 失敗時
console.error("delete TRANSACTION 失敗: TBL="+ data.table_name);
console.error(err);
},
function(){
// 成功時
console.log("delete TRANSACTION 成功: TBL="+ data.table_name);
}
);
}, 0);
return deferred.promise;
}
}]);
// DBバージョン管理クラス
// 参考: http://blog.maxaller.name/2010/03/html5-web-sql-database-intro-to-versioning-and-migrations/
function Migrator(db){
var migrations = [];
this.migration = function(number, func){
migrations[number] = func;
};
var doMigration = function(number){
if(migrations[number]){
db.changeVersion(db.version, String(number), function(t){
migrations[number](t);
}, function(err){
if(console.error) console.error("Error!: %o", err);
}, function(){
doMigration(number+1);
});
}
};
this.doIt = function(){
var initialVersion = parseInt(db.version) || 0;
try {
doMigration(initialVersion+1);
} catch(e) {
if(console.error) console.error(e);
}
}
}
参考)HTML5 Web SQL Database – Intro to Versioning and Migrations « occasionally useful
DBのテーブル管理用
// テーブル:人物情報
var PersonTableCls = (function () {
// コンストラクタ
function PersonTableCls(id, sei, mei, birthday, sex) {
// 親クラスのコンストラクタ呼び出し
BaseTableCls.call(this, "PERSON_TBL", id);
// カラム:姓
this.col_sei = sei;
// カラム:名
this.col_mei = mei;
// カラム:生年月日
this.col_birthday = birthday;
// カラム:性別
this.col_sex = sex;
}
// configure prototype
PersonTableCls.prototype = new BaseTableCls();
PersonTableCls.prototype.constructor = PersonTableCls;
return PersonTableCls; // return constructor
})();
var UrlTableCls = (function () {
// コンストラクタ
function UrlTableCls(id, name, url) {
// 親クラスのコンストラクタ呼び出し
BaseTableCls.call(this, "URL_TBL", id);
// サブクラス自身のプロパティ
// カラム:サイト名称
this.col_name = name;
// カラム:サイトURL
this.col_url = url;
}
// configure prototype
UrlTableCls.prototype = new BaseTableCls();
UrlTableCls.prototype.constructor = UrlTableCls;
return UrlTableCls; // return constructor
})();
function BaseTableCls(tableName, id) {
this.table_name = tableName;
// ID (オートインクリメント用)
this.col_id = id;
// カラムリストを返す
this.getColumns = function() {
var columns = [];
// 自身の中で"col_"から始まるのだけ抽出する
for (var p in this) {
if (p.toLowerCase().indexOf("col_") == 0) {
columns.push(p);
}
}
return columns;
}
// IDを除いたカラムリストを返す
this.getColumnsIgnoreId = function() {
var columns = [];
// 自身の中で"col_"から始まるのだけ抽出する
// ただしIDは除く
for (var p in this) {
if (p.toLowerCase().indexOf("col_") == 0 && p.toLowerCase() != "col_id") {
columns.push(p);
}
}
return columns;
}
// 値リストを返す
this.getValues = function() {
var values = [];
// 自身の中で"col_"から始まるのだけ抽出する
for (var p in this) {
if (p.toLowerCase().indexOf("col_") == 0) {
values.push(this[p]);
}
}
return values;
}
// IDを除いた値リストを返す
this.getValuesIgnoreId = function() {
var values = [];
// 自身の中で"col_"から始まるのだけ抽出する
// ただしIDは除く
for (var p in this) {
if (p.toLowerCase().indexOf("col_") == 0 && p.toLowerCase() != "col_id") {
values.push(this[p]);
}
}
return values;
}
}
参考)JavaScriptのサブクラス定義チートシート - Qiita
CSS
ons-input, ons-select {
display: block;
margin-top: 1.5em;
margin-bottom: 1em;
}
解説
…と言っても、特徴でほとんど書いちゃったから書く事ないんですがw
prototype
やら constructor
を分からないなりに駆使してみました。
BaseTableCls
を継承したサブクラスで、テーブル定義を行います。
カラム名は、 col_
で始める、というのがサブクラスのルールです。
コントローラ側では、テーブル定義サブクラスを扱うだけで、DBの細かいところを意識する必要はありません。
非同期処理もコントローラ側ではできるだけシンプルにしたつもりです。
db.js
では、各TBLの細かいカラム構成などはすべて吸収しています。
今回は、ver.1の場合のみを定義していますが、バージョンアップする場合には、M.migration
の処理をver.1の後に記載してください。
(詳細は、参考URLを参照してください)
ソースコード
最後に
初めてアドベントカレンダーなるものに参加させていただきました。
来年もぼちぼち色んなことにチャレンジできたらいいな、と思っています。
メリークリスマス&よいお年を!