ccgus / fmdb

A Cocoa / Objective-C wrapper around SQLite
Other
13.85k stars 2.77k forks source link

Where can I find a clear tutorial that shows how to run UPDATE SELECT AND DELETE concurrently #200

Open ilanl opened 11 years ago

ilanl commented 11 years ago

My app is saving the data at once on concurrent threads to the server. Each response is then parsed and there's a saving to db

I have moved all the previous SQLite C code to use the FMDB wrapper, but still FMDB is busy: The FMDatabase <FMDatabase: 0x17d4b950> is currently in use

I'm quite new to objective C , but an example of GCD using FMDatabaseQueue would be greatly helpful:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            [[Weather getInstance] reload]; //sync request/response and save to db on completion, see below (1)

            dispatch_sync(dispatch_get_main_queue(), ^{
                forecasts = [[DbAdapter getInstance] getForecasts]; //reload from db

---- (1) this is the save to db rountine performed on response completion Inside DbAdapter singleton:

-(void)saveForecastsToLocal:(ForecastResponse*) response { [_writeQueue addOperationWithBlock:^{

    [_writeQueueLock lock];

    [_queue inDatabase:^(FMDatabase *db) {
        [fmdb open];
        [fmdb executeUpdate:@"DELETE FROM Forecast"];
        for (Forecast* f in response.Forecasts)
        {
            [fmdb executeUpdate:@"INSERT INTO Forecast(Day,Hour,TempC,WaterTempC,WaveH,Weather,SwellSecs,WindDir,WindF) VALUES(?,?,?,?,?,?,?,?,?)",
             [NSString stringWithFormat:@"%@", f.Day],
             [NSString stringWithFormat:@"%@", f.Hour],
             [NSString stringWithFormat:@"%@", f.TempC],
             [NSString stringWithFormat:@"%@", f.WaterTempC],
             [NSString stringWithFormat:@"%@", f.WaveH],
             [NSString stringWithFormat:@"%@", f.Weather],
             [NSString stringWithFormat:@"%@", f.SwellSecs],
             [NSString stringWithFormat:@"%@", f.WindDir],
             [NSString stringWithFormat:@"%@", f.WindF],nil];
        } 
        [fmdb close];
    }];
    [_writeQueueLock unlock];

}];
robertmryan commented 11 years ago

In answer to your question of your title, you don't, technically, perform SELECT and UPDATE statements concurrently. You use FMDatabaseQueue to (a) ensure the database operations happen sequentially; but (b) to coordinate these database requests being issued from concurrent threads. For examples, see the FMDatabaseQueue documentation.

A couple of observations:

  1. What is that fmdb variable? The _queue passes you a db variable which you should be using for your database interaction. Your app should really have just the one FMDatabaseQueue instance, and no other instances of FMDatabase.
  2. As an aside, you experience a notable performance improvement when doing a series of UPDATE/INSERT statements if you use transactions. See inTransaction.
  3. I'm unclear as to the purpose of your _writeQueueLock. One of the advantages of using queues as it obviates the need for locks (if that queue is serial, i.e. a maxConcurrentOperationCount of 1, which your update queue should be). Clearly, if you have some other operations that you need to synchronize with your _writeQueue, you can use locks, but then again, you could just add those other operations to the _writeQueue, and you'd enjoy full synchronization without the use of locks.
  4. You're using stringWithFormat for all of those parameters to the executeUpdate, which is unnecessary if all of your properties are, themselves, objects (which I infer to be the case from your use of the @"%@" format string). It's better to just use the property objects directly. If those might be nil, then add a check to pass [NSNull null] if it's nil.
  5. You're adding nil to the end of the executeUpdate. That's not necessary (and possibly problematic).
  6. You might want to conform to Cocoa Guidelines for Cocoal: Naming Properties and Datatypes. Namely, generally properties should start with lowercase letter, only classes and data types should start with uppercase letter.

Thus, I might suggest:

//  DbAdapter.m

#import "DbAdapter.h"
#import "FMDatabaseQueue.h"
#import "FMDatabase.h"
#import "Forecast.h"
#import "ForecastResponse.h"

@interface DbAdapter ()

@property (nonatomic, strong) FMDatabaseQueue *queue;
@property (nonatomic, strong) NSOperationQueue *writeQueue;

@end

@implementation DbAdapter

+ (instancetype)sharedAdapter
{
    static id sharedMyManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedMyManager = [[self alloc] init];
    });
    return sharedMyManager;
}

- (id)init
{
    self = [super init];
    if (self) {
        _writeQueue = [[NSOperationQueue alloc] init];
        _writeQueue.maxConcurrentOperationCount = 1;

        NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
        NSString *path          = [documentsPath stringByAppendingPathComponent:@"weather.sqlite"];
        BOOL      fileExists    = [[NSFileManager defaultManager] fileExistsAtPath:path];

        // if you want to copy the database from the bundle if the file doesn't exist, do that here
        //
        // if (!fileExists) {
        //     // copy database from bundle
        // }

        _queue = [[FMDatabaseQueue alloc] initWithPath:path];

        // alternatively, if you want to build the database dynamically, do that after opening/creating the database
        //
        // if (!fileExists) {
        //     [self createForecastTable];
        // }
    }
    return self;
}

- (void)saveForecastsToLocal:(ForecastResponse*) response
{
    [_writeQueue addOperationWithBlock:^{
        [_queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
            BOOL success = [db executeUpdate:@"DELETE FROM Forecast"];
            NSAssert(success, @"delete failed: %@", [db lastErrorMessage]);

            for (Forecast* f in response.forecasts)
            {
                success = [db executeUpdate:@"INSERT INTO Forecast(Day,Hour,TempC,WaterTempC,WaveH,Weather,SwellSecs,WindDir,WindF) VALUES(?,?,?,?,?,?,?,?,?)",
                 f.day        ?: [NSNull null],
                 f.hour       ?: [NSNull null],
                 f.tempC      ?: [NSNull null],
                 f.waterTempC ?: [NSNull null],
                 f.waveH      ?: [NSNull null],
                 f.weather    ?: [NSNull null],
                 f.swellSecs  ?: [NSNull null],
                 f.windDir    ?: [NSNull null],
                 f.windF      ?: [NSNull null]];
                NSAssert(success, @"insert failed: %@", [db lastErrorMessage]);
            }
        }];
    }];
}

@end