Всем привет, данная публикация будет посвящена работе с встраиваемой реляционной базой данных SQLite в Unity. Данная статья написана новичком для новичков с целью показания работы с SQLite, предполагается, что вы знаете основы SQL. Так как в интернете нет ясного тутора для новичков, я решил занять эту нишу. В данной статье мы напишем простенький класс для работы с данной СУБД, который можно использовать для решения широкого круга задач (локализация, сохранение данных, ведение разных таблиц).

Что такое SQLite и зачем она нам нужна?


SQLite – компактная встраиваемая реляционная СУБД, которая является довольно таки популярной. Важный плюс SQLite – это кроссплатформенность, по этому мы можем использовать SQLite для различных платформ. SQLite можно использовать когда нужна скорость и компактность, по этому, при возникновении проблемы хранения данных я надумал решить её использованием данной СУБД.

Как работать с SQLite?


Для создания и редактирование нашей БД есть большое количество бесплатных утилит и плагинов для браузеров, лично я буду использовать DB Browser (SQLite), меня он зацепил своей простотой, а работа с различными плагинами в браузере, мне показалась не очень удобной. В общем, кто как хочет, так и работает. Использую DB Browser можно спокойно создать таблицы, сделать между ними связи и заполнить их данными не прибегая к использованию SQL. Так же, в DB Browser вы можете делать всё ручками с помощью SQLite, так что, тут уже кому как удобнее.

Создание и заполнение тестовой БД


Создаём базу данных в Assets/StreamingAssets нашего проекта (у меня это db.bytes, так как Unity понимает только *.bytes для баз данных мы будем использовать именно это расширение). Чисто для примера я создал такую БД со следующими таблицами:

1) Таблица «Player», которая описывает сущность игрока:

CREATE TABLE "Player" (
	"id_player" INTEGER NOT NULL,
	"nickname" TEXT NOT NULL,
	PRIMARY KEY("id_player")
);

Заполнил её следующими данными:



2) Таблица «Scores», которая введена для повышения уровня нормализации БД

CREATE TABLE "Scores" (
	"id"	INTEGER NOT NULL,
	"id_player" INTEGER NOT NULL,
	"score" INTEGER NOT NULL,
	PRIMARY KEY("id"),
	FOREIGN KEY("id_player") REFERENCES "Player"("id_player")
);

Заполнил её следующими данными:



Подключение библиотек


Создаём базу данных в Assets/StreamingAssets нашего проекта (у меня это db.bytes), далее нам нужно подключить библиотеки для работы с этой БД. Качаем файлик sqlite3.dll с официального сайта для работы с SQLite в Windows. Что б подружить данную СКБД с Android у меня ушло пару дней, так как библиотека указанная в данной статье оказалась не рабочей, лично у меня не вышло с ней работать на Android, постоянно лезли ошибки, по этому заливаю найденную где-то в просторах интернета эту версию библиотеки для Android. Размещаем библиотеки здесь — Assets/Plugins/sqlite.dll и Assets/Plugins/Android/sqlite.so.

После всех этих манипуляций копируем System.Data.dll и Mono.Data.Sqlite.dll с C:\Program Files (x86)\Unity \Editor\Data\Mono\lib\mono\2.0 и вставляем Assets/Plugins вашего Unity проекта. Хочу заметить, что в 2018 версии Unity может писать что System.Data.dll уже подключен и происходит конфликт двух одинаковых файлов. Собственно, решается это просто, не удаляем только что вставленный System.Data.dll.

Структура библиотек должна быть такая:

Assets/Plugins/Mono.Data.Sqlite.dll – просто надо :)
Assets/Plugins/System.Data.dll – аналогичная причина
Assets/Plugins/sqlite3.dll – для работы с SQLite на Windows
Assets/Plugins/Android/libsqlite3.so – для работы с SQLite на Android

Написание скрипта для работы с БД


И наконец то мы можем приступить к написанию скрипта для работы с созданной БД. Для начала, создадим файл MyDataBase и подключим библиотеки System.Data, Mono.Data.Sqlite, System.IO, сделаем класс MyDataBase статическим и, естественно, уберём наследование от MonoBehaviour. Добавим 3 приватные переменные и константу с названием файла БД. У нас должно выйти, что-то такое:

