AIR SQLite utility library (SQL runner, database copy)

A set of libraries for optimizing the process of working with SQLite databases in Adobe AIR.

  1. Download/source
  2. Documentation
  3. Examples
  4. Project history
  5. Features
  6. Design philosophy

Download/Source

The AIR SQLite utility library source code is hosted on Github, released under the MIT license.

The current version is 0.1.2 beta. Download|Changes

Documentation

air-sqlite project language reference documentation (including examples)

Examples

Short examples

The minimum to get going.

Example 1: Standard (pooled) SQLRunner running a SQL SELECT statement

A common use case — running a single SELECT statement to load one row from a table, passing the primary key as a parameter:

ActionScript code:

// setup code:
// define database file location
var dbFile:File = File.applicationStorageDirectory.resolvePath("myDatabase.db");
// create the SQLRunner
var sqlRunner:SQLRunner = new SQLRunner(dbFile);

// ...

// run the statement, passing in one parameter (":employeeId" in the SQL)
// the statement returns an Employee object as defined in the 4th parameter
sqlRunner.execute(LOAD_EMPLOYEE_SQL, {employeeId:102}, resultHandler, Employee);

private function resultHandler(result:SQLResult):void
{
    var employee:Employee = result.data[0];
    // do something with the employee data
}

// constant for actual SQL statement text
[Embed(source="sql/LoadEmployee.sql", mimeType="application/octet-stream")]
private static const LoadEmployeeStatementText:Class;
private static const LOAD_EMPLOYEE_SQL:String = new LoadEmployeeStatementText();

Corresponding SQL statement (LoadEmployee.sql):

SELECT firstName,
    lastName,
    email,
    phone
FROM main.employees
WHERE employeeId = :employeeId

Robotlegs example

If you’re wondering how to use the library in a Robotlegs application, look no further. In the Robotlegs “AddressBook” example that has been used in Robotlegs presentations, this library is used in a Robotlegs service layer class to provide database access. The complete app was built using a dummy XML service, then converted to use the SQL service by changing only a couple of lines of code (plus adding the new service class)—demonstrating the benefit of clean separation that Robotlegs provides.

Longer examples with explanations

These examples are taken from real applications where I really used this library. (In fact, one reason I feel that this design approach works well with my preferred workflow is because I developed and adapted the library as I was building real-world apps. For example, I didn’t add the “batch” functionality until I needed it, and I didn’t build the “unpooled” version of the SQLRunner until I needed it for an app.)

Standard SQLRunner

This is the “normal” version of the SQLRunner. It uses connection pooling and executes multiple SQL statements simultaneously, so it’s designed for use in desktop apps on fairly modern hardware that can handle multiple threads (it doesn’t have to be “multicore”). I developed and used this version on a laptop from mid-2006 with a 2.33 GHz Intel Centrino Duo processor, which is reasonably fast but no faster than today’s consumer-grade systems.

One technique I’ve used is to create a simple app-specific “service” class that wraps the SQLRunner. Application code then calls that service to perform various operations. This code is an example of the service class from one of my apps:

package demo.data
{
    import com.probertson.data.QueuedStatement;
    import com.probertson.data.SQLRunner;
    import flash.filesystem.File;

    public class SQLService
    {
        // ------- Constants -------

        private static const dbFile:File = File.applicationStorageDirectory.resolvePath("MyDatabase.db");

        // ------- Singleton implementation -------

        private static var _instance:SQLService;
        public static function getInstance():SQLService
        {
            if (_instance == null)
            {
                _instance = new SQLService(dbFile);
            }

            return _instance;
        }

        // ------- Constructor -------

        public function SQLService(databaseFile:File)
        {
            _sqlRunner = new SQLRunner(databaseFile);
        }

        // ------- Instance vars -------

        private var _sqlRunner:SQLRunner;

        // ------- Public methods -------

        public function close(resultHandler:Function):void
        {
            _sqlRunner.close(resultHandler);
        }

        public function execute(sql:String, parameters:Object, handler:Function, itemClass:Class=null):void
        {
            _sqlRunner.execute(sql, parameters, handler, itemClass);
        }

        public function executeModify(statementBatch:Vector.<QueuedStatement>, resultHandler:Function, errorHandler:Function, progressHandler:Function):void
        {
            _sqlRunner.executeModify(statementBatch, resultHandler, errorHandler, progressHandler);
        }
    }
}

