Browse files

Took out FMDatabasePool from the readme, moved those docs to its head…

…er. I really want to discourage its use if possible. Took away -database from FMDatabaseQueue since I've already seen it misused once, and if you really need to get at it, you can do so with the inDatabase: methods.
  • Loading branch information...
1 parent 626fa9e commit 58c3e7005d890a6cb034c58ab233605bcf18ccb3 @ccgus committed Feb 10, 2012
Showing with 262 additions and 178 deletions.
  1. +29 −67 README.markdown
  2. +0 −2 fmdb.xcodeproj/project.pbxproj
  3. +91 −0 src/FMDatabasePool.h
  4. +0 −7 src/FMDatabaseQueue.h
  5. +2 −7 src/FMDatabaseQueue.m
  6. +140 −95 src/fmdb.m
View
96 README.markdown
@@ -9,11 +9,15 @@ http://www.sqlite.org/faq.html
Since FMDB is built on top of SQLite, you're going to want to read this page top to bottom at least once. And while you're there, make sure to bookmark the SQLite Documentation page: http://www.sqlite.org/docs.html
+## Automatic Reference Counting (ARC) or Manual Memory Management?
+You can use either style in your Cocoa project. FMDB Will figure out which you are using at compile time and do the right thing.
+
## Usage
-There are two main classes in FMDB:
+There are three main classes in FMDB:
1. `FMDatabase` - Represents a single SQLite database. Used for executing SQL statements.
2. `FMResultSet` - Represents the results of executing a query on an `FMDatabase`.
+3. `FMDatabaseQueue` - If you're wanting to perform queries and updates on multiple threads, you'll want to use this class. It's described in the "Thread Safety" section below.
### Database Creation
An `FMDatabase` is created with a path to a SQLite database file. This path can be one of these three:
@@ -127,90 +131,48 @@ Alternatively, you can use the `-execute*WithFormat:` variant to use `NSString`-
Internally, the `-execute*WithFormat:` methods are properly boxing things for you. The following percent modifiers are recognized: `%@`, `%c`, `%s`, `%d`, `%D`, `%i`, `%u`, `%U`, `%hi`, `%hu`, `%qi`, `%qu`, `%f`, `%g`, `%ld`, `%lu`, `%lld`, and `%llu`. Using a modifier other than those will have unpredictable results. If, for some reason, you need the `%` character to appear in your SQL statement, you should use `%%`.
-<h2 id="threads">Using FMDatabasePool and Thread Safety.</h2>
-
-**Note:** This is preliminary and subject to change. Consider it experimental, but feel free to try it out and give me feedback.
+<h2 id="threads">Using FMDatabaseQueue and Thread Safety.</h2>
-Using a single instance of FMDatabase from multiple threads at once is not supported. The Fine Print: It's 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*.
+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. It's your friend and it's here to help. Here's how to use it:
+First, make your queue.
-Instead, use FMDatabasePool. It's your friend and it's here to help. Here's how to use it:
-
-
-First, make your pool.
-
- FMDatabasePool *pool = [FMDatabasePool databasePoolWithPath:aPath];
-
-If you just have a single statement- use it like so:
-
- [[pool db] executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:42]];
-
-The pool's db method will return an instance of FMDatabase that knows it is in a pool. After it is done with the update, it will place itself back into the pool.
-
-Making a query is similar:
-
- FMResultSet *rs = [[pool db] executeQuery:@"SELECT * FROM myTable"];
- while ([rs next]) {
- //retrieve values for each record
- }
-
-When the result set is exhausted or [rs close] is called, the result set will tell the database it was created from to put itself back into the pool for use later on.
+ FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
-If you'd rather use multiple queries without having to call [pool db] each time, you can grab a database instance, tell it to stay out of the pool, and then tell it to go back in the pool when you're done:
+Then use it like so:
- FMDatabase *db = [[pool db] popFromPool];
-
- [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
- [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
- [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
-
- // put the database back in the pool.
- [db pushToPool];
-
-Alternatively, you can use this nifty block based approach:
-
- [dbPool useDatabase: ^(FMDatabase *aDb) {
+ [queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
- }];
-
-And it will do the right thing.
-
-Starting a transaction will keep the db from going back into the pool automatically:
-
- FMDatabase *db = [pool db];
- [db beginTransaction];
-
- [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
- [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
- [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
-
- [db commit]; // or a rollback here would work as well.
-
+
+ FMResultSet *rs = [db executeQuery:@"select * from foo"];
+ while ([rs next]) {
+
+ }
+ }];
-There is also a block based transaction approach:
+An easy way to wrap things up in a transaction can be done like this:
- [dbPool inTransaction:^(FMDatabase *db, BOOL *rollback) {
- [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
+ [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
+
+ if (whoopsSomethingWrongHappened) {
+ *rollback = YES;
+ return;
+ }
+ // etc…
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
}];
-
-If you check out a database, but never execute a statement or query, **you need to put it back in the pool yourself**.
-
- FMDatabase *db = [pool db];
- // lala, don't do anything with the database
-
- // oh look, I BETTER PUT THE DB BACK IN THE POOL OR ELSE IT IS GOING TO LEAK:
- [db pushToPool];
-
-Do you have feedback on this pooled approach? Email Gus! The plan is to eventually move this class to the master branch once it's been tweaked and baked a while.
+FMDatabaseQueue will make a serialized GCD queue in the background and execute the blocks you pass to the GCD queue. This means if you call your FMDatabaseQueue's methods from multiple threads at the same time GDC will execute them in the order they are received. This means queries and updates won't step on each other's toes, and every one is happy.
## History
View
2 fmdb.xcodeproj/project.pbxproj
@@ -273,7 +273,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD_64_BIT)";
- CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_OBJCPP_ARC_ABI = YES;
COPY_PHASE_STRIP = NO;
GCC_DYNAMIC_NO_PIC = NO;
@@ -290,7 +289,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD_32_64_BIT)";
- CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_OBJCPP_ARC_ABI = YES;
GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
GCC_MODEL_TUNING = G5;
View
91 src/FMDatabasePool.h
@@ -9,6 +9,97 @@
#import <Foundation/Foundation.h>
#import "sqlite3.h"
+/*
+
+ ***README OR SUFFER***
+Before using FMDatabasePool, please consider using FMDatabaseQueue instead.
+
+I'm also not 100% sold on this interface. So if you use FMDatabasePool, things like
+[[pool db] popFromPool] might go away.
+
+If you really really really know what you're doing and FMDatabasePool is what
+you really really need, OK you can use it. But just be careful not to deadlock!
+
+First, make your pool.
+
+ FMDatabasePool *pool = [FMDatabasePool databasePoolWithPath:aPath];
+
+If you just have a single statement- use it like so:
+
+ [[pool db] executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:42]];
+
+The pool's db method will return an instance of FMDatabase that knows it is in a pool. After it is done with the update, it will place itself back into the pool.
+
+Making a query is similar:
+
+ FMResultSet *rs = [[pool db] executeQuery:@"SELECT * FROM myTable"];
+ while ([rs next]) {
+ //retrieve values for each record
+ }
+
+When the result set is exhausted or [rs close] is called, the result set will tell the database it was created from to put itself back into the pool for use later on.
+
+If you'd rather use multiple queries without having to call [pool db] each time, you can grab a database instance, tell it to stay out of the pool, and then tell it to go back in the pool when you're done:
+
+ FMDatabase *db = [[pool db] popFromPool];
+
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
+
+ // put the database back in the pool.
+ [db pushToPool];
+
+Alternatively, you can use this nifty block based approach:
+
+ [dbPool useDatabase: ^(FMDatabase *aDb) {
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
+ }];
+
+And it will do the right thing.
+
+Starting a transaction will keep the db from going back into the pool automatically:
+
+ FMDatabase *db = [pool db];
+ [db beginTransaction];
+
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
+
+ [db commit]; // or a rollback here would work as well.
+
+
+There is also a block based transaction approach:
+
+ [dbPool inTransaction:^(FMDatabase *db, BOOL *rollback) {
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
+ [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
+ }];
+
+
+
+If you check out a database, but never execute a statement or query, **you need to put it back in the pool yourself**.
+
+ FMDatabase *db = [pool db];
+ // lala, don't do anything with the database
+
+ // oh look, I BETTER PUT THE DB BACK IN THE POOL OR ELSE IT IS GOING TO LEAK:
+ [db pushToPool];
+
+*/
+
+
+
+
+
+
+
+
+
@class FMDatabase;
@interface FMDatabasePool : NSObject {
View
7 src/FMDatabaseQueue.h
@@ -28,13 +28,6 @@
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
- (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;
-/* the database is exposed so you can do things like turning on cached statements
- or setup encryption or whatever. Don't grab the database and start making
- queries with it. Use the block based access above instead (otherwise you're
- going to deadlock and cause baby unicorns to cry.
- */
-- (FMDatabase*)database;
-
#if SQLITE_VERSION_NUMBER >= 3007000
// NOTE: you can not nest these, since calling it will pull another database out of the pool and you'll get a deadlock.
// If you need to nest, use FMDatabase's startSavePointWithName:error: instead.
View
9 src/FMDatabaseQueue.m
@@ -15,14 +15,8 @@
FMDatabaseQueue is released on another thread and we're in the middle of doing
something in dispatch_sync
- Another Note:
- Some day, I think it would be awesome to add support for creating a queue with
- DISPATCH_QUEUE_CONCURRENT, and then make dispatch_barrier_async the default
- way to execute things- but give the option to use dispatch_sync for cases where
- we know we're going to make read only operations for the database.
-
*/
-
+
@implementation FMDatabaseQueue
@synthesize path = _path;
@@ -114,6 +108,7 @@ - (void)inDatabase:(void (^)(FMDatabase *db))block {
FMDBRelease(self);
}
+
- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
FMDBRetain(self);
dispatch_sync(_queue, ^() {
View
235 src/fmdb.m
@@ -6,9 +6,11 @@
#define FMDBQuickCheck(SomeBool) { if (!(SomeBool)) { NSLog(@"Failure on line %d", __LINE__); abort(); } }
+void testPool(NSString *dbPath);
+
int main (int argc, const char * argv[]) {
- @autoreleasepool {
+@autoreleasepool {
NSString *dbPath = @"/tmp/tmp.db";
@@ -587,13 +589,6 @@ int main (int argc, const char * argv[]) {
}
-
-
-
-
-
-
-
// just for fun.
rs = [db executeQuery:@"PRAGMA database_list"];
while ([rs next]) {
@@ -617,15 +612,148 @@ int main (int argc, const char * argv[]) {
[db close];
+ testPool(dbPath);
+
+
+
+ FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
+
+ FMDBQuickCheck(queue);
+
+ {
+ [queue inDatabase:^(FMDatabase *db) {
+
+
+ int count = 0;
+ FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"];
+ while ([rsl next]) {
+ count++;
+ }
+
+ FMDBQuickCheck(count == 2);
+
+ count = 0;
+ rsl = [db executeQuery:@"select * from likefoo where foo like ?", @"h%"];
+ while ([rsl next]) {
+ count++;
+ }
+
+ FMDBQuickCheck(count == 2);
+ }];
+
+ }
+
+
+ {
+ // You should see pairs of numbers show up in stdout for this stuff:
+ int ops = 16;
+
+ dispatch_queue_t dqueue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH);
+
+ dispatch_apply(ops, dqueue, ^(size_t nby) {
+
+ // just mix things up a bit for demonstration purposes.
+ if (nby % 2 == 1) {
+ [NSThread sleepForTimeInterval:.1];
+
+ [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
+ NSLog(@"Starting query %ld", nby);
+
+ FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"];
+ while ([rsl next]) {
+ ;// whatever.
+ }
+
+ NSLog(@"Ending query %ld", nby);
+ }];
+
+ }
+
+ if (nby % 3 == 1) {
+ [NSThread sleepForTimeInterval:.1];
+ }
+
+ [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
+ NSLog(@"Starting update %ld", nby);
+ [db executeUpdate:@"insert into likefoo values ('1')"];
+ [db executeUpdate:@"insert into likefoo values ('2')"];
+ [db executeUpdate:@"insert into likefoo values ('3')"];
+ NSLog(@"Ending update %ld", nby);
+ }];
+ });
+
+ [queue close];
+
+ [queue inDatabase:^(FMDatabase *db) {
+ FMDBQuickCheck([db executeUpdate:@"insert into likefoo values ('1')"]);
+ }];
+ }
+
+
+
+ {
+ [queue inDatabase:^(FMDatabase *db) {
+ [db executeUpdate:@"create table transtest (a integer)"];
+ FMDBQuickCheck([db executeUpdate:@"insert into transtest values (1)"]);
+ FMDBQuickCheck([db executeUpdate:@"insert into transtest values (2)"]);
+
+ int rowCount = 0;
+ FMResultSet *rs = [db executeQuery:@"select * from transtest"];
+ while ([rs next]) {
+ rowCount++;
+ }
+
+ FMDBQuickCheck(rowCount == 2);
+ }];
+
+
+
+ [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
+ FMDBQuickCheck([db executeUpdate:@"insert into transtest values (3)"]);
+
+ if (YES) {
+ // uh oh!, something went wrong (not really, this is just a test
+ *rollback = YES;
+ return;
+ }
+
+ FMDBQuickCheck([db executeUpdate:@"insert into transtest values (4)"]);
+ }];
+
+ [queue inDatabase:^(FMDatabase *db) {
+
+ int rowCount = 0;
+ FMResultSet *rs = [db executeQuery:@"select * from transtest"];
+ while ([rs next]) {
+ rowCount++;
+ }
+
+ NSLog(@"after rollback, rowCount is %d (should be 2)", rowCount);
+
+ FMDBQuickCheck(rowCount == 2);
+ }];
+ }
+
+ NSLog(@"That was version %@ of sqlite", [FMDatabase sqliteLibVersion]);
+
+}// this is the end of our @autorelease pool.
+ return 0;
+}
+
+/*
+ Test the various FMDatabasePool things.
+*/
+
+void testPool(NSString *dbPath) {
FMDatabasePool *dbPool = [FMDatabasePool databasePoolWithPath:dbPath];
FMDBQuickCheck([dbPool countOfOpenDatabases] == 0);
- db = [dbPool db];
+ FMDatabase *db = [dbPool db];
FMDBQuickCheck([dbPool countOfOpenDatabases] == 1);
@@ -821,7 +949,7 @@ int main (int argc, const char * argv[]) {
FMDBQuickCheck([dbPool countOfOpenDatabases] == 1);
- err = [dbPool inSavePoint:^(FMDatabase *db, BOOL *rollback) {
+ NSError *err = [dbPool inSavePoint:^(FMDatabase *db, BOOL *rollback) {
FMDBQuickCheck(![adb hadError]);
[adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1006]];
}];
@@ -855,7 +983,7 @@ int main (int argc, const char * argv[]) {
{
-
+
err = [dbPool inSavePoint:^(FMDatabase *adb, BOOL *rollback) {
FMDBQuickCheck(![adb hadError]);
[adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1009]];
@@ -898,93 +1026,10 @@ int main (int argc, const char * argv[]) {
}
FMDBQuickCheck(count == 2);
-
- }];
-
-
- }
-
- FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
-
- FMDBQuickCheck(queue);
-
- {
-
- [queue inDatabase:^(FMDatabase *db) {
-
- int count = 0;
- FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"];
- while ([rsl next]) {
- count++;
- }
-
- FMDBQuickCheck(count == 2);
-
- count = 0;
- rsl = [db executeQuery:@"select * from likefoo where foo like ?", @"h%"];
- while ([rsl next]) {
- count++;
- }
-
- FMDBQuickCheck(count == 2);
}];
- }
-
-
- {
-
- int ops = 16;
-
- dispatch_queue_t dqueue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH);
-
- dispatch_apply(ops, dqueue, ^(size_t nby) {
-
- // just mix things up a bit for demonstration purposes.
- if (nby % 2 == 1) {
- [NSThread sleepForTimeInterval:.1];
-
-
- [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
- NSLog(@"Starting query %ld", nby);
-
- FMResultSet *rsl = [db executeQuery:@"select * from likefoo where foo like 'h%'"];
- while ([rsl next]) {
- ;// whatever.
- }
-
- NSLog(@"Ending query %ld", nby);
- }];
-
- }
-
- if (nby % 3 == 1) {
- [NSThread sleepForTimeInterval:.1];
- }
-
- [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
- NSLog(@"Starting update %ld", nby);
- [db executeUpdate:@"insert into likefoo values ('1')"];
- [db executeUpdate:@"insert into likefoo values ('2')"];
- [db executeUpdate:@"insert into likefoo values ('3')"];
- NSLog(@"Ending update %ld", nby);
- }];
- });
-
- [queue close];
-
- [queue inDatabase:^(FMDatabase *db) {
- FMDBQuickCheck([db executeUpdate:@"insert into likefoo values ('1')"]);
- }];
- }
-
-
-
- NSLog(@"That was version %@ of sqlite", [FMDatabase sqliteLibVersion]);
}
-
-
- return 0;
+
}

0 comments on commit 58c3e70

Please sign in to comment.