using UnityEngine;
using System.Data;
using Mono.Data.Sqlite;
using System.IO;

static class MyDataBase
{
    private const string fileName = "db.bytes";
    private static string DBPath;
    private static SqliteConnection connection;
    private static SqliteCommand command;
}

Это всё конечно хорошо, но всё же работать с БД мы не сможем. Для работы с БД мы должны получить путь к ней, предлагаю сделать статический конструктор, который как раз и будет получать путь к БД (Напомню, что БД лежит в StreamingAssets).

static MyDataBase()
{
    DBPath = GetDatabasePath();
}

/// <summary> Возвращает путь к БД. Если её нет в нужной папке на Андроиде, то копирует её с исходного apk файла. </summary>
private static string GetDatabasePath()
{
#if UNITY_EDITOR
    return Path.Combine(Application.streamingAssetsPath, fileName);
#if UNITY_STANDALONE
    string filePath = Path.Combine(Application.dataPath, fileName);
    if(!File.Exists(filePath)) UnpackDatabase(filePath);
    return filePath;
#elif UNITY_ANDROID
    string filePath = Path.Combine(Application.persistentDataPath, fileName);
    if(!File.Exists(filePath)) UnpackDatabase(filePath);
    return filePath;
#endif
}

/// <summary> Распаковывает базу данных в указанный путь. </summary>
/// <param name="toPath"> Путь в который нужно распаковать базу данных. </param>
private static void UnpackDatabase(string toPath)
{
    string fromPath = Path.Combine(Application.streamingAssetsPath, fileName);

    WWW reader = new WWW(fromPath);
    while (!reader.isDone) { }

    File.WriteAllBytes(toPath, reader.bytes);
}

Примечание. Нам нужно распаковывать БД в указанные пути (Application.dataPath/db.bytes для Windows и Application.persistentDataPath/db.bytes для Android) так как папка StreamingAssets, после сборки, имеет атрибут ReadOnly (кроме Android) и мы не сможем записывать что-то в БД. Собственно, для того, что б можно было записывать что либо в БД, мы и распаковываем нашу базу данных. Подробно сказано какие пути, под какую платформу нужно использовать в этой статье.

Напишем методы открытия подключения и закрытия, а так же метод, который будет выполнять запрос, который не требует возврата значений, допустим, INSERT, UPDATE, CREATE, DELETE, DROP.

/// <summary> Этот метод открывает подключение к БД. </summary>
private static void OpenConnection()
{
    connection = new SqliteConnection("Data Source=" + DBPath);
    command = new SqliteCommand(connection);
    connection.Open();
}

/// <summary> Этот метод закрывает подключение к БД. </summary>
public static void CloseConnection()
{
    connection.Close();
    command.Dispose();
}

/// <summary> Этот метод выполняет запрос query. </summary>
/// <param name="query"> Собственно запрос. </param>
public static void ExecuteQueryWithoutAnswer(string query)
{
    OpenConnection();
    command.CommandText = query;
    command.ExecuteNonQuery();
    CloseConnection();
}

Чудесно, теперь наш скрипт может выполнять запросы на модификацию данных. Но как же быть с очень важным SELECT? Я решил, что возвращаемое значение метода, который должен выполнять запрос на выборку данных, должен иметь тип DataTable или же string, если требуется получить 1 значение. Для этого напишем 2 метода:

/// <summary> Этот метод выполняет запрос query и возвращает ответ запроса. </summary>
/// <param name="query"> Собственно запрос. </param>
/// <returns> Возвращает значение 1 строки 1 столбца, если оно имеется. </returns>
public static string ExecuteQueryWithAnswer(string query)
{
    OpenConnection();
    command.CommandText = query;
    var answer = command.ExecuteScalar();
    CloseConnection();

    if (answer != null) return answer.ToString();
    else return null;
}

/// <summary> Этот метод возвращает таблицу, которая является результатом выборки запроса query. </summary>
/// <param name="query"> Собственно запрос. </param>
public static DataTable GetTable(string query)
{
    OpenConnection();

    SqliteDataAdapter adapter = new SqliteDataAdapter(query, connection);

    DataSet DS = new DataSet();
    adapter.Fill(DS);
    adapter.Dispose();

    CloseConnection();

    return DS.Tables[0];
}