Here’s an example of some code that called the service for a typical operation (e.g. a SQL SELECT command). A few notes about this code:

import flash.data.SQLResult;
import demo.data.SQLService;

[Embed(source="sql/load/EmployeeDataLoader.sql", mimeType="application/octet-stream")]
private static const StatementText:Class;
private static const SQL:String = new StatementText();

private function loadEmployeeData(empId:String):void
{
    SQLService.getInstance().execute(SQL, { employeeId:empId }, loadEmployeeData_result, EmployeeData);
}

private function loadEmployeeData_result(result:SQLResult):void
{
    if (result.data == null)
    {
        _resultData = null;
        dispatchEvent(new ElementLoadEvent(ElementLoadEvent.NO_ROWS_RETURNED));
    }
    else
    {
        _tempResult = result.data[0];

        // load child elements
        loadAvailability();
        loadRelatedLinks();
    }
}

Here’s an example of code that calls the executeModify() method. This supports two more “advanced” use cases:

package demo.data
{
    import com.probertson.data.QueuedStatement;
    import flash.data.SQLResult;
    import flash.errors.SQLError;

    public class BuildDatabase
    {
        // ------- Member vars -------

        private var _resultHandler:Function;
        private var _errorHandler:Function;
        private var _progressHandler:Function;

        // ------- Public methods -------

        public function execute(resultHandler:Function, errorHandler:Function, progressHandler:Function):void
        {
            _resultHandler = resultHandler;
            _errorHandler = errorHandler;
            _progressHandler = progressHandler;

            createStructure();
        }

        // ------- Private methods -------

        private function createStructure():void
        {
            var statements:Vector.<QueuedStatement> = new Vector.<QueuedStatement>();

            // CREATE TABLE
            statements[statements.length] = new QueuedStatement(CREATE_TBL_EMPLOYEES_SQL);
            statements[statements.length] = new QueuedStatement(CREATE_TBL_DEPARTMENTS_SQL);
            statements[statements.length] = new QueuedStatement(CREATE_TBL_LOCATIONS_SQL);

            // ...

            // INSERT default data
            statements[statements.length] = new QueuedStatement(INSERT_DATA_SQL);			

            // execute
            SQLService.getInstance().executeModify(statements, executeBatch_complete, executeBatch_error, executeBatch_progress);
        }

        // ------- Event handling -------

        private function executeBatch_progress(completedCount:uint, totalCount:uint):void
        {
            _progressHandler(completedCount, totalCount);
        }

        private function executeBatch_complete(results:Vector.):void
        {
            _resultHandler();
        }

        private function executeBatch_error(error:SQLError):void
        {
            // handle the error as desired or bubble

            _errorHandler(error);
        }

        // ------- SQL statements -------

        // --- CREATE TABLE ---

        [Embed(source="sql/create/CreateTable_employees.sql", mimeType="application/octet-stream")]
        private const CreateEmployeesStatementText:Class;
        private const CREATE_TBL_EMPLOYEES_SQL:String = new CreateEmployeesStatementText();

        [Embed(source="sql/create/CreateTable_departments.sql", mimeType="application/octet-stream")]
        private const CreateDepartmentsStatementText:Class;
        private const CREATE_TBL_DEPARTMENTS_SQL:String = new CreateDepartmentsStatementText();

        [Embed(source="sql/create/CreateTable_locations.sql", mimeType="application/octet-stream")]
        private const CreateLocationsStatementText:Class;
        private const CREATE_TBL_LOCATIONS_SQL:String = new CreateLocationsStatementText();

        // --- INSERT default values ---

        [Embed(source="sql/create/Insert_data.sql", mimeType="application/octet-stream")]
        private const InsertDataStatementText:Class;
        private const INSERT_DATA_SQL:String = new InsertDataStatementText();
    }
}

