As the title, how to use Vacuum to shrink a SQLite database using FMDB?
Thanks in advance!
Thanks for all your support
I've found out the answer : [database executeUpdate:#"vacuum"];
1.Database Updating
I have a single database controller — VSDatabaseController in my latest app — that talks to SQLite via FMDB.
FMDB differentiates between updates and queries. To update the database the app calls:
-[VSDatabaseController runDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock]
VSDatabaseUpdateBlock is simple:
typedef void (^VSDatabaseUpdateBlock)(FMDatabase *database);
runDatabaseBlockInTransaction is also simple:
- (void)runDatabaseBlockInTransaction:(VSDatabaseUpdateBlock)databaseBlock {
dispatch_async(self.serialDispatchQueue, ^{
#autoreleasepool {
[self beginTransaction];
databaseBlock(self.database);
[self endTransaction];
}
});
}
Here’s a simple example of a call to update the database:
SELECT ALL
- (void)emptyTagsLookupTableForNote:(VSNote *)note {
NSString *uniqueID = note.uniqueID;
[self runDatabaseBlockInTransaction:^(FMDatabase *database) {
[database executeUpdate:
#"delete from tagsNotesLookup where noteUniqueID = ?;", uniqueID];
}];
}
[self.database executeUpdate:
#"CREATE INDEX if not exists noteUniqueIDIndex on tagsNotesLookup (noteUniqueID);"];
Database Fetching
To fetch objects, the app calls:
SELECT ALL
-[VSDatabaseController runFetchForClass:(Class)databaseObjectClass
fetchBlock:(VSDatabaseFetchBlock)fetchBlock
fetchResultsBlock:(VSDatabaseFetchResultsBlock)fetchResultsBlock];
These two lines do much of the work:
SELECT ALL
FMResultSet *resultSet = fetchBlock(self.database);
NSArray *fetchedObjects = [self databaseObjectsWithResultSet:resultSet
class:databaseObjectClass];
A database fetch using FMDB returns an FMResultSet. With that resultSet you can step through and create model objects.
3.Keeping Objects in Memory
FMResultSet *resultSet = [self.database executeQuery:#"select uniqueID from some_table"];
4.Web APIs
- (void)uploadNote:(VSNote *)note {
VSNoteAPICall *apiCall = [[VSNoteAPICall alloc] initWithNote:[note detachedCopy]];
[self enqueueAPICall:apiCall];
}
5.Handling Web API Return Values
VSNote *cachedNote = [self.mapTable objectForKey:downloadedNote.uniqueID];
6.Database Migration
[self.database executeUpdate:#"CREATE TABLE if not exists tags "
"(uniqueID TEXT UNIQUE, name TEXT, deleted INTEGER, deletedModificationDate DATE);"];
I hope, it will help you.
Related
I created new DB in Documents using SQLite Manager.
Created a table there too with a sample row.
This code I am using for db path :
+(ModelManager *) getInstance
{
if(!instance)
{
instance=[[ModelManager alloc]init];
instance.database=[FMDatabase databaseWithPath:[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:#"Inventorydb.sqlite"]];
}
return instance;
}
Now when I am using function to display data; it shows :
DB Error: 1 "no such table: inventorydata"
Code for Display data is like this :
-(void) displayData
{
[instance.database open];
FMResultSet *resultSet=[instance.database executeQuery:#"SELECT * FROM inventorydata"];
if(resultSet)
{
while([resultSet next])
NSLog(#"UPC : %# Name : %#",[resultSet stringForColumn:#"upc"],[resultSet stringForColumn:#"name"]);
}
[instance.database close];
}
Image Showing Created table in sqlite manager.
Whats the issue here.
I missed to copy DB file inside App Delegate file.
[Util copyFile:#"Inventorydb.sqlite"];
Is this possible and how would one go about it if it is? Info on the topic is a bit sparse given a Google & Stack search, lots on batch inserts, but nothing solid on batch updates.
Yes it is possible if you insert the correct SQL to do it, but your question is a bit vague.
Rather than trying to update multiple records in a query though, why don't you use a transaction queue instead? Pass your queries in as an array to this function. (requires you have setup an FMDatabase dbQueue of course)
-(BOOL) executeQueryArray:(NSMutableArray*)queryArray {
__block BOOL noErrors = YES;
[self.dbQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
db.logsErrors = YES;
for (NSString* query in queryArray) {
if (![db executeUpdate:query]) noErrors = NO;
}
*rollback = !noErrors;
}];
return noErrors;
}
When I encrypt my database using sqlcipher, and call inDatabase in FMDatabaseQueue——success!
But When I change inDatabase to inTransaction, the console says "File is encrypted or is not a database".
The code:
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:st_dbPath];
// success
[queue inDatabase:^(FMDatabase *db) {
[db setKey:st_dbKey];
[db executeUpdate:#"INSERT INTO t_user VALUES (16)"];
}];
// fail : File is encrypted or is not a database
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db setKey:st_dbKey];
[db executeUpdate:#"INSERT INTO t_user VALUES (17)"];
}];
And the encrypt database code:
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDir = [documentPaths objectAtIndex:0];
NSString *ecDB = [documentDir stringByAppendingPathComponent:st_dbEncryptedName];
// SQL Query. NOTE THAT DATABASE IS THE FULL PATH NOT ONLY THE NAME
const char* sqlQ = [[NSString stringWithFormat:#"ATTACH DATABASE '%#' AS encrypted KEY '%#';", ecDB, st_dbKey] UTF8String];
sqlite3 *unencrypted_DB;
if (sqlite3_open([st_dbPath UTF8String], &unencrypted_DB) == SQLITE_OK) {
// Attach empty encrypted database to unencrypted database
sqlite3_exec(unencrypted_DB, sqlQ, NULL, NULL, NULL);
// export database
sqlite3_exec(unencrypted_DB, "SELECT sqlcipher_export('encrypted');", NULL, NULL, NULL);
// Detach encrypted database
sqlite3_exec(unencrypted_DB, "DETACH DATABASE encrypted;", NULL, NULL, NULL);
sqlite3_close(unencrypted_DB);
}
else {
sqlite3_close(unencrypted_DB);
NSAssert1(NO, #"Failed to open database with message '%s'.", sqlite3_errmsg(unencrypted_DB));
}
the encrypt code come from there:http://www.guilmo.com/fmdb-with-sqlcipher-tutorial/
Calling inTransaction causes the SQL statement begin exclusive transaction to be executed on your database before calling your completion block. Therefore that SQL is executed before you have a chance to call setKey.
You could instead use inDatabase and call beginTransaction on the FBDatabase instance that is passed back like this:
[self.queue inDatabase:^(FMDatabase *db) {
[db setKey:st_dbKey];
[db beginTransaction];
[db executeUpdate:#"INSERT INTO t_user VALUES (17)"];
[db commit];
}];
Gus Hovland's answer works but I think what works better is to change inTransaction: to inDeferredTransaction:. From FMDB's documentation for inTransaction:
" Unlike SQLite's BEGIN TRANSACTION, this method currently performs
an exclusive transaction, not a deferred transaction. This behavior
is likely to change in future versions of FMDB, whereby this method
will likely eventually adopt standard SQLite behavior and perform
deferred transactions. If you really need exclusive tranaction, it is
recommended that you use inExclusiveTransaction, instead, not only
to make your intent explicit, but also to future-proof your code."
I am using Azure Mobile Service as a backend for an iOS application. I have set up everything to work with offline sync which allows me to view, add, or modify data even when there is no network connection. I am running into a problem when I add a new object into a table. The add works well locally but when I synchronize data it creates a duplicate item on the local database with a slightly different objectId. The created item is not duplicated on the server side.
Here's how I am setup. By the way, thanks to #TheBasicMind for posting this model.
Here's a link to his explanation of the model: enter link description here
Here's what I do to setup the sync context and sync table:
// Initialize the Mobile Service client with your URL and key
MSClient *client = self.hpc.client;
NSManagedObjectContext *context = self.hpc.syncContext;
MSCoreDataStore *store = [[MSCoreDataStore alloc] initWithManagedObjectContext:context];
client.syncContext = [[MSSyncContext alloc] initWithDelegate:syncDelegate dataSource:store callback:nil];
// Add a Mobile Service filter to enable the busy indicator
self.client = [client clientWithFilter:self];
// Create an MSSyncTable instance to allow us to work with the Athlete table
self.syncAthleteTable = [self.client syncTableWithName:#"Athlete"];
Here's how I add a record for the moment:
NSDictionary *newItem = #{#"firstname": firstname, #"lastname": lastname, #"laterality" : laterality};
[self.athletesService addItem:newItem completion:^{
NSLog(#"New athlete added");
}];
-(void)addItem:(NSDictionary *)item completion:(CompletionBlock)completion
{
// Insert the item into the Athlete table
[self.syncAthleteTable insert:item completion:^(NSDictionary *result, NSError *error)
{
[self logErrorIfNotNil:error];
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
The add works as expected and it is added in a UITableView as I have an NSFetchedResultsController listening on my Main Context.
Here's where the problem occurs. When I synchronize data with the server using this function:
-(void)syncData:(CompletionBlock)completion
{
// push all changes in the sync context, then pull new data
[self.client.syncContext pushWithCompletion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self pullData:completion];
}];
}
-(void)pullData:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
// Pulls data from the remote server into the local table.
// We're pulling all items and filtering in the view
// query ID is used for incremental sync
[self.syncAthleteTable pullWithQuery:query queryId:#"allAthletes" completion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self refreshDataOnSuccess:completion];
}];
}
- (void) refreshDataOnSuccess:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
[query readWithCompletion:^(MSQueryResult *results, NSError *error) {
[self logErrorIfNotNil:error];
NSLog(#"Data that pulled from local store: ");
for ( NSDictionary *dict in results.items ) {
NSLog(#"%# %#", [dict objectForKey:#"firstname"], [dict objectForKey:#"lastname"] );
}
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
After the synchronization the NSFetchedResultsChangeInsert is called a second time for the same record with a slightly different objectID. Here's an example of the first and second objectIDs:
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE9
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE11
I am stuck here.
Any help is highly appreciated. Thank you!
In the past, when I've seen this happen, its because the "id" field the client is sending was being changed or ignored by the server logic.
Locally the store finds the object in core data using that field, so a change to it could result in the client SDK thinking it needs to insert a new object and not update an existing one.
One easy way to confirm this, is by using the tableOperation:complete: method on the data delegate and comparing the "id" column between the item originally and that being returned by operation execute.
I am developing an app for ipad and i am using sqlite sentences (select, update, insert, delete).
I open (sqlite3_open) the database at the beginning and close (sqlite3_close) at the end of each sentence. But sometimes i´ve got the "database is locked" message.
I don´t know what can i do to solve this.
Thanks and sorry for this little information.
If I'm not mistaken , the problem with sqllite is that you can only access it once at a time.
If you have multiple threads, you can run in this situation. Example:
Run method1 (which accesses the database) on thread t1.
Run method2 (which accesses the database) on thread t2 after x seconds.
If method1 is not finished in those x seconds , both methods will access it at the same time.
And , as I said , I know that sqllite does not support this.
You should try to flag the usage of your database and if you want to access it but it is in use , try again after x seconds. Like this:
- (void) generalMethodThatUsesDatabses
{
if(databaseIsUsed)
{
[self performSelector:#selector(generalMethodThatUsesDatabses) withObject:nil afterDelay:5];
return;
}
databaseIsUsed = TRUE; //global bool variable
//your code here
databaseIsUsed = FALSE;
}
Hope this helps. Cheers!
You probably opened the database before using the same simulator.
To conclude all actions to a database and release all resources you always
have to use both (!) statements:
sqlite3_finalize(statement);
sqlite3_close(database);
A good way to solve this is to wrap this into a C++ library. This way, you can can create the library wrapper on the stack. This means that the moment that the function goes out of scope, you can close the connection in the destructor.
(note that I use reference counting for the Objective-C)
For instance:
NSArray* ScoreBoard::getAllScores()
{
ScoreBoard::ensureExistingTable();
//Stack allocated
SqliteWrapper sqlite("Scores.sqlite");
NSArray* result = sqlite.RunQuery("SELECT * FROM Scores ORDER BY ID DESC");
return result;
//after this, the sqlite destructor is called
}
It is very nice that the Objective-C compiler allows you to merge C++. It can be extremely useful.
Also
void SqliteWrapper::Close()
{
sqlite3_close(db);
}
as Vincent has pointed out, you have to finalize the statement. If you want keep the connection open, use finalize after each statement. Close out the connection the moment you are discarding the connection.
This method works for me.
it is use for three methed
1.isert
2.update
3. delete.
-(NSMutableArray *)resultSet
-(void)insertWithTitle:(NSString *)title Body:(NSString *)body
-(BOOL)updateAtIndex:(int)index Title:(NSString *)title Body:(NSString *)body
NSMutableArray *result = [[[NSMutableArray alloc] initWithCapacity:0] autorelease];
FMResultSet *rs = [db executeQuery:[self SQL:#"SELECT * FROM %#" inTable:TABLE_NAME]];
while ([rs next]) {
Record *tr = [[Record alloc] initWithIndex:[rs intForColumn:#"id"]
Title:[rs stringForColumn:#"title"]
Body:[rs stringForColumn:#"body"]];
[result addObject:tr];
[tr release];
}
[rs close];
2....
return result;
[db executeUpdate:[self SQL:#"INSERT INTO %# (title, body) VALUES (?,?)" inTable:TABLE_NAME], title, body];
if ([db hadError]) {
NSLog(#"Err %d: %#", [db lastErrorCode], [db lastErrorMessage]);
Delete record :
BOOL success = YES;
[db executeUpdate:[self SQL:#"DELETE FROM %# WHERE id = ?" inTable:TABLE_NAME], [NSNumber numberWithInt:index]];
if ([db hadError]) {
NSLog(#"Err %d: %#", [db lastErrorCode], [db lastErrorMessage]);
success = NO;
}
return success;
}