AIR SQLite – Utilisation des transactions (begin / commit / rollback) en asynchrone
Dans l'article précédent, on a vu ce qu'étaient les transactions SQL et comment effectuer une transaction dans AIR SQLite:
AIR SQLite – Utilisation des transactions (begin / commit / rollback) en synchrone
Essayons maintenant de réaliser cette transaction mais sur une base SQLite ouverte en asynchrone.
http://www.flex-tutorial.fr/2011/01/24/air-sqlite-mode-synchrone-et-asynchrone/
http://www.flex-tutorial.fr/2011/01/27/air-sqlite-execution-de-requetes-sql-sqlstatement-et-syntaxe/
Je dis essayons car je vais passer par une étape où je fais une erreur qui est assez "classique" puis on verra comment la résoudre.
Transaction asynchrone sans utiliser de queue
On va ici traduire notre exemple synchrone de manière asynchrone de manière basique:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
horizontalAlign="left" viewSourceURL="srcview/index.html">
<mx:Script>
<![CDATA[
// -- IMPORTS ---------------------------------------------------------------- /
import vo.Contact;
// -- PROPERTIES ------------------------------------------------------------- /
private var conn:SQLConnection;
private var _contacts:Array = [new Contact(0, "A", "A", "A", "a@gmail.com", "M"), new Contact(1,
"B", "B", "B", "b@gmail.com", "M"), new Contact(2, "C", "C", "C", "c@gmail.com", "F"), new Contact(3,
"D", "D", "D", "d@gmail.com", "M"), new Contact(4, "E", "E", "E", "e@gmail.com", "F"), new Contact(5,
"F", "F", "F", "f@gmail.com", "M"), new Contact(6, "G", "G", "G", "g@gmail.com", "F")];
// -- AUTO INIT FUNCTIONS --------------------------------------------------- /
private function clickConnect():void {
conn = new SQLConnection();
var dbFile:File = File.applicationDirectory.resolvePath("employees.db");
// Event Listener that will tell us when the DB is opened
conn.addEventListener(SQLEvent.OPEN, openSuccess);
// Event Listener that will tell us if an error occurs
conn.addEventListener(SQLErrorEvent.ERROR, openFailure);
conn.openAsync(dbFile);
}
private function openSuccess(event:SQLEvent):void {
connLabel.text = "Database Status: Connected";
createTable();
}
private function openFailure(event:SQLEvent):void {
connLabel.text = "Database Status: Not Connected";
}
private function createTable():void {
var createStmt:SQLStatement = new SQLStatement();
createStmt.sqlConnection = conn;
var sql:String = "CREATE TABLE IF NOT EXISTS employees (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" firstName TEXT, lastName TEXT, middleInitial TEXT, email TEXT, gender TEXT)";
createStmt.text = sql;
createStmt.addEventListener(SQLEvent.RESULT, onCreateTableResult);
createStmt.addEventListener(SQLErrorEvent.ERROR, onCreateTableError);
createStmt.execute();
}
private function onCreateTableResult(event:SQLEvent):void {
connLabel.text = "Database Status: Connected and table created";
queryButton.enabled = true;
}
private function onCreateTableError(event:SQLErrorEvent):void {
connLabel.text = "Database Status: Can't create table";
queryButton.enabled = false;
}
private function callQuery(event:Event):void {
// on commence la transaction
conn.begin();
var q:SQLStatement = new SQLStatement();
q.addEventListener(SQLEvent.RESULT, onQueryResult);
q.addEventListener(SQLErrorEvent.ERROR, onQueryError);
q.sqlConnection = conn;
var sql:String = "INSERT INTO employees (firstName, lastName, middleInitial, email, gender)" +
"VALUES (:firstName, :lastName, :middleInitial, :email, :gender)";
q.text = sql;
for each (var contact:Contact in _contacts) {
q.clearParameters();
q.parameters[":firstName"] = contact.firstName;
q.parameters[":lastName"] = contact.lastName;
q.parameters[":middleInitial"] = contact.middleInitial;
q.parameters[":email"] = contact.email;
q.parameters[":gender"] = contact.gender;
q.execute();
}
// on valide la transaction
conn.commit();
}
private function onQueryResult(event:SQLEvent):void {
connLabel.text = "Database Status: Connected and table created";
queryButton.enabled = true;
}
private function onQueryError(event:SQLErrorEvent):void {
connLabel.text = "Database Status: Error in query: " + event.error.message;
// on annule la transaction entière
conn.rollback();
}
]]>
</mx:Script>
<mx:Label text="SQLite Example" fontSize="18" fontWeight="bold" />
<mx:Label id="connLabel" text="Database Status: Not Connected" />
<mx:HBox>
<mx:Button id="connectButton" click="clickConnect()" label="Connect" enabled="true" />
<mx:Button id="queryButton" click="callQuery(event)" label="Insert" enabled="true" />
</mx:HBox>
</mx:WindowedApplication>
C'est une traduction vraiment simpliste de l'exemple synchrone en asynchrone. Trop simpliste en fait car on obtient cette erreur:
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 SQLTransactionASync/callQuery()[F:\Workspaces\Flex\BlogFx4\SQLTransactionASync\src\SQLTransactionASync.mxml:81] at SQLTransactionASync/__queryButton_click()[F:\Workspaces\Flex\BlogFx4\SQLTransactionASync\src\SQLTransactionASync.mxml:103]
Diagnostic rapide
Alors que nous indique cette exception? Elle nous indique que l'on a essayé d'exécuter un SQLStatement alors que celui-ci était déjà en train d'être exécuté. Cette propriété se trouve sur SQLStatement, la propriété "executing" (Boolean).
On essaie en fait d'exécuter à nouveau le même SQLStatement (nommé "q" ici) alors que son exécution n'est pas terminé. En mode synchrone, on ne pouvait pas rencontrer ce problème car le programme "attend" que l'exécution de la requête soit terminée pour continuer son exécution.
On pourrait alors choisir de créer un SQLStatement à chaque fois mais cela voudrait dire que nous n'utiliserions plus les requêtes SQL paramétrées et les performances de notre application tomberait en flèche.
Transaction asynchrone avec utilisation d'une queue
Le mieux pour résoudre ce problème est d'exécuter les SQLStatement sous forme de chaîne. Pour réutiliser notre SQLStatement "q", on va donc attendre que son exécution soit terminée puis relancer une requête avec ce même SQLStatement. Pour cela, on va utiliser les évènements dispatchés lors de l'exécution d'une requête SQL.
Voici donc le code corrigé pour l'exécution de transactions asynchrones:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
horizontalAlign="left" viewSourceURL="srcview/index.html">
<mx:Script>
<![CDATA[
// -- IMPORTS ---------------------------------------------------------------- /
import vo.Contact;
// -- PROPERTIES ------------------------------------------------------------- /
private var conn:SQLConnection;
/**
* SQLStatement ré-utilisé lors de l'insertion
*/
private var q:SQLStatement = null;
private var _contacts:Array = [new Contact(0, "A", "A", "A", "a@gmail.com", "M"), new Contact(1,
"B", "B", "B", "b@gmail.com", "M"), new Contact(2, "C", "C", "C", "c@gmail.com", "F"), new Contact(3,
"D", "D", "D", "d@gmail.com", "M"), new Contact(4, "E", "E", "E", "e@gmail.com", "F"), new Contact(5,
"F", "F", "F", "f@gmail.com", "M"), new Contact(6, "G", "G", "G", "g@gmail.com", "F")];
// -- AUTO INIT FUNCTIONS --------------------------------------------------- /
private function clickConnect():void {
conn = new SQLConnection();
var dbFile:File = File.applicationDirectory.resolvePath("employees.db");
// Event Listener that will tell us when the DB is opened
conn.addEventListener(SQLEvent.OPEN, openSuccess);
// Event Listener that will tell us if an error occurs
conn.addEventListener(SQLErrorEvent.ERROR, openFailure);
conn.openAsync(dbFile);
}
private function openSuccess(event:SQLEvent):void {
connLabel.text = "Database Status: Connected";
createTable();
}
private function openFailure(event:SQLEvent):void {
connLabel.text = "Database Status: Not Connected";
}
private function createTable():void {
var createStmt:SQLStatement = new SQLStatement();
createStmt.sqlConnection = conn;
var sql:String = "CREATE TABLE IF NOT EXISTS employees (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" firstName TEXT, lastName TEXT, middleInitial TEXT, email TEXT, gender TEXT)";
createStmt.text = sql;
createStmt.addEventListener(SQLEvent.RESULT, onCreateTableResult);
createStmt.addEventListener(SQLErrorEvent.ERROR, onCreateTableError);
createStmt.execute();
}
private function onCreateTableResult(event:SQLEvent):void {
connLabel.text = "Database Status: Connected and table created";
queryButton.enabled = true;
}
private function onCreateTableError(event:SQLErrorEvent):void {
connLabel.text = "Database Status: Can't create table";
queryButton.enabled = false;
}
private function callQuery():void {
if (_contacts.length == 0) {
// traitement terminé
// on valide la transaction
conn.addEventListener(SQLEvent.COMMIT, commitHandler);
conn.commit();
return;
}
if (!q) {
// on commence la transaction
conn.begin();
q = new SQLStatement();
q.addEventListener(SQLEvent.RESULT, onQueryResult);
q.addEventListener(SQLErrorEvent.ERROR, onQueryError);
q.sqlConnection = conn;
var sql:String = "INSERT INTO employees (firstName, lastName, middleInitial, email, gender)" +
"VALUES (:firstName, :lastName, :middleInitial, :email, :gender)";
q.text = sql;
}
// on prend le premier élément de la queue que l'on supprime en même temps (shift)
var contact:Contact = _contacts.shift() as Contact;
q.clearParameters();
q.parameters[":firstName"] = contact.firstName;
q.parameters[":lastName"] = contact.lastName;
q.parameters[":middleInitial"] = contact.middleInitial;
q.parameters[":email"] = contact.email;
q.parameters[":gender"] = contact.gender;
q.execute();
}
private function onQueryResult(event:SQLEvent):void {
callQuery();
}
private function onQueryError(event:SQLErrorEvent):void {
connLabel.text = "Database Status: Error in query: " + event.error.message;
// on annule la transaction entière
if (conn.inTransaction) {
conn.addEventListener(SQLEvent.ROLLBACK, rollbackHandler);
conn.rollback();
}
}
private function commitHandler(event:SQLEvent):void {
conn.removeEventListener(SQLEvent.COMMIT, commitHandler);
connLabel.text = "Transaction terminée.";
}
private function rollbackHandler(event:SQLEvent):void {
conn.removeEventListener(SQLEvent.ROLLBACK, rollbackHandler);
connLabel.text = "Rollback terminé";
}
]]>
</mx:Script>
<mx:Label text="SQLite Example" fontSize="18" fontWeight="bold" />
<mx:Label id="connLabel" text="Database Status: Not Connected" />
<mx:HBox>
<mx:Button id="connectButton" click="clickConnect()" label="Connect" enabled="true" />
<mx:Button id="queryButton" click="callQuery()" label="Insert" enabled="true" />
</mx:HBox>
</mx:WindowedApplication>
Télécharger les sources de l'exemple au format FXP
Dans cet exemple, j'ai même mis les écouteurs d'évènements sur les évènements SQLEvent.COMMIT et SQLEvent.ROLLBACK qui se font aussi de manière asynchrone si vous avez ouvert votre base avec openAsync().
Articles similaires
- AIR SQLite – Utilisation de Responder en mode asynchrone
- AIR SQLite – Utilisation des transactions (begin / commit / rollback) en synchrone
- AIR SQLite – Création de tables SQLite et types de données
- AIR SQLite – Typer les données renvoyées par un SELECT avec itemClass
- AIR SQLite – Création d'une base de données SQLite






31 mai 2011
Belle utilisation du récursif, il fallait y penser ! bien vu, par contre j'ai mis un peu de temps à comprendre, tu devrais peut être étoffer tes explications? nice work sinon
31 mai 2011
Salut,
ce n'est pas récursif, c'est simplement une file d'attente
Fabien