Використання SQLite в Windows і Windows Phone додатках на JavaScript



Новим для Windows Phone 8.1 є можливість створювати і запускати додатки, написані на JavaScript також, як на Windows 8.1. Тим не менш, є деякі відмінності в специфіці API, доступних для додатків на Windows Phone 8.1. Однією з таких відмінностей є відсутність IndexedDB на телефоні. Це становить труднощі для JavaScript розробників універсальних додатків, яким потрібна структуроване сховище. У цій статті ми подивимося, як створити компонент WinRT, що дозволяє використовувати SQLite з JavaScript. Також ми підготували для вас приклад застосування.

Примітка: Нижче наведено два існуючих проекту, які обгортають SQLite WinRT. Ви можете використовувати їх замість того, щоб писати свою власну обгортку. Перш ніж писати свої обгортки, подивіться, чи надають вони ту функціональність, яка вам потрібна і підходять їх ліцензії для вас. Рішення, про яке йде мова в цьому пості, виникло головним чином для того, щоб уникнути проблем з ліцензуванням.

План
Будемо дотримуватися наступного плану, щоб навчитися використовувати SQLite в універсальному Windows додатку на JavaScript:

  1. Відкриємо проект Visual Studio для існуючого універсального Windows програми на JavaScript.
  2. Встановимо розширення SQLite для Visual Studio.
  3. Створимо компонент WinRT для проекту універсального додатка.
  4. Напишемо код для обгортки SQLite.
  5. Напишемо загальний код додатка, що використовує компонент WinRT для SQLite.
Ми будемо слідувати цьому плану, розбираючи реалізацію програми та звертаючи увагу на зміну коду IndexedDB для використання SQLite. За основу ми взяли приклад IndexedDB для Windows 8.1, зробили його універсальним додатком, і пройшли кроків, описаних нижче.

Установка розширення SQLite для Visual Studio
Команда розробників SQLite випустила розширення для Visual Studio SQLite для Windows Runtime (Windows 8.1), максимально спростивши додавання SQLite додатки Windows 8.1. Відвідайте посиланню вище, натисніть посилання Завантаження, відкрийте VSIX файл для установки розширення Visual Studio.

Також команда розробників SQLite випустила ще одне розширення для VS — SQLite для Windows Phone 8.1. Виконайте ті ж кроки, щоб встановити розширення.

Створення компонента WinRT для проекту універсального додатка
SQLite написаний на З, і, щоб використовувати його в додатках на JavaScript, необхідно обернути SQLite API в WinRT компонент.

Відкрийте ваш додаток в Visual Studio і додайте новий проект Windows Runtime Component для універсальних додатків, який можна знайти по дорозі: Visual C++ > Apps Store > Універсальний Apps. Створяться проекти Windows, Windows Phone і загальні файли у вашому рішення для нового компонента WinRT.

Щоб використовувати новий компонент WinRT, вам необхідно додати посилання з проектів програми на проект компонента WinRT. У проект Windows 8.1 додайте посилання на WinRT-компонент для Windows 8.1, а в проект Windows Phone 8.1, відповідно, посилання на WinRT-компонент для Windows Phone.

Тепер додатки можуть використовувати компонент WinRT, але вони як і раніше не використовують розширення SQLite. Додайте посилання на SQLite WinRT компонент для Windows 8.1 і для Windows Phone 8.1. Розширення можна знайти у діалоговому вікні додавання посилань у розширення для Windows Phone) 8.1.

Пишемо код обгортки SQLite
Для детальної інформації по створенню компонентів C++/CX WinRT, дивіться посилання Creating Windows Runtime Components in C++ document і Visual C++ Language Reference (C++/CX). Ми створимо компонент WinRT з мінімально необхідною функціональністю, яка дозволить використовувати його для більшості завдань на JavaScript.

У нашому випадку додатка потрібне підключення до бази даних, створення таблиць, вставка даних та виконання операцій в транзакціях. Схема, необхідна для нашого прикладу дуже проста, тому обгортка WinRT містить тільки об'єкт Database, який може відкривати і закривати базу даних і виконувати інструкції SQL. Для того, щоб спростити додавання даних, ми підтримуємо параметри згідно з інструкціями SQL. Для отримання запитуваних даних, ми повертаємо масив об'єктів рядків з нашого виконуваного методу. Всі наші методи є асинхронними, тому вони не блокують потік користувальницького інтерфейсу програми під час використання бази даних.

