sqlite iphone best practice for reading data - ios

I'm trying to make an app, that reads from an SQLite3 database. I plan to pre-load data during development, so the app does not need to modify anything in the database, only read from it, make queries, etc.
What is the best practice for solely reading data? Should I open the database, read the data, and close it, with each query? The app will be making many small queries and a few large ones. Is it better to have the database open for the duration of the app, or open/close it with each fetch?

Reading:
1. For queries, it's important to re-use compiled statements.
2. Make sure you use parameters so you can re-use those compiled queries
When you call sqlite3_prepare_v2, it compiles the statement and gives you a reference to the statement back. Find a way to save that off and re-use it. See the code below for *statement. You pass &statement into prepare.
Also, note the use of ? for parameters. If you're going to re-use the statement, it's important to call sqlite3_reset() againt the statement, rebind the inputs from the program (parameters) and execute it again.
sqlite3_stmt *statement;
NSString *querySQL = #"update contacts set name=?,address=?,phone=? where id=?";
NSLog(#"query: %#", querySQL);
const char *query_stmt = [querySQL UTF8String];
// preparing a query compiles the query so it can be re-used.
// find a way to save off the *statement so you can re-use it.
sqlite3_prepare_v2(_contactDb, query_stmt, -1, &statement, NULL);
// use sqlite3_bind_xxx functions to bind in order values to the params
sqlite3_bind_text(statement, 1, [[contact name] UTF8String], -1, SQLITE_STATIC);
sqlite3_bind_text(statement, 2, [[contact address] UTF8String], -1, SQLITE_STATIC);
sqlite3_bind_text(statement, 3, [[contact phone] UTF8String], -1, SQLITE_STATIC);
sqlite3_bind_int64(statement, 4, [[contact id] longLongValue]);
Always check the return codes! and log or handle the errors.
rc = sqlite3_step(stmt);
switch (rc)
{
case SQLITE_ROW:
// ...
break;
case SQLITE_OK:
case SQLITE_DONE:
break;
default:
// ....
}
return NO;
}
if you get an error, log or get the eror message to provide more info:
- (NSString*)errorMessage
{
return [NSString stringWithCString:sqlite3_errmsg(_sqlite3) encoding:NSUTF8StringEncoding];
}

Use sqlite_open_v2 with the SQLITE_OPEN_READONLY flag. For example, I use the following method to open a database for reading only.
// Open for reading only.
- (int) openDatabaseAtPath:(NSString *) path
{
if (database != nil)
{
sqlite3_close(self.database);
[self setDatabase:nil];
}
int errorCode = SQLITE_OK;
errorCode = sqlite3_open_v2([path UTF8String],
&database,
SQLITE_OPEN_READONLY,
NULL);
return errorCode;
}

Unless you copy the database to the Documents directory, you work with the DB from the resource dir, and that one is readonly.

When you open the database using sqlite_open_v2 and the SQLITE_OPEN_READONLY flag, SQLite opens the file itself in read-only mode, so even if your application, due to a bug, corrupts memory belonging to SQLite, the database will stay untouched.
With this in mind, I'd keep the database open until the application quits. (You may wish to close it if you receive a low-memory notification and reopen it on demand, but opening and closing it for every query would be wasteful.)

