// // TravelBug.m // Geopher Lite // // Created by Branden Russell on 8/16/08. // Copyright 2008 Stone Software and Branden Russell. All rights reserved. // #import "TravelBug.h" // Static variables for compiled SQL queries. This implementation choice is to be able to share a one time // compilation of each query across all instances of the class. Each time a query is used, variables may be bound // to it, it will be "stepped", and then reset for the next usage. When the application begins to terminate, // a class method will be invoked to "finalize" (delete) the compiled queries - this must happen before the database // can be closed. static sqlite3_stmt *insert_statement = nil; static sqlite3_stmt *init_statement = nil; static sqlite3_stmt *delete_statement = nil; static sqlite3_stmt *hydrate_statement = nil; static sqlite3_stmt *dehydrate_statement = nil; @implementation TravelBug @synthesize tableName; // Finalize (delete) all of the SQLite compiled queries. + (void)finalizeStatements { if (insert_statement) sqlite3_finalize(insert_statement); if (init_statement) sqlite3_finalize(init_statement); if (delete_statement) sqlite3_finalize(delete_statement); if (hydrate_statement) sqlite3_finalize(hydrate_statement); if (dehydrate_statement) sqlite3_finalize(dehydrate_statement); } // Creates the object with primary key and title is brought into memory. - (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db tableName:(NSString *)tn { if ( (self = [super init]) ) { primaryKey = pk; database = db; tableName = tn; // Compile the query for retrieving travel bug data. See insertIntoDatabase: for more detail. if (init_statement == nil) { // Note the '?' at the end of the query. This is a parameter which can be replaced by a bound variable. // This is a great way to optimize because frequently used queries can be compiled once, then with each // use new variable values can be bound to placeholders. strInit = [NSString stringWithFormat:@"SELECT name FROM %@ WHERE pk=?", self.tableName]; const char *sql = [strInit UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &init_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // For this query, we bind the primary key to the first (and only) placeholder in the statement. // Note that the parameters are numbered from 1, not from 0. sqlite3_bind_int(init_statement, 1, primaryKey); if (sqlite3_step(init_statement) == SQLITE_ROW) { self.name = [NSString stringWithUTF8String:(char *)sqlite3_column_text(init_statement, 0)]; } else { self.name = @"Unknown"; } // Reset the statement for future reuse. sqlite3_reset(init_statement); dirty = NO; } return self; } - (void)insertIntoDatabase:(sqlite3 *)db { database = db; // This query may be performed many times during the run of the application. As an optimization, a static // variable is used to store the SQLite compiled byte-code for the query, which is generated one time - the first // time the method is executed by any travel bug object. if (insert_statement == nil) { strInsertIntoDatabase = [NSString stringWithFormat:@"INSERT INTO %@ (name) VALUES(?)", self.tableName]; const char *sql = [strInsertIntoDatabase UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &insert_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } sqlite3_bind_text(insert_statement, 1, [name UTF8String], -1, SQLITE_TRANSIENT); int success = sqlite3_step(insert_statement); // Because we want to reuse the statement, we "reset" it instead of "finalizing" it. sqlite3_reset(insert_statement); if (success == SQLITE_ERROR) { NSAssert1(0, @"Error: failed to insert into the database with message '%s'.", sqlite3_errmsg(database)); } else { // SQLite provides a method which retrieves the value of the most recently auto-generated primary key sequence // in the database. To access this functionality, the table should have a column declared of type // "INTEGER PRIMARY KEY" primaryKey = sqlite3_last_insert_rowid(database); } // All data for the travel bug is already in memory, but has not be written to the database // Mark as hydrated to prevent empty/default values from overwriting what is in memory hydrated = YES; } - (void)deleteFromDatabase { // Compile the delete statement if needed. if (delete_statement == nil) { strDeleteFromDatabase = [NSString stringWithFormat:@"DELETE FROM %@ WHERE pk=?", self.tableName]; const char *sql = [strDeleteFromDatabase UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &delete_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // Bind the primary key variable. sqlite3_bind_int(delete_statement, 1, primaryKey); // Execute the query. int success = sqlite3_step(delete_statement); // Reset the statement for future use. sqlite3_reset(delete_statement); // Handle errors. if (success != SQLITE_DONE) { NSAssert1(0, @"Error: failed to delete from database with message '%s'.", sqlite3_errmsg(database)); } } // Brings the rest of the object data into memory. If already in memory, no action is taken (harmless no-op). - (void)hydrate { // Check if action is necessary. if (hydrated) return; // Compile the hydration statement, if needed. if (hydrate_statement == nil) { // TODO: Missing time after date. Is it needed or does date track time? strHydrate = [NSString stringWithFormat:@"SELECT id, ref FROM %@ WHERE pk=?", self.tableName]; const char *sql = [strHydrate UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &hydrate_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // Bind the primary key variable. sqlite3_bind_int(hydrate_statement, 1, primaryKey); // Execute the query. int success =sqlite3_step(hydrate_statement); if (success == SQLITE_ROW) { int idx = 0; self.ident = sqlite3_column_int(hydrate_statement, idx++); char *str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.ref = (str) ? [NSString stringWithUTF8String:str] : @""; } else { // The query did not return self.ident = 0; self.ref = @"Unknown"; } // Reset the query for the next use. sqlite3_reset(hydrate_statement); // Update object state with respect to hydration. hydrated = YES; } // Flushes all but the primary key and name out to the database. - (void)dehydrate { if (dirty) { // Write any changes to the database. // First, if needed, compile the dehydrate query. if (dehydrate_statement == nil) { // TODO: Missing time after date. Is it needed or does date track time? strDehydrate = [NSString stringWithFormat:@"UPDATE %@ SET name=?, id=?, ref=? WHERE pk=?", self.tableName]; const char *sql = [strDehydrate UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &dehydrate_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } int idx = 1; sqlite3_bind_text(dehydrate_statement, idx++, [name UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_int(dehydrate_statement, idx++, ident); sqlite3_bind_text(dehydrate_statement, idx++, [ref UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_int(dehydrate_statement, idx++, primaryKey); // Execute the query. int success = sqlite3_step(dehydrate_statement); // Reset the query for the next use. sqlite3_reset(dehydrate_statement); // Handle errors. if (success != SQLITE_DONE) { NSAssert1(0, @"Error: failed to dehydrate with message '%s'.", sqlite3_errmsg(database)); } // Update the object state with respect to unwritten changes. dirty = NO; } // Release member variables to reclaim memory. Set to nil to avoid over-releasing them // if dehydrate is called multiple times. [ref release]; ref = nil; [data release]; data = nil; // Update the object state with respect to hydration. hydrated = NO; } - (void)dealloc { [name release]; [super dealloc]; } #pragma mark Properties // Accessors implemented below. All the "get" accessors simply return the value directly, with no additional // logic or steps for synchronization. The "set" accessors attempt to verify that the new value is definitely // different from the old value, to minimize the amount of work done. Any "set" which actually results in changing // data will mark the object as "dirty" - i.e., possessing data that has not been written to the database. // All the "set" accessors copy data, rather than retain it. This is common for value objects - strings, numbers, // dates, data buffers, etc. This ensures that subsequent changes to either the original or the copy don't violate // the encapsulation of the owning object. - (NSInteger)primaryKey { return primaryKey; } - (NSString *)name { return name; } - (void)setName:(NSString *)aString { if ((!name && !aString) || (name && aString && [name isEqualToString:aString])) return; dirty = YES; [name release]; name = [aString copy]; } - (int)ident { return ident; } - (void)setIdent:(int)val { if ((!ident && !val) || (ident && val && ident == val)) return; dirty = YES; ident = val; } - (NSString *)ref { return ref; } - (void)setRef:(NSString *)aString { if ((!ref && !aString) || (ref && aString && [ref isEqualToString:aString])) return; dirty = YES; [ref release]; ref = [aString copy]; } @end