FMDatabase locked, best practice for usage within

2019-04-02 19:20发布

问题:

I have a sync method for an app I'm building storing data in SQLite locally, using the FMDatabase wrapper. When I put all the queries in one class everything works fine. However to maintain complexity I added some data controller classes for parts of the sync, but when I do so FMDatabase gives 'database locked' errors, both when I add a new connection in a data class and when I send along the initial database connection as a parameter.

Now I was thinking to add the database connection in a songleton, and was wondering if that is good practice and how I wrap FMDatabase in a singleton class. Any help on why the issue happens and what's the best way of working around this would be greatly appreciated!

回答1:

You should also consider using FMDatabaseQueue, and stick that in some shared area of your code. It'll be safe to use it on multiple threads as well.



回答2:

I don't agree with Tumtum's answer. Please read document from FMDB:

Using a single instance of FMDatabase from multiple threads at once is a bad idea. It has always been OK to make a FMDatabase object per thread. Just don't share a single instance across threads, and definitely not across multiple threads at the same time. Bad things will eventually happen and you'll eventually get something to crash, or maybe get an exception, or maybe meteorites will fall out of the sky and hit your Mac Pro. This would suck.

So don't instantiate a single FMDatabase object and use it across multiple threads.

Instead, use FMDatabaseQueue. Instantiate a single FMDatabaseQueue and use it across multiple threads. The FMDatabaseQueue object will synchronize and coordinate access across the multiple threads.



回答3:

I've implemented a singelton with an FMDatabase using the following code, which seems to have resolved the issue with the locked Database error. Not sure if this is good practice, but here's the code for everyone that needs a similar implementation. I used this tutorial on singeletons.

The singleton:

DatabaseController.h:

#import <Foundation/Foundation.h>
#import "FMDatabase.h"

@interface DatabaseController : NSObject

@property (strong, nonatomic) FMDatabase *db;

+ (id)sharedDatabase;
- (void)initDB;
- (FMDatabase *)getDB;

@end

DatabaseController.m:

#import "DatabaseController.h"

@implementation DatabaseController

static DatabaseController *sharedDatabase = nil;

// Get the shared instance and create it if necessary.
+ (DatabaseController *)sharedDatabase {
    if (sharedDatabase == nil) {
        sharedDatabase = [[super allocWithZone:NULL] init];
    }

    return sharedDatabase;
}

- (void)initDB {
    NSString *databaseName = @"MyDatabase.db";
    NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentDir = [documentPaths objectAtIndex:0];
    NSString *databasePath = [documentDir stringByAppendingPathComponent:databaseName];

    _db = [FMDatabase databaseWithPath:databasePath];

    if (![_db open])
        NSLog(@"Database problem");
}

// Return database instance
- (FMDatabase *)getDB {
    return _db;
}

// We can still have a regular init method, that will get called the first time the Singleton is used.
- (id)init {
    self = [super init];

    if (self) {
        // Work your initialising magic here as you normally would
    }

    return self;
}

// Equally, we don't want to generate multiple copies of the singleton.
- (id)copyWithZone:(NSZone *)zone {
    return self;
}

@end

Using the singelton

Then everywhere I need to acces the database I use:

DatabaseController* sharedDatabase = [DatabaseController sharedDatabase];
[sharedDatabase initDB]; //only the first time! In my case in my app delegate
FMDatabase *db = [sharedDatabase getDB];