Вам понадобится

Для разработки под Windows вам понадобиться следующее ПО:

Пробная версия Windows 10

Попробуйте новейшую версию ОС.

Visual Studio

Visual Studio — это интегрированная среда разработки с широкими возможностями для создания потрясающих приложений для Windows, Android и iOS, а также современных веб-приложений и облачных служб.

Microsoft .NET Framework 4.6

Пакет многоплатформенного нацеливания .NET Framework 4.6 позволяет разработчикам создавать приложения для .NET Framework 4.6, используя Visual Studio или сторонние IDE.

Использование 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++ > 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)
        {
//Параметры связки индексированы

            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. Надеемся, что код базы данных вашего приложения хорошо отделен от остального кода или его легко преобразовать.