SQLRunner with only one connection

This is the “lite” version of the SQLRunner. It only uses one SQLConnection object, so there isn’t any connection pooling. (It also avoids the use of Dictionary objects that are used in the standard version.) It still has the benefit of caching statements and ease-of-use, but definitely runs leaner. I developed this version for use in one of the Flash-based iPhone apps that was created for Adobe MAX 2009. It worked great on the iPhone 3G/iPod Touch 2nd generation — which is obviously a lower-end hardware target compared to a desktop computer.

Here is the SQLService class I use with the unpooled SQLRunner (note that it’s nearly identical to the one described above):

package demo.data
{
    import com.probertson.data.Responder;
    import com.probertson.data.SQLRunnerUnpooled;
    import flash.filesystem.File;

    public class SQLService
    {
        // ------- Constants -------

        private static const dbFileName:String = "MyDatabase.db";
        private static const dbFile:File = File.applicationStorageDirectory.resolvePath(dbFileName);

        // ------- Singleton implementation -------

        private static var _instance:SQLService;
        public static function getInstance():SQLService
        {
            if (_instance == null)
            {
                _instance = new SQLService(dbFile);
            }

            return _instance;
        }

        // ------- Constructor -------

        public function SQLService(databaseFile:File)
        {
            _sqlRunner = new SQLRunnerUnpooled(databaseFile);
        }

        // ------- Instance vars -------

        private var _sqlRunner:SQLRunnerUnpooled;

        // ------- Public methods -------

        public function execute(sql:String, parameters:Object, responder:Responder, itemClass:Class=null):void
        {
            _sqlRunner.execute(sql, parameters, responder, itemClass);
        }
    }
}

Here’s an example of some code that calls the SQLService to execute a SQL query. (Again, it’s very similar to the previous example, so see that example’s explanation for more details.):

import com.probertson.data.Responder;

import demo.data.SQLService;
import demo.vo.CategoryData;

// ------- SQL statements -------

// --- Load Categories ---
[Embed(source="sql/CategoryLoader_categories.sql", mimeType="application/octet-stream")]
private const LoadCategoriesStatementText:Class;
private const LOAD_CATEGORIES_SQL:String = new LoadCategoriesStatementText();

// --- Load subcategories ---
[Embed(source="sql/CategoryLoader_subcategories.sql", mimeType="application/octet-stream")]
private const LoadSubcategoriesStatementText:Class;
private const LOAD_SUBCATEGORIES_SQL:String = new LoadSubcategoriesStatementText();

// ------- Constructor -------

public function CategoryLoader()
{
    _sqlService = SQLService.getInstance();
}

// ------- Private vars -------

private var _sqlService:SQLService;

// ------- Public methods -------

public function loadCategories():void
{
    _sqlService.execute(LOAD_CATEGORIES_SQL, null, new Responder(categories_complete, categories_error), CategoryData);
}

public function loadSubcategories(categoryId:int):void
{
    var params:Object = new Object();
    params.parentId = categoryId;

    _sqlService.execute(LOAD_SUBCATEGORIES_SQL, params, new Responder(subcategories_complete, subcategories_error), CategoryData);
}

// ------- Event handling -------

private function categories_complete(result:SQLResult):void
{
    // do something with the result data
}

private function categories_error(error:SQLError):void
{
    // handle the error
}

private function subcategories_complete(result:SQLResult):void
{
    // do something with the result data
}

private function subcategories_error(error:SQLError):void
{
    // handle the error
}

Project history

0.1.2 (2010-08-18):

0.1.1 (2010-01-02):

0.1 (2010-02-23): Initial release

Features

The code currently provides two main features:

Design philosophy

As the blurb above says, the goal of this project is to “optimize the developer experience.” That’s the underlying philosophy behind the design of this library.

A key part of optimization is to optimize your (the developer’s) workflow. These APIs are designed to make it easy to integrate SQLite functionality into an AIR application with no boilerplate code necessary. You write your SQL code and your event handlers…and that’s all.

