Pull to refresh
0
Microsoft
Microsoft — мировой лидер в области ПО и ИТ-услуг

Использование SQLite в Windows и Windows Phone приложениях на JavaScript

Reading time 11 min
Views 12K
Original author: Dave Risney


Новым для 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++ > Store Apps > Universal 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
Tags:
Hubs:
+11
Comments 0
Comments Leave a comment

Articles

Information

Website
www.microsoft.com
Registered
Founded
Employees
Unknown
Location
США