В JavaScript ми реалізуємо декілька функцій, які дозволять виконувати запити асинхронно, по одному або в транзакції і перетворювати результати запитів в об'єкти JavaScript.

Для вашого власного проекту можуть знадобитися додаткові API SQLite; наш приклад — це просто демонстраційний зразок, для якого не потрібні розширені функції SQLite.

Деталі реалізації прикладу
Нижче представлений код WinRT для SQLite.

C++/CX

API бібліотеки SQLite написаний на С і переважно використовує UTF-8 char* і при виникненні помилки повертає її код. WinRT, навпаки, зазвичай використовує UTF-16 Platform::String при виникненні помилки повертає виняток. У файлах util.* ми реалізували ValidateSQLiteResult, який перетворює коди помилок, які повернулися від функцій SQLite, виключення WinRT, або передає значення, що повертається у разі, якщо помилки не сталося. Також у файлах util.* є дві функції для перетворення між типами рядків UTF-8 std::string, та UTF-16 і типами рядків Platform::String.

У файлах Database.* ми реалізуємо клас Database для WinRT, у якого є декілька методів. Нижче представлений код класу Database.h:

public ref class Database sealed
{
public:
static Windows::Foundation::IAsyncOperation<Database^>
^OpenDatabaseInFolderAsync(Windows::Storage::StorageFolder 
^databaseFolder,
Platform::String ^databaseFileName);

virtual ~Database();

Windows::Foundation::IAsyncOperationWithProgress<

Windows::Foundation::Collections::IVector<ExecuteResultRow^>^,
ExecuteResultRow^> ^ExecuteAsync(Platform::String ^statementAsString);

Windows::Foundation::IAsyncOperationWithProgress<

Windows::Foundation::Collections::IVector<ExecuteResultRow^>^,
ExecuteResultRow^> 
^BindAndExecuteAsync(Platform::String ^statementAsString,

Windows::Foundation::Collections::IVector<Platform::String^>
^parameterValues);

private:
Database();

void CloseDatabase();

void EnsureInitializeTemporaryPath();
void OpenPath(const std::string &databasePath);

static int SQLiteExecCallback(void *context, int columnCount,
char **columnNames, char **columnValues);

sqlite3 *database;
};

Статичний метод OpenDatabaseInFolderAsync — єдиний загальнодоступний метод для створення об'єкта Database. Цей асинхронний метод повертає IAsyncOperation<Database^>^ створений або відкритий об'єкт Database. У реалізації ми переконуємося, що тимчасовий шлях SQLite налаштований так, як описано в документації SQLite, а потім ми викликаємо sqlite3_open_v2, використовуючи функції з util.*. Ми реалізуємо асинхронну операцію за допомогою PPL create_async.

Ось визначення методу OpenDatabaseInFolderAsync з файлу Database.cpp:

Windows::Foundation::IAsyncOperation<Database^>
^Database::OpenDatabaseInFolderAsync(Windows::Storage::StorageFolder ^databaseFolder,
Platform::String ^databaseFileName)
{
return create_async([databaseFolder, databaseFileName]() -> Database^
{
Database ^database = ref new Database();
string databasePath = PlatformStringToUtf8StdString(databaseFolder->Path);
databasePath += "";
databasePath += PlatformStringToUtf8StdString(databaseFileName);

database->OpenPath(databasePath);
return database;
});
}

Database::ExecuteAsync також асинхронний, в цей раз повертає IAsyncOperationWithProgress< IVector<ExecuteResultRow^>^, ExecuteResultRow^>, в яких асинхронний результат є вектором будь-якого ExecuteResultRows, запитуваної виконується SQL інструкцією та додатково надає повідомлення про виконання, що містять ті ж самі запитувані рядки, але надані тільки у разі одночасного вибору. Ми викликаємо sqlite3_exec, який використовує зворотний виклик, для того, щоб повертати результат виконання запиту. Нижче представлена реалізація методу ExecuteAsync і SQLiteExecCallback з файлу Database.cpp:

struct SQLiteExecCallbackContext
{
Windows::Foundation::Collections::IVector<ExecuteResultRow^> ^rows;
Concurrency::progress_reporter<SQLite::ExecuteResultRow^> reporter;
};

