I'm fairly new to both Ionic and Angular. I'm trying to write to a SQLite database and it's intermittently successful. When I do a for loop and insert records rapidly, some succeed and some fail (without apparent error). The query execution uses promises, so multiple queries may be trying to execute concurrently. It seems that this causes a synchronization issue in SQLite - or the SQLite plugin. I've tried opening a new DB connection with every execute(), reopening the existing connection on every execute(), and I've also tried opening a connection globally in app.js once and reusing that connection. They all seem to behave the same.
A custom 'dbQuery' function is used to build queries and is chainable. The idea is that any place in my app with access to the DB service can execute a query and expect the results to flow into an "out" variable like:
var my_query_results = [];
DB.begin().select("*","table1").execute(my_query_results).then(function() {
console.log("Query complete");
});
That much works, but the problem comes from writes:
var records = [
{id:1, name:"Bob"},
{id:2, name:"John"},
{id:3, name:"Jim"},
];
for (var i = 0; i < records.length; i++) {
var obj = records[i];
var result = [];
DB.begin().debug(true).insert("table1", "(id,name)", "("+obj.id+","+ obj.name+")").execute(result).then(function () {
console.log("Inserted record", JSON.stringify(obj));
});
}
Sometimes it fails without any logged or apparent error, sometimes it succeeds. If I perform the inserts slowly over time, it seems to work without issue.
app.js
var db;
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])
.run(function ($ionicPlatform, $cordovaSQLite, appConfig, $q) {
$ionicPlatform.ready(function () {
db = $cordovaSQLite.openDB({
name: appConfig.sqlite_db,
location: appConfig.sqlite_db_location
});
dbQuery = function () {
this.bDebug = false;
this.query = "";
this.result = [];
this.params = [];
this.debug = function (value) {
this.bDebug = (value === true ? true : false);
return this;
};
this.rawQuery = function (query) {
this.query = query;
return this;
};
this.insert = function (table, fields, values) {
this.query = "INSERT INTO '" + table + "' (" + fields + ") VALUES (" + values + ")";
return this;
};
this.select = function (fields, table) {
this.query = "SELECT " + fields + " FROM " + table;
return this;
};
this.delete = function (query) {
this.query = "DELETE FROM " + query;
return this;
};
this.where = function (column, expression, value) {
expression = expression || "=";
this.query += " WHERE `" + column + "` " + expression + " ? ";
this.params[this.params.length] = value;
return this;
};
this.and = function (column, expression, value) {
expression = expression || "=";
this.query += " AND '" + column + "' " + expression + " ? ";
this.params[this.params.length] = value;
return this;
};
this.execute = function (out_var) {
var self = this;
this.result = out_var;
if (this.bDebug) {
console.log("Compiled query is", this.query);
}
var deferred = $q.defer();
db.open(function () {
console.log("Opened");
}, function () {
console.log("Failed");
});
//actually execute the query
$cordovaSQLite.execute(db, this.query, this.params).then(
function (res) {
for (var i = 0; i < res.rows.length; i++) {
self.result.push(res.rows.item(i));
console.log("Added row to set", JSON.stringify(res.rows.item(i)));
}
if (res.rows.length == 0 && self.bDebug === true) {
console.log("No results found ");
}
deferred.resolve();
}, function (err) {
console.error(JSON.stringify(err), this.query);
deferred.reject();
});
return deferred.promise;
}
services.js
.factory('DB', function ($ionicPlatform) {
return {
begin: function () {
return new dbQuery();
}
}
})
.factory('DbBootstrap', function ($cordovaSQLite, appConfig, $q, $state, DB) {
return {
wipe: function () {
DB.begin().rawQuery("DELETE FROM table1").execute().then(function () {
console.log("Purged records");
});
},
init: function () {
var result = []; //out variable
DB.begin().rawQuery("CREATE TABLE IF NOT EXISTS table1 (id integer primary key, name text)").execute(result).then(function () {
console.log("Schema create returned", JSON.stringify(result));
});
var records = [
{
id: 1, name:'Jim'
...
},
{
id: 2, name:'Bob'
...
},
{
id: 3, name:'John'
...
}
];
for (var i = 0; i < records.length; i++) {
var obj = records[i];
var result = [];
DB.begin().debug(true).insert("table1", "(id,name)", "(obj.id, obj.name).execute(result).then(function () {
console.log("Inserted record", JSON.stringify(obj));
});
}
}
})
I'm sure I'm missing something fundamental about angular, promises, and sqlite locking. If anyone has advice I'd really appreciate it.
I resolved this following the excellent advice here - Angular/Ionic and async SQLite - ensuring data factory initialised before return
The key issue being that I needed to wrap all my DB operations in promises and use them for orderly initialization and callbacks.
.factory('DB', function ($q, $cordovaSQLite, appConfig) {
//private variables
var db_;
// private methods - all return promises
var openDB_ = function (dbName, location) {
var q = $q.defer();
try {
db_ = $cordovaSQLite.openDB({
name: dbName,
location: location
});
q.resolve(db_);
} catch (e) {
q.reject("Exception thrown while opening DB " + JSON.stringify(e));
}
return q.promise;
};
var performQuery_ = function (query, params, out) {
var q = $q.defer();
params = params || [];
out = out || [];
//open the DB
openDB_(appConfig.sqlite_db, appConfig.sqlite_db_location)
.then(function (db) {
//then execute the query
$cordovaSQLite.execute(db, query, params).then(function (res) {
//then add the records to the out param
console.log("Query executed", JSON.stringify(query));
for (var i = 0; i < res.rows.length; i++) {
out.push(res.rows.item(i));
console.log("Added row to set", JSON.stringify(res.rows.item(i)));
}
if (res.rows.length == 0 && self.bDebug === true) {
console.log("No results found ");
}
}, function (err) {
console.log("Query failed", JSON.stringify(query));
q.reject();
});
db_.open(function () {
q.resolve("DB Opened")
}, function () {
q.reject("Failed to open DB");
});
}, function (err) {
console.log(JSON.stringify(err), this.query);
q.reject(err);
});
return q.promise;
};
// public methods
var execute = function (query, params, out) {
var q = $q.defer();
performQuery_(query, params, out).then(function () {
q.resolve([query, params]);
}, function (err) {
q.reject([query, params, err]);
});
return q.promise;
};
return {
execute: execute
};
})
EDIT2:
var LanguageCtrl;
LanguageCtrl = function($scope, $http, $window) {
var vm;
vm = this;
if ($window.navigator.language.indexOf('it') !== -1) {
this.lang = 'it';
this.setLanguage = 'it';
} else {
this.lang = 'en';
this.setLanguage = 'us';
}
this.message = [];
this.pizze = [];
this.getPizze = function(lang) {
$http.get('localization/pizze-' + lang + '.json').success(function(pizze) {
vm.pizze = pizze;
});
};
this.setLanguage = function(lang) {
$http.get('localization/' + lang + '.json').success(function(data) {
vm.lang = lang;
vm.message = data;
vm.getPizze(vm.lang);
$window.location.href = '#!/order';
});
};
this.setLanguage(this.lang);
};
angular.module('myApp').controller('LanguageCtrl', LanguageCtrl);
EDIT: what I said doesn't work, doesn't work when I use this rather than $scope!
I'm moving to the controller as syntax and everything's fine when I do this for variables, but when I try the same thing for arrays populated by an $http.get, the code breaks.
Don't care about that horrible <button onclick>, just wanted to test code postponing the style for links.
controller
var LanguageCtrl;
LanguageCtrl = function($scope, $http, $window) {
if ($window.navigator.language.indexOf('it') !== -1) {
this.lang = 'it';
this.setLanguage = 'it';
} else {
this.lang = 'en';
this.setLanguage = 'us';
}
this.message = [];
this.pizze = [];
$scope.getPizze = function(lang) {
$http.get('localization/pizze-' + lang + '.json').success(function(pizze) {
$scope.pizze = pizze;
});
};
$scope.setLanguage = function(lang) {
$http.get('localization/' + lang + '.json').success(function(data) {
this.lang = lang;
$scope.message = data;
$scope.getPizze(this.lang);
$window.location.href = '#!/order';
});
};
$scope.setLanguage(this.lang);
};
angular.module('myApp').controller('LanguageCtrl', LanguageCtrl);
index.html works
<html ng-app="myApp" ng-controller="LanguageCtrl as langctrl" lang="{{langctrl.lang}}">
order.html doesn't
<button onclick="location.href='#!/cart'" ng-disabled="howManyPizze === 0">{{langctrl.message.cart}} {{howManyPizze === 0 ? langctrl.message.empty : '(' + howManyPizze + ')'}}</button>
<button onclick="location.href='#!/'">{{langctrl.message.change}}</button>
The problem is that the self-referencing this is not available when your $http.get() resolves. That is why you'll often see var vm = this; or something similar as the very first line in a controller when using the 'Controller as' syntax. By assigning this to a local variable in the controller it gives you access when your promise resolves. In short, add...
var vm = this;
...to your controller and then change your $http.get() to this...
vm.setLanguage = function(lang) {
$http.get('localization/' + lang + '.json').success(function(data) {
vm.lang = lang;
vm.message = data;
vm.getPizze(vm.lang);
$window.location.href = '#!/order';
});
};
UPDATE
I missed this on my original answer:
$scope.getPizze = function(lang) {
$http.get('localization/pizze-' + lang + '.json').success(function(pizze) {
$scope.pizze = pizze;
});
};
That needs to be changed to eliminate the use of $scope.
vm.getPizze = function(lang) {
$http.get('localization/pizze-' + lang + '.json').success(function(pizze) {
vm.pizze = pizze;
});
};
var LanguageCtrl;
LanguageCtrl = function( $http, $window) {
var parent = this;
if ($window.navigator.language.indexOf('it') !== -1) {
parent.lang = 'it';
parent.setLanguage = 'it';
} else {
parent.lang = 'en';
parent.setLanguage = 'us';
}
this.message = [];
this.pizze = [];
parent.getPizze = function(lang) {
$http.get('localization/pizze-' + lang + '.json').success(function(pizze) {
console.log("Your language is:"+lang)
parent.pizze = pizze;
});
};
parent.setLanguage = function(lang) {
$http.get("example.json").success(function(data) {
// $scope.$apply(function(){
console.log("Your language is:"+lang)
parent.lang = lang;
parent.message = data;
parent.getPizze(parent.lang);
$window.location.href = '#!/order';
//})
});
};
parent.setLanguage(parent.lang);
};
angular.module('myApp',[]).controller('LanguageCtrl', LanguageCtrl);
Use above code to work your angular code.
Created Plukar here https://plnkr.co/edit/psOaylBtnCsLDIJydT9N?p=preview
I am using promises in my controller, and most of the times it works well. But sometimes it just loads forever and the WordPress.getAllCategories() function does not even get called.
This is my controller:
var mod = angular.module('app.controllers.home', []);
mod.controller('HomeCtrl', function ($scope, $q, $sce, $ionicPlatform, WordPress, Loading) {
console.log('HomeCtrl init');
$scope.$on('$ionicView.enter', function () {
Loading.show();
WordPress.getAllCategories()
.then(function (cats) {
console.info(angular.toJson(cats));
console.info('cats ^');
$q.all(cats.data.map(function (cat) {
var d = $q.defer();
console.error(cat.name);
WordPress.getLatestPostOfCategory(cat.id)
.then(function (post) {
console.debug(post.data.title.rendered);
WordPress.getMediaById(post.data.featured_media)
.then(function (media) {
console.log(media.data.source_url);
cat.firstPost = {};
cat.firstPost.id = post.data.id;
cat.firstPost.title = post.data.title.rendered;
cat.firstPost.content = post.data.content.rendered;
if (cat.firstPost.title.length > 50) {
cat.firstPost.title = cat.firstPost.title + '...';
}
if (cat.firstPost.content.length > 70) {
cat.firstPost.content = cat.firstPost.content.substr(0, 60) + '...';
}
cat.firstPost.thumbnail = media.data.source_url;
d.resolve(cat);
}, function (err) {
console.error(angular.toJson(err));
});
});
return d.promise;
})).then(function (cats) {
console.log('Loaded all articles and for main page.');
$scope.homeCategories = cats;
Loading.hide();
});
});
});
});
Is there anything wrong in my controller?
P.S. I also debug all the WordPress service functions and they work just fine, and provide the needed data.
EDIT:
Sometimes when it loads forever, I see the console.error(cat.name); debug message only logs 3 messages. But still proceeds to the next function...
This is how I solved it, by Bergi's advice.
Source for help: Promise anti pattern by Gorgi Kosev (bluebird)
var categories = [];
function sort() {
return WordPress.getAllCategories()
.then(function (cats) {
console.log('thens');
return $q.all(cats.data.map(function (cat) {
console.info('cat: ' + cat.name);
var category = {};
category.name = cat.name;
return WordPress.getLatestPostOfCategory(cat.id)
.then(function (post) {
var post = post.data;
category.post = {};
category.post.id = post.id;
category.post.title = post.title.rendered;
category.post.content = post.content.rendered;
console.log('ID: ' + category.post.id + ', title: ' + category.post.title);
return WordPress.getMediaById(post.featured_media);
}).then(function (media) {
category.post.thumbnail = media.data.source_url;
categories.push(category);
console.log('Pushed category "' + category.name + '"');
});
}));
}, function (err) {
console.error('ERR1');
console.error(angular.toJson(err));
});
}
sort()
.then(function () {
console.info('LOADED ALL CATEGORIES');
$scope.categories = categories;
}, function (err) {
console.error('err:' + angular.toJson(err));
});
I have a set of data within a users profile which holds the UID of all the posts they have liked. I am using a data snapshot to show these images within the users profile, but I would like to load it lazy or use ionic infinite scroll to load more items as the user scrolls.
I tried by making two duplicate snap shots. First one intialises the feed upon the page loading, and then I wanted the second one to load by using ionic infinite scroller.
But it doesn't work.
Does anyone know of a better way to do this?
service.js
getWardrobeFeed: function(){
var defered = $q.defer();
var user = User.getLoggedInUser();
var wardrobeKeyRef = new Firebase(FIREBASE_URL + '/userProfile/' + user.uid + '/wardrobe');
wardrobeKeyRef.orderByKey().limitToLast(2).on('child_added', function (snap) {
var imageId = snap.key();
userImageRef.child(imageId).on('value', function (snap) {
$timeout(function () {
if (snap.val() === null) {
delete userWardrobeFeed[imageId];
} else {
userWardrobeFeed[imageId] = snap.val();
}
});
});
});
wardrobeKeyRef.orderByKey().limitToLast(2).on('child_removed', function (snap) {
var imageId = snap.key();
$timeout(function () {
delete userWardrobeFeed[imageId];
});
});
defered.resolve(userWardrobeFeed);
return defered.promise;
},
getWardrobeItems: function(){
var defered = $q.defer();
var user = User.getLoggedInUser();
var wardrobeKeyRef = new Firebase(FIREBASE_URL + '/userProfile/' + user.uid + '/wardrobe');
wardrobeKeyRef.orderByKey().limitToFirst(2).on('child_added', function (snap) {
var imageId = snap.key();
userImageRef.child(imageId).on('value', function (snap) {
$timeout(function () {
if (snap.val() === null) {
delete userWardrobe[imageId];
} else {
userWardrobe[imageId] = snap.val();
}
});
});
});
wardrobeKeyRef.orderByKey().on('child_removed', function (snap) {
var imageId = snap.key();
$timeout(function () {
delete userWardrobe[imageId];
});
});
defered.resolve(userWardrobe);
return defered.promise;
},
controller.js
$scope.userWardrobe = [];
Service.getWardrobeFeed().then(function(items){
$scope.userWardrobe = items;
});
$scope.loadMore = function() {
Service.getWardrobeItems().then(function(items){
$scope.userWardrobe = $scope.userWardrobe.concat(items);
$scope.$broadcast('scroll.infiniteScrollComplete');
});
};
this is my controller code
$scope.loadMajorObjects = function () {
var appId = $scope.currentAppId;
var type = $scope.majorObject.type;
if (_.isEmpty(appId))
return;
var cacheKey = { key: cacheKeys.objectTypeList($scope.asCacheOptions({ ObjectType: type })) };
return dataFactory.get("/MajorObject/All?applicationId=" + appId + "&type=" + type, { cache: cacheKey })
.then(function (result) {
$scope.majorObjects = result.data.data;
$scope.majorObjectsList = $scope.majorObjects.slice(0, $scope.itemsPerPage);
$scope.totalItems = $scope.majorObjects.length;
$scope.isLoad = true;
});
};
and this is test case and in this i am calling $scope.loadMajorObject
describe("Testing MajorObjectsCtrl", function () {
//checking dataFactory.get called only once
it("should call dataFactory.get and called only once", function () {
scope.currentAppId = mockApplicationId;
scope.applications = applicationsMockData;
scope.itemsPerPage = 10;
$controller('AppSettingCtrl',
{
$scope: scope,
dataFactory: mainmockdataFactory
});
expect(mockdataFactory.get.calls.count()).toBe(0);
scope.currentAppId = mockApplicationId;
scope.majorObjects.slice();
scope.loadMajorObjects();
scope.$digest();
expect(mockdataFactory.get).toHaveBeenCalled();
expect(mockdataFactory.get.calls.count()).toBe(1);
});
when i call this function it is throwing and exception as object doesn't support property or method 'slice', how to rectify this?