Готово, теперь у нас есть простой скрипт, который может делать запросы на модификацию и выборку данных. Давайте сейчас напишем скрипт ScoreManager. Который будет получать таблицу лучших результатов отсортированных по убыванию. И, для проверки, отобразим в Debug.Log ник лидера и его очки.

using System.Collections;
using System.Collections.Generic;
using System.Data;
using UnityEngine;

public class ScoreManager : MonoBehaviour
{
    private void Start()
    {
        // Получаем отсортированную таблицу лидеров
        DataTable scoreboard = MyDataBase.GetTable("SELECT * FROM Scores ORDER BY score DESC;");
        // Получаем id лучшего игрока
        int idBestPlayer = int.Parse(scoreboard.Rows[0][1].ToString());
        // Получаем ник лучшего игрока
        string nickname = MyDataBase.ExecuteQueryWithAnswer($"SELECT nickname FROM Player WHERE id_player = {idBestPlayer};");
        Debug.Log($"Лучший игрок {nickname} набрал {scoreboard.Rows[0][2].ToString()} очков.");
    }
}

Вот что получаем при запуске:



Спасибо за внимание, с удовольствием приму конструктивную критику.

Комментарии (6)


  1. MrMureno
    07.03.2019 19:18
    +1

    простите, но мало чем отличается от статьи https://habr.com/ru/post/181239/ (хотя может я что упустил бегло проглядывая)


    плюс вы пропустили важный момент (из старой доброй статьи) — права на чтение только, если говорить не только про андроид, а про кроссплатформенность.
    Так что папка стриминг ассетов — хороша для чтения, а вот если что-то записывать хотим — лучше так не хранить. (у вас оно и не хранится, а заготовка базы данных распаковывается в другое место, просто как то пропущен этот момент в пояснениях, а это довольно важно, как по мне)


    1. Leopotam
      07.03.2019 21:24

      Ну и если уж говорить про кроссплатформенность — автор может ради интереса попробовать переключить платформу на iOS и удивиться ошибкам компиляции.


    1. best_programmer Автор
      07.03.2019 21:36

      Просмотрел я статью, указанную вами, в ней упор больше на работу с БД через Linq. После прочтения этой статьи нужно будет ещё гуглить как работать с той библиотекой более детально, например те же виды атрибутов в этой библиотеки, в статье ничего не сказано про них. В статье показан пример кода и классов, которых мало для нормального понимания (хотя б потому что не показано как устанавливать внешние ключи). В статье максимум теории что, куда, зачем.

      Моя статья ориентирована, что б человек, знающий основы SQL, мог работать с SQLite сразу же после прочтения этой статьи, без необходимости дополнительной гуглёжки. И в моей статье упор на код.

      К отличиям статей, можно ещё отнести, что у меня описана ошибка про System.Data, по своему опыту могу сказать, что даже такие ошибки могут потратить пару часов у новичков. И в той статье, указана ссылка на нерабочую библиотеку для работы с Android, лично у меня не вышло подружить ту библиотеку, а библиотека на оф. сайте SQLite уже формата *.arm для AndroidStudio и в Unity не работает.

      Я считаю, что Ваше замечание по поводу отсутствие пояснения с распаковкой БД достаточно важно, по этому сейчас это поправлю.
      Спасибо большое Вам.)


      1. MrMureno
        07.03.2019 22:30

        на офф саите sqlite-android-3270200.aar
        а формат aar — давно юнити переваривает адекватно.
        Хотя надо конечно пробовать подставить, может и вправду намудрили что-то внутри, но маловероятно.


        1. best_programmer Автор
          08.03.2019 10:18

          Сейчас проверил всё, если я ничего не упустил, для работы с *.aar файлами в Unity нужно использовать AndroidJavaClass. Т.е., если работать с *.aar файлом SQLite придётся переписывать полностью скрипт работы с БД под Android, что слегка усложняет разработку всего приложения в целом. А при использовании моего *.so файла, ничего переписывать не нужно и код на этих двух платформах будет абсолютно одинаковым, за исключением путей.


  1. p4p
    08.03.2019 17:29

    Только sqilte и использую. Удобно выстраивать логику. Но как заметили некоторые комментаторы, есть подводные камни с правами доступа и проблемы на некоторых китайцах. Допустим игрок может изменить место храните по умолчанию уже после установки игры и база будет недоступна.