Windows::Foundation::IAsyncOperationWithProgress<
Windows::Foundation::Collections::IVector<ExecuteResultRow^>^,
ExecuteResultRow^> ^Database::ExecuteAsync(Platform::String ^statementAsString)
{
sqlite3 *database = this->database;

return create_async([database,
statementAsString](Concurrency::progress_reporter<SQLite::ExecuteResultRow^>
reporter) -> Windows::Foundation::Collections::IVector<ExecuteResultRow^>^
{
SQLiteExecCallbackContext context = {ref new Vector<ExecuteResultRow^>(),
reporter};
ValidateSQLiteResult(sqlite3_exec(database,
PlatformStringToUtf8StdString(statementAsString).c_str(),
Database::SQLiteExecCallback, reinterpret_cast<void*>(&context),
nullptr));
return context.rows;
});
}

int Database::SQLiteExecCallback(void *contextAsVoid, int columnCount,
char **columnNames, char **columnValues)
{
SQLiteExecCallbackContext *context = 
reinterpret_cast<decltype(context)>(contextAsVoid);
ExecuteResultRow ^row = ref new ExecuteResultRow(columnCount,
columnNames, columnValues);

context->rows->Append(row);
context->reporter.report(row);

return 0;
}

Для забезпечення прив'язки параметра SQL, ми реалізували Database::BindAndExecuteAsync, повертає те ж значення, що і Database::ExecuteAsync. Database::ExecuteAsync приймає параметр, який є вектором рядків, які повинні бути прив'язані до інструкцій SQL. Цікаво зауважити: параметр IVector<String^>^ прив'язаний до зухвалому потоку, тому ми створюємо копію списку рядків як std::vector < String^>. Його ми фіксуємо в нашому лямбда-виразу create_async і можемо використовувати в іншому потоці. Тому sqlite3_exec не забезпечує прив'язку параметра, ми виконуємо послідовність явних реалізацій sqlite3_prepare, sqlite3_bind, sqlite3_step, sqlite3_finalize.

Нижче представлено визначення BindAndExecuteAsync з файлу Database.cpp:

Windows::Foundation::IAsyncOperationWithProgress<
Windows::Foundation::Collections::IVector<ExecuteResultRow^>^,
ExecuteResultRow^> ^Database::BindAndExecuteAsync(
Platform::String ^statementAsString,
Windows::Foundation::Collections::IVector<Platform::String^>
^parameterValuesAsPlatformVector)
{
sqlite3 *database = this->database;

// Створюємо нашу власну копію параметрів, так як //наданий IVector не доступний на інших потоках

std::vector<Platform::String^> parameterValues;
for (unsigned int index = 0; index < parameterValuesAsPlatformVector->Size; ++index)
{
parameterValues.push_back(parameterValuesAsPlatformVector->GetAt(index));
}

return create_async([database, statementAsString,
parameterValues](Concurrency::progress_reporter<SQLite::ExecuteResultRow^>
reporter) -> Windows::Foundation::Collections::IVector<ExecuteResultRow^>^
{
IVector<ExecuteResultRow^> ^results = ref new Vector<ExecuteResultRow^>();
sqlite3_stmt *statement = nullptr;

ValidateSQLiteResult(sqlite3_prepare(database,
PlatformStringToUtf8StdString(statementAsString).c_str(), -1,
&statement, nullptr));

const size_t parameterValuesLength = parameterValues.size();
for (unsigned int parameterValueIndex = 0;
parameterValueIndex < parameterValuesLength; ++parameterValueIndex)
{
//Параметри зв'язки індексовані 1ей

ValidateSQLiteResult(sqlite3_bind_text(statement, parameterValueIndex + 1,
PlatformStringToUtf8StdString(parameterValues[parameterValueIndex]).c_str(),
-1, SQLITE_TRANSIENT));
}

int stepResult = SQLITE_ROW;
while (stepResult != SQLITE_DONE)
{
stepResult = ValidateSQLiteResult(sqlite3_step(statement));
if (stepResult == SQLITE_ROW)
{
const int columnCount = sqlite3_column_count(statement);
ExecuteResultRow ^currentRow = ref new ExecuteResultRow();

for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex)
{
currentRow->Add(
reinterpret_cast<const char*>(sqlite3_column_text(statement,
columnIndex)), sqlite3_column_name(statement, columnIndex));
}

results->Append(currentRow);
reporter.report(currentRow);
}
}

ValidateSQLiteResult(sqlite3_finalize(statement));

return results;
});
}


У файлах ExecuteResultRow.* ми реалізуємо ExecuteResultRow ColumnEntry, які містять результати запитів до бази даних. Це необхідно для використання даних в WinRT і тут немає взаємодії з API SQLite. Найбільш цікава частина ExecuteResultRow — це те, як він користується методами Database::*ExecuteAsync.