Simplicity shouldn’t require you to sacrifice performance. The code is written to follow best practices and guidance for database performance optimization. You don’t have to worry about designing your data access code for performance, because it’s already done for you.

Simplicity also shouldn’t require you to sacrifice control and flexibility. You write your own SQL code so you can load only the data you need, when you need it, and optimize the design of your database and queries based on your knowledge of your application. (This is in contrast to systems such as ORM which provide a nice benefit that you don’t have to write SQL, with the downside that you have to limit yourself to basic object structures and generic data loading — usually loading more data than you really need — rather than being able to tune the database and queries to your application’s needs.)

Comments/trackbacks are closed for this post.

16 Comments so far


  1. Mario Rodrigues is reported to have said:

    Hi, thank you for this library. Do you have any sample code or pointers for getting started?


  2. Mario Rodrigues is reported to have said:

    Hi, I get the following error when trying to use your library for both simple SQL statements and batch statements.

    Error: Error #3110: Operation cannot be performed while SQLStatement.executing is true.
    at Error$/throwError()
    at flash.data::SQLStatement/checkReady()
    at flash.data::SQLStatement/execute()
    at com.probertson.data.sqlRunnerClasses::PendingBatch/executeStatements()[C:\git\air-sqlite\src\com\probertson\data\sqlRunnerClasses\PendingBatch.as:112]
    at com.probertson.data.sqlRunnerClasses::PendingBatch/conn_begin()[C:\git\air-sqlite\src\com\probertson\data\sqlRunnerClasses\PendingBatch.as:95]

    Any ideas on what I might be doing wrong?

    [This should be fixed in version 0.1.1]


  3. Don Mitchinson is reported to have said:

    Hi Matt:

    I don’t know if you found the solution to your problemm or not yet, but I encountered similar errors when I was opening my db asynchronlously.

    I had to ensure SQLStatement.executing was false before executing the next statement.

    Don

    [The problem was a bug in the library, which is fixed in version 0.1.1]


  4. Nicholas Shipes is reported to have said:

    I’m having the same problem with SQLStatement.executing returning true when running batch statements. Don, you stated you had to ensure .executing was false before executing the next statement, but how did you accomplish this? Did you modify the PendingBatch class for this?

    [This should be fixed in version 0.1.1]


  5. Adobe Flash Platform * Application design for database performance is reported to have said:

    […] execute multiple statements simultaneously. For more information and to download the library, visit http://probertson.com/projects/air-sqlite/. […]


  6. AIR for Android resource roundup | simon-townsend.com is reported to have said:

    […] AIR SQLite Utility […]


  7. Jeffery Fernandez is reported to have said:

    Hi Robert,

    Why do I keep getting the following error:

    SQLError: ‘Error #3125: Unable to open the database file.’, details:’Connection closed.’, operation:’open’, detailID:’1001’

    I have followed the examples you have shown here but I get the above error. I understand what the error means ( that the database path does not exist ) but am I missing something else. Do I have to make an SQLConnection before creating the SQLRunner object?


  8. Lukasz 'Severiaan' Grela is reported to have said:

    @Jeffrey Fernandez
    I could guess that you have used that on android device. I have the same error when I am trying to save to the sdcard so basically any path for file:///mnt/sdcard from the list below.

    File.applicationDirectory:app:/
    File.applicationStorageDirectory:app-storage:/
    File.desktopDirectory:file:///mnt/sdcard
    File.documentsDirectory:file:///mnt/sdcard
    File.userDirectory:file:///mnt/sdcard


  9. Damon is reported to have said:

    I’m confused a bit between execute vs executeModify. I think i get that execute does a single statement and can assign the results as a specific class, and execute modify allows you to batch statements. But what if you’d like to batch statements and have the results be typed as a class? Is that simply not possible because you can’t have an array of specific classes the same way you can have an array of objects?

    the reason i’m curious is I have a large statement batch to pull a variety of objects from different tables from a day, and I would like to be able to have them come out as their proper class types so that i can update them right in a datagrid.


  10. Paul Robertson is reported to have said:

    Hi Damon,

    The difference between execute() and executeModify() really just comes down to the types of SQL commands each one can run — it doesn’t have anything to do with “batch” or “not a batch”.

    I. For SELECT statements, use execute()

    if you want to run several SELECT statements at the same time, just call execute() several times. It’s okay — in fact, the library is designed specifically around that use case.
    The statements will run in parallel, up to the number of available database connections (specified in the SQLRunner constructor, default 5).
    If you send more statements than there are connections available, the rest will just patiently wait and will run as soon as a connection becomes available.

    II. For INSERT/UPDATE/DELETE statements, use executeModify()

    if you want the statement to run in isolation, call executeModify() and just pass one statement (wrapped in a “batch”).
    If you want to run several statements and have them linked together in a transaction (so that if one fails they are all reverted), call executeModify() one time with all the statements together in the batch.
    There’s no point to having an itemClass for executeModify(), since you (presumably) aren’t going to send a SELECT statement in the middle of a transaction of INSERT/UPDATE/DELETE statements
    If you want to for example perform an INSERT or UPDATE then run a SELECT statement immediately after, just call executeModify() then call execute(). The library takes care of keeping them in order so the INSERT/UPDATE will run first, the transaction will finish, and then your SELECT will run.

    Sorry if this is not very clear. I’m going to try to rethink the api to figure out a way to make this make more sense (or else to not have the two methods, or something).


  11. rudi is reported to have said:

    hello,
    I’m still trying to create some sort of application “server” that is able to respond to multiple clients through your library on lan.
    Now I’m a little panicked at the time that it does not respond well, let me explain: If you start to change simultaneously two modify queries I get from my “server”only the first response to the query made ‚Äã‚Äã… now I’d send you my job so you could see it if you want! unfortunately I do not have good depth of your work, and I’m not a great programmer, it may be that your library may stop when there’s two or more simultaneous requests for changes? or rather, it is possible that in the previous case, your utility takes into account only the first query (perhaps not consider … is lost … or anything else?).
    if I understood, your library takes all queries that come in, put them in a queue, and executes them one after the other, keeping the query blockers, without skipping any request.
    thanks for your kind reply.


  12. Tac is reported to have said:

    Thanks, this looks like an interesting approach. I’m trying to integrate it with SurveyApe, but am such a newbie at Flash/Flex that I’m stuck on some of the basics. Can you post (or point me to) a working project, with the associated mxml and Employee.as class files?

    Thx,

    Tac


  13. SQLiteNoob is reported to have said:

    Hey, Amazing stuff! I’ve been learning a lot about SQLite and Flex from the posts you’ve done on this site and from Adobe.

    Quick question - I created an SQLite db, and now I want to use your DBCopier.as class to encrypt it - but I am having a lot of difficulty: Flash Builder doesn’t recognize the “EncrypterProgressPhase” properties as well as the start() method! Could a lowly newb like myself humbly request a post of how to use this class?

    Thanks for everything you have already contributed to the community!


  14. Paul is reported to have said:

    Paul.

    I am finding your code very helpful while developing an Android App.
    At the moment I am using your SQLRunnerUnpooled class but I got to a moment in which I need to do an INSERT to the database. How can I do that, or should I change code to use the SQLRunner class instead.

    Thx a lot


  15. Paul is reported to have said:

    Hmmm. I guess I need to make the docs clearer on that point!

    The SQLRunnerUnpooled.execute() method should work for any type of SQL statement, so you should be fine using it with an INSERT statement.

    If it doesn’t work, let me know and I’ll fix it!


  16. Paul is reported to have said:

    Hey Paul, You are totally right as it is working fine. I was having some permission issue with db.
    Your library is great by the way!

    Cheers,
    Paul

Articles by Type

Articles by Topic

Random Reading

Currently...

Adobe MAX 2011 Speaker H. Paul Robertson: Adobe Community Professional

Subscribe