As per you question you want to read data from database. So following are the answer of you questions.
No need to open db each time when you fire the query. Just one it one time. It is better if you create singleton class and open db in it for first time when it initiate.
Use following code method which will work for all select queries which has conditional select, group by etc.
I) it takes Output/result column names as input array ii ) TableName iii) Where condition iv)OrderBy clause v)group By clause
- (NSMutableArray *)runSelecteQueryForColumns: (NSArray *)p_columns ontableName: (NSString *)p_tableName withWhereClause: (NSString *)p_whereClause withOrderByClause: (NSString *)p_orederByCalause withGroupByClause: (NSString *)p_groupByClause
{
NSMutableArray *l_resultArray = [[NSMutableArray alloc] init];
if(!self.m_database)
{
if(![self openDatabase])
{
sqlite3_close(self.m_database);
//NSLog(#"error in select : DB creating : %#",p_whereClause);
return nil;
}
}
NSMutableString *l_simpleQuery =[[NSMutableString alloc] initWithString:#"Select"] ;
if(p_columns)
{
for(int l_row = 0 ; l_row < [p_columns count] ; l_row++)
{
if(l_row != [p_columns count]-1)
{
[l_simpleQuery appendString:[NSString stringWithFormat:#" %#,", [p_columns objectAtIndex:l_row]]];
}
else
{
[l_simpleQuery appendString:[NSString stringWithFormat:#" %#", [p_columns objectAtIndex:l_row]]];
}
}
}
else
{
[l_simpleQuery appendString:#" *"];
}
[l_simpleQuery appendString:[NSString stringWithFormat:#" From %#",p_tableName]];
if(p_whereClause)
{
[l_simpleQuery appendString:[NSString stringWithFormat:#" %#",p_whereClause]];
}
if(p_groupByCaluase)
{
[l_simpleQuery appendString:[NSString stringWithFormat:#" %#",p_groupByCaluase]];
}
if(p_orederByCalause)
{
[l_simpleQuery appendString:[NSString stringWithFormat:#" %#",p_orederByCalause]];
}
//NSLog(#"Select Query: - %#",l_simpleQuery);
const char *l_query_stmt = [l_simpleQuery UTF8String];
sqlite3_stmt *l_statement = nil;
int i = sqlite3_prepare_v2(self.m_database,
l_query_stmt, -1, &l_statement, NULL);
if (i == SQLITE_OK)
{
while(sqlite3_step(l_statement) == SQLITE_ROW)
{
[l_resultArray addObject:[self createDictionary:l_statement]];
}
sqlite3_finalize(l_statement);
}
else
{
sqlite3_finalize(l_statement);
//sqlite3_close(l_database);
DDLogError(#"%# - error in SQL :%#",THIS_FILE,l_simpleQuery);
return nil;
}
//NSLog(#"RESULT %#",l_resultArray);
return l_resultArray;
}

Related

Sqlite - while (sqlite3_step(statement) == SQLITE_ROW) is not executing

I am trying to get the highest value in the Points column in my DB, however the sqlite_step statement never get executed.
This is the method with this in it.
+(NSMutableArray*)getMax {
[self databaseInit];
highScore *newMax = [[highScore alloc]init];
NSMutableArray *maxPointsArray = [[NSMutableArray alloc]init];
if (sqlite3_open(dbpath,&peopleDB)==SQLITE_OK)
{
NSString *selectMaxSQL = [NSString stringWithFormat:#"SELECT MAX POINTS FROM PEOPLE"];
if(sqlite3_prepare_v2(peopleDB, [selectMaxSQL UTF8String],-1,&statement, NULL)==SQLITE_OK){
while (sqlite3_step(statement) == SQLITE_ROW){
newMax.max = sqlite3_column_int(statement, 0);
}
[maxPointsArray addObject:newMax];
}
}
sqlite3_finalize(statement);
sqlite3_close(peopleDB);
return maxPointsArray;
}
If it makes a difference the databaseInit method is -
+(void)databaseInit {
//get documents directory
dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
docsDir = dirPaths [0];
//create path to database file
databasePath = [[NSString alloc]initWithString:[docsDir stringByAppendingPathComponent:#"people.db"]];
dbpath = [databasePath UTF8String];
}
Any help with this would be hugely appreciated, let me know if any more code is needed.
It is not executed because your query is wrong.
Assuming that points is a field of your people table, the query should be:
SELECT MAX(points) FROM people
You have the immediate answer, but the long term solution is to log sqlite3_errmsg if any SQLite functions fail. The way your code is written, you have no way of knowing which of the calls failed and why.
Thus, you may want:
+(NSMutableArray*)getMax {
int rc;
[self databaseInit];
highScore *newMax = [[highScore alloc]init];
NSMutableArray *maxPointsArray = [[NSMutableArray alloc]init];
if ((rc = sqlite3_open(dbpath, &peopleDB)) == SQLITE_OK) {
const char *selectMaxSQL = "SELECT MAX(POINTS) FROM PEOPLE";
if(sqlite3_prepare_v2(peopleDB, selectMaxSQL, -1, &statement, NULL)==SQLITE_OK){
if ((rc = sqlite3_step(statement)) == SQLITE_ROW){
newMax.max = sqlite3_column_int(statement, 0);
[maxPointsArray addObject:newMax];
} else {
if (rc == SQLITE_DONE) {
NSLog(#"No record returned");
} else {
NSLog(#"Step failed %s (%d)", sqlite3_errmsg(peopleDB), rc);
}
}
sqlite3_finalize(statement);
} else {
NSLog(#"Prepare failed %s (%d)", sqlite3_errmsg(peopleDB), rc);
}
} else {
NSLog(#"Open failed %d", rc);
}
sqlite3_close(peopleDB);
return maxPointsArray;
}
If you had done this with your original SQL, it would have reported precisely where in the SQL it encountered the error. Furthermore, if you have any problems in the future, this sort of defensive programming will help diagnose problems more quickly.
It's also worth noting that it's generally recommended not to open and close the database with every database call, but rather to open the database only once. It's more efficient and people have reported problems when constantly opening and closing databases.

How to retrieve value from SQLite database

I'm trying to get data from my DB but I have some problem.
Here is my code:
NSString *action=[[NSString alloc]init];
NSString *queryStatement = [NSString stringWithFormat:#"SELECT ACTIONNAME FROM ACTIONS WHERE ACTIONSYMBOL = '%#'", symbol];
// Prepare the query for execution
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(database, [queryStatement UTF8String], -1, &statement, NULL) == SQLITE_OK)
{
// Create a new address from the found row
while (sqlite3_step(statement) == SQLITE_ROW) {
action = [NSString stringWithUTF8String:(char*)sqlite3_column_text(statement, 1)]; // fails on this line
}
sqlite3_finalize(statement);
return action;
}
(the parameter symbol come from outside)
When I run this, it fails at the line with the call to stringWithUTF8String with sqlite3_column_text results.
You probably want to collect the results in an NSMutableArray:
NSMutableArray *action = [[NSMutableArray alloc] init];
...
while (sqlite3_step(statement) == SQLITE_ROW) {
[action addObject:[NSString stringWithUTF8String:(char*)sqlite3_column_text(statement, 0)]];
}
...
You can then see what was collected later:
for (NSString *s in action) {
NSLog(#"%#", s);
}
EDIT As pointed out in #Rob's answer, the first column is 0, not 1.
In your call to sqlite3_column_text, you're using the index 1, but it takes a zero-based index. Use 0 instead of 1. See the SQLite sqlite_column_XXX documentation, which says:
The leftmost column of the result set has the index 0.
By the way, since stringWithUTF8String throws an exception if you pass it a NULL value, it's often safer to check the result if sqlite3_column_text is not NULL before proceeding, and handle the error gracefully otherwise. Also, you might want to check for sqlite3_step and sqlite3_prepare_v2 errors, like so:
NSString *queryStatement = [NSString stringWithFormat:#"SELECT ACTIONNAME FROM ACTIONS WHERE ACTIONSYMBOL = '%#'", symbol]; // note, it can be dangerous to use `stringWithFormat` to build SQL; better to use `?` placeholders in your SQL and then use `sqlite3_bind_text` to bind the `symbol` value with the `?` placeholder
if (sqlite3_prepare_v2(database, [queryStatement UTF8String], -1, &statement, NULL) == SQLITE_OK)
{
int rc;
while ((rc = sqlite3_step(statement)) == SQLITE_ROW) {
const unsigned char *value = sqlite3_column_text(statement, 0); // use zero
if (value) {
NSString *action = [NSString stringWithUTF8String:(const char *)value];
// now do whatever you want with `action`, e.g. add it to an array or what
} else {
// handle the error (or NULL value) gracefully here
}
// make sure to check for errors in `sqlite3_step`
if (rc != SQLITE_DONE)
{
NSLog(#"%s: sqlite3_step failed: %s", __FUNCTION__, sqlite3_errmsg(database));
}
}
}
else
{
NSLog(#"%s: sqlite3_prepare_v2 failed: %s", __FUNCTION__, sqlite3_errmsg(database));
}
Incidentally, as the above illustrates, to correctly perform all of the error checking is a little cumbersome. This is where FMDB can be useful, simplifying the above to (where db is an FMDatabase object that has been opened):
FMResultSet *rs = [db executeQuery:#"SELECT ACTIONNAME FROM ACTIONS WHERE ACTIONSYMBOL = ?", symbol];
if (!rs) {
NSLog(#"%s: executeQuery failed: %#", __FUNCTION__, [db lastErrorMessage]);
return;
}
while ([rs next]) {
NSString *action = [rs stringForColumnIndex:0];
// do whatever you want with `action` here
}
[rs close];
And if you use ? placeholders (rather than using stringWithFormat to build your SQL, which is dangerous) the benefits of using FMDB are even more compelling.

Sqlite database cannot read column

I have a DBManager wich is fetching data from database (sqlite file). All other queries are fine, but this one seems to be somehow not working
-(NSArray *)readCountries{
NSLog(#"[DBManager] readCountries");
NSMutableArray *countriesArray = [[NSMutableArray alloc] init];
//open db from users filesystem
if (sqlite3_open([dbPath UTF8String], &database) == SQLITE_OK) {
const char* sql = "SELECT DISTINCT country FROM aed ORDER BY rowid";
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK) {
//loop through results
while (sqlite3_step(statement) == SQLITE_ROW) {
//read data from record
NSString *_country;
char* tmpCountry = (char*)sqlite3_column_text(statement, 1);
NSLog(#"tmpCountry = %#", [NSString stringWithUTF8String:tmpCountry]);
if (tmpCountry != NULL) {
_country = [NSString stringWithUTF8String:tmpCountry];
}else{
_country = #"n/a";
}
NSLog(#"country = %#", _country);
[countriesArray addObject:_country];
}
}
//finalize statement
sqlite3_finalize(statement);
}
//close database
sqlite3_close(database);
NSLog(#"[DBManager] countriesArray has %d objects", [countriesArray count]);
return (NSArray*)countriesArray;
}
All I get from logs, that my array has 5 objects, which is fine - but it souldn't be only "n/a"... any idea? Other queries are good, they mostly use sqlite3_column_text so I don't get it, why it's not working here - maybe a fresh eye will help.
This is a confusing inconsistency with the sqlite C-api. When using the sqlite3_column_xxx functions, the column index is 0-based. But with the sqlite3_bind_xxx functions, the column index is 1-based.
Change this:
char* tmpCountry = (char*)sqlite3_column_text(statement, 1);
to:
char* tmpCountry = (char*)sqlite3_column_text(statement, 0);
BTW - you should add else statements to your sqlite3_open and sqlite3_prepare calls. If they fail you can log the error using the sqlite3_errmsg function.

Not able to query my sqlite database

Below is my code, I'm attempting to retrieve data from an sqlite database with airport city names query from a user entered text field and retrieve the ICAO identifier to be presented in a label. It seems the db is loading but it will not query when I select the IBAction button. I think there might be something wrong with my query statement or my database, although I can't list that on here. Any Help would be greatly appreciated.
The Last error I received is: database3[30351:c07] -[ViewController searchICAO:] 1st SQL error 'library routine called out of sequence' (21)
-(NSString*)filePath {
NSArray*paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
return [[paths objectAtIndex:0]stringByAppendingPathComponent:#"mydatabase.sqlite"];
}
//open database
- (void)viewDidLoad{
[self openDB];
}
-(void)openDB {
if(sqlite3_open([[self filePath]UTF8String], &airportDB) !=SQLITE_OK) {
sqlite3_close(airportDB);
NSAssert(0, #"Databese failed to open");
status.text = #"Database Failed to Open";
}
else if (sqlite3_open([[self filePath]UTF8String], &airportDB) ==SQLITE_OK) {//this line not really needed but was trying everything
NSLog(#"database opened"); //test
status.text = #"Database Opened"; //test
}
}
- (IBAction)searchICAO:(id)sender
{
//[self.delegate detailViewControllerDidFinish:self]; //for later use
//Get airport name from the text field user enters
NSString*sql = [NSString stringWithFormat:#"SELECT * FROM airports WHERE city=\"%#\"", [searchDB text]];
const char *query_stmt = [sql UTF8String];
sqlite3_stmt *statement;
NSLog(#"%s 1st SQL error '%s' (%1d)", __FUNCTION__, sqlite3_errmsg(airportDB), sqlite3_errcode(airportDB)); //Error Test
This is where I seem to be having problems at...
if (sqlite3_prepare_v2(airportDB, query_stmt, -1, &statement, NULL)==SQLITE_OK) {// Problem is from here, can't get past this point
NSLog(#"%s 2nd SQL error '%s' (%1d)", __FUNCTION__, sqlite3_errmsg(airportDB), sqlite3_errcode(airportDB)); //Error Test
if (sqlite3_step(statement)==SQLITE_ROW) {
status.text = #""; //Clear the status line
NSString *returnICAO = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];
status.text = returnICAO; //Insert Airport ICAO letters from the database table
}
sqlite3_finalize(statement);
[super viewDidLoad];
}
sqlite3_close(airportDB);
}
I am not seeing exactly what is going wrong, but I would offer that, unless you have a specific objective that cannot be met by using third-party code, you should consider using FMDB https://github.com/ccgus/fmdb - assuming that you need to go directly to SQLite. I have used it quite a bit and had good success with it.
It's a little strange to be "closing" the DB and calling [super viewDidLoad] from searchICAO:. Was this deliberate? It seems like this might be the source of your problem? On the second query, the DB will be closed?

iOS: Multiple queries crashing app

I have an application where I access an SQLite database several times.. But, once I've accessed the database one time, all following attempts cause the app to crash...
I'm not sure if it's because the database has not been properly released...
An example, I run a search to populate a tableview with names of artists. Once I select an artist, I'm navigated to a new tableview, where I want to populate it with the artist's works.
But here's the problem. I access the database to populate the first view, but when I want to populate the second view, it doesn't enter sqlite3_prepare_v2 of the query... so this must mean the database is still in use by the old query..
So what is the proper way of handling closing a database after use?
Currently I do a query like this:
-(NSArray *)findAllArtists
{
NSMutableArray *returnArray = [[[NSMutableArray alloc] init] autorelease];
NSString *query = #"SELECT * FROM Painting GROUP BY Artist";
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil)
== SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
char *uniqueIdChars = (char *) sqlite3_column_text(statement, 0);
char *artistChars = (char *) sqlite3_column_text(statement, 1);
NSString *uniqueId = [[NSString alloc] initWithUTF8String:uniqueIdChars];
NSString *artist = [[NSString alloc] initWithUTF8String:artistChars];
PaintingInfo *info = [[PaintingInfo alloc] initWithUniqueId:uniqueId artist:artist];
[returnArray addObject:info];
[uniqueId release];
[artist release];
}
sqlite3_finalize(statement);
}
sqlite3_close(database);
return returnArray;
}
You should look at fmdb wrapper at github. Even if you don't use it, look at the code.
Where are you opening the database? You're closing it in this code. Before you call it again, it needs to be open. You should consider just keeping it open for the duration of the single user iOS app and closing when you're done. What happens if you simply remove the close call?
The first thing you should do is check all your return codes for sqlite calls. for example, with step you're not handling anything other than SQLITE_ROW. At least log others. Also for finalize and close you're not handling or logging others.
Also, you're preparing (compiling) the sql statement but your not saving it off. prepare_v2 gives you back a compiled statement. Save it off as a member variable and call reset against it before using it again.
To answer your specific question of how to close - you need to consider that some statements may not have been finalized. Here's my close method: (BTW, ENDebug is my wrapper over NSLog)
- (void)close
{
if (_sqlite3)
{
ENInfo(#"closing");
[self clearStatementCache];
int rc = sqlite3_close(_sqlite3);
ENDebug(#"close rc=%d", rc);
if (rc == SQLITE_BUSY)
{
ENError(#"SQLITE_BUSY: not all statements cleanly finalized");
sqlite3_stmt *stmt;
while ((stmt = sqlite3_next_stmt(_sqlite3, 0x00)) != 0)
{
ENDebug(#"finalizing stmt");
sqlite3_finalize(stmt);
}
rc = sqlite3_close(_sqlite3);
}
if (rc != SQLITE_OK)
{
ENError(#"close not OK. rc=%d", rc);
}
_sqlite3 = NULL;
}
}
finally, consider adding much more logging along with the return codes so you can get more insight.
Hope that helps.

Resources