JavaScript

У файлі default.js ми реалізуємо кілька методів, щоб спростити використання компонента WinRT в додатку JavaScript.

Функція runPromisesInSerial приймає масив об'єктів Promise і Ensure, які запускаються один за іншим, щоб полегшити запуск серій асинхронних команд ExecuteAsync.

function runPromisesInSerial(promiseFunctions) {
return promiseFunctions.reduce(function (promiseChain, nextPromiseFunction) {
return promiseChain.then(nextPromiseFunction);
},
WinJS.Promise.wrap());
}

Функція executeAsTransactionAsync відкриває транзакцію, виконує функцію, потім закриває транзакцію. Єдиний цікавий аспект у тому, що функція асинхронна, щоб завершити транзакцію нам необхідно дочекатися асинхронного виконання і отримати результат. Переконайтеся, що вона все ще повертає успішний результат або повертає значення помилки.

function executeAsTransactionAsync(database, workItemAsyncFunction) {
return database.executeAsync("BEGIN TRANSACTION").then(workItemAsyncFunction).then(
function (result) {
var successResult = result;
return database.executeAsync("COMMIT").then(function () {
return successResult;
});
},
function (error) {
var errorResult = error;
return database.executeAsync("COMMIT").then(function () {
throw errorResult;
});
});
}

ExecuteStatementsAsTransactionAsync bindAndExecuteStatementsAsTransactionasync об'єднують дві попередні функції, щоб полегшити роботу із запитами та результатами.

function executeStatementsAsTransactionAsync(database, statements) {
var executeStatementPromiseFunctions = statements.map(function statementToPromiseFunction(statement) {
return database.executeAsync.bind(database, statement);
});

return executeAsTransactionAsync(database, function () {
return runPromisesInSerial(executeStatementPromiseFunctions);
});
}

function bindAndExecuteStatementsAsTransactionasync(database, statementsAndParameters) {
var bindAndExecuteStatementPromiseFunctions = statementsAndParameters.map(
function (statementAndParameter){
return database.bindAndExecuteAsync.bind(database,
statementAndParameter.statement, statementAndParameter.parameters);
});

return executeAsTransactionAsync(database, function () {
return runPromisesInSerial(bindAndExecuteStatementPromiseFunctions);
});
}

Далі ви можете побачити, як ці функції використовуються для виконання запитів SQL, асинхронно і послідовно:

SQLite.Database.openDatabaseInFolderasync(
Windows.Storage.ApplicationData.current.roamingfolder, "BookDB.sqlite").then(
function (openedOrCreatedDatabase) {
database = openedOrCreatedDatabase;
return SdkSample.executeStatementsAsTransactionasync(database, [
"CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY UNIQUE, title TEXT, authorid INTEGER);",
"CREATE TABLE IF NOT EXISTS authors (id INTEGER PRIMARY KEY UNIQUE, name TEXT);",
"CREATE TABLE IF NOT EXISTS checkout (id INTEGER PRIMARY KEY UNIQUE, status INTEGER);"
]);
// ...

Перехід від IndexedDB до SQLite
Причиною переходу може бути те, що у вас існує на додаток Windows 8.1, яке використовує IndexedDB і ви хочете зробити з нього універсальне додаток. Щоб це реалізувати, вам знадобиться змінити свій код у бік використання обгортки WinRT SQLite замість IndexedDB.

На жаль, немає простої відповіді, що робити в цій ситуації. Для програми, описаного в прикладі, ми надаємо необроблені контракти SQL і використовуємо звичайні SQL таблиці, вимагають попередньої схеми і представляють асинхронне виконання з об'єктами Promise. IndexedDB, навпаки, читає і записує об'єкти JavaScript. Він орієнтований швидше на використання інструкцій SQL, і використовує Event об'єкти, на відміну від Promise.

Перетворений код в прикладі додатка сильно відрізняється від початкового прикладу IndexedDB. Якщо у вас є багато IndexedDB коду, ви можете написати вашу WinRT обгортку так, що її інтерфейс буде більше нагадувати IndexedDB. Сподіваємося, що код бази даних вашого додатки добре відокремлений від решти коду або його легко перетворити.

Додаткові матеріали

Бібліотека SQLite
Скачать SQLite для Windows Runtime
Приклад універсального додатка SQLite на JavaScript
Стаття про SQLite-WinRT
Навчальні курси віртуальної академії Microsoft (MVA)
Завантажити безкоштовну або пробну версію Visual Studio 2013

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.