SQLite — замечательная встраиваемая БД (часть 2)

    Часть 1
    Часть 3

    В этой части будут затронуты непростые вопросы использования SQLite через работу с его программным интерфейсом (API).



    Что такое SQLite API? Это набор функций sqlite3_XXX на языке C.

    Заголовки этих функций находятся в sqlite3.h, а описание API в виде одного большого HTML находится тут.

    Мы начнем использовать API, а позднее плавно перейдем к вопросам оборачивания его в уютный фреймворк.

    Попробуем создать небольшой проект на C++ (я делал в MS Visual Studio), который что-то делает с SQLite базой.

    Использовать SQLite в своем проекте можно двумя способами.

    «Вкомпилировать» код (если это C или C++). Или загружать и использовать sqlite3.dll (позволяет легко обновлять SQLite и не привязан к языку).

    В любом случае, надо создать новый проект (консольного) приложения (я использовал MS Visual Studio) и добавить в него вот такой main.cpp:

     
    #include <stdio.h>
    #include "sqlite3.h"
     
    const char* SQL = "CREATE TABLE IF NOT EXISTS foo(a,b,c); INSERT INTO FOO VALUES(1,2,3); INSERT INTO FOO SELECT * FROM FOO;";
     
    int main(int argc, char **argv){
     
    sqlite3 *db = 0; // хэндл объекта соединение к БД
    char *err = 0;
     
    // открываем соединение
    if( sqlite3_open("my_cosy_database.dblite", &db) )
    fprintf(stderr, "Ошибка открытия/создания БД: %s\n", sqlite3_errmsg(db));
    // выполняем SQL
    else if (sqlite3_exec(db, SQL, 0, 0, &err))
    {
    fprintf(stderr, "Ошибка SQL: %sn", err);
    sqlite3_free(err);
    }
    // закрываем соединение
    sqlite3_close(db);
    return 0;
    }
     


    Далее, если мы желаем собрать проект вместе с кодом SQLite, то надо:

    — скачать исходный код в виде amalgamation;
    — извлечь из него sqlite3.h и sqlite3.c и добавить их в проект.

    Если же мы хотим использовать sqlite3.dll, необходимо:

    — скачать SQLite DLL и распаковать;
    — выполнить «LIB.EXE /DEF:sqlite3.def» в папке куда распаковали для получения sqlite3.lib (убедитесь, что пути к lib.exe прописаны через вызов vcvars32.bat);
    — включить в проект sqlite3.lib;
    — скачать amalgamation и извлечь из него sqlite3.h;
    — включить в проект sqlite3.h;

    Компилируем, выполняем (во втором случае, DLL должна быть доступна для исполняемого файла).

    При первом запуске будет создан файл «my_cosy_database.dblite» с БД, в нем — одна таблица и две записи в ней.
    При последующих запусках — программа будет присоединяться к уже существующей БД и удваивать число записей в таблице.

    Давайте разбираться с кодом проекта.

    Использование SQLite предполагает, что мы хотим выполнять команды на языке SQL в какой-то БД (Ваш К.О.!).

    Можно представить работу с SQLite базой как работу с файлами в ОС Windows. Мы открываем файл и получаем _хэндл_ файла, к которому «привязан» некий системный объект (файл). Передавая этот хэндл в различные функции мы просим систему что-то сделать с файлом. Затем — закрываем файл. Также и в SQLite. Мы открываем файл с БД и получаем хэндл объекта «соединение к БД». Затем мы исполняем некие SQL команды через вызовы функций, получающих этот хэндл, и, в конце, закрываем соединение.

    Ничего оригинального, особенного или хитрого, как видим, тут нет.

    Расширение файла с БД SQLite не стандартизовано. Некоторые делают его ".sqlite3", но можно поставить любое.

    Функция

    int sqlite3_open(
      const char *filename,   /* Database filename (UTF-8) */
      sqlite3 **ppDb          /* OUT: SQLite db handle */
    );
     

    создает или открывает БД в указанном файле (UTF-8!) и заносит хендл соединения в db. Она возвращает 0 (успех) или код ошибки. Закрывающий вызов sqlite3_close нужен в любом случае (даже при ошибке sqlite3_open).

    Функция

    int sqlite3_exec(
      sqlite3*,                                  /* An open database */
      const char *sql,                           /* SQL to be evaluated (UTF-8) */
      int (*callback)(void*,int,char**,char**),  /* Callback function */
      void *,                                    /* 1st argument to callback */
      char **errmsg                              /* Error msg written here */
    );
     


    выполняет команду SQL (состояющую из одного или нескольких операторов SQL в UTF-8, разделенных ";") в контексте указанного (открытого) соединения к БД.

    Функция обратного вызова (callback) и custom аргумент к ней нужны, если делается SELECT (для извлечения данных). Об этом позже, пока не используем.

    В параметре errmsg можно получить текст ошибки и позднее очистить его через sqlite3_free. Если передать здесь NULL, то текста ошибки не получим.

    Как и sqlite3_open возвращается 0 или код ошибки.

    Команда «CREATE TABLE» содержит уточнение «IF NOT EXISTS». Это означает, что таблица создается, если ее — нет. Если есть, то ничего не происходит (и нет ошибки).

    Собственно, вот и все. Несмотря на то, что в реальных проектах не используются ни sqlite3_open (есть более мощный вызов sqlite3_open_v2), ни sqlite3_exec (обычно используется связка вызовов, компилирующих SQL, привязывающих параметры и пр.) это вполне рабочий проект.

    Он несет и еще одну важную миссию. Как уже было сказано при каждом запуске он удваивает кол-во строк в таблице foo. После 20 запусков в таблице будет ~2 млн записей.

    Т.е. 21-й запуск вставляет ~2 млн записей.

    На моей машине (Windows 7 x64, i5 2.8 Ghz, HDD, не SSD ) это заняло ~ 15 секунд. Пускай это убогий и синтетический тест, но он все-таки дает определенное представление о производительности SQLite.

    Продолжение следует.
    Поделиться публикацией

    Похожие публикации

    Комментарии 34

      0
      в первой части ссылку сюда добавьте плиз.
        0
        Ок, пока не получается. Хабр ошибку выдает припопытке отредактировать.
        0
        Объясните, как так получается, что я открываю базу из множества процессов следующим образом:
        sqlite3_open_v2(dbname, &dbconn, SQLITE_OPEN_READONLY, NULL);
        но всё равно получаю ошибки «database locked»?
          0
          sqlite.org/c3ref/open.html тут полное описание. База существует, если логика не подводит, то при открытии в RO база уже должна существовать, т.к. создание = запись => запрещено. Других причин не вижу.
            0
            Платформа?
              0
              Windows.
                0
                Ошибка db locked — это совсем не тоже, что и SQLITE_BUSY!

                Эта ошибка означает ошибку в приложении. Которая приводит к запутыванию вызовов Sqlite.

                Вот тут почитайте www.sqlite.org/cvstrac/wiki?p=DatabaseIsLocked
                  0
                  О! Это возможно. Пойду посмотрю возникает ли нечто подобное в коде.
                    0
                    Почитал коментарии в конце этого документа: у меня это тоже случается когда 'not set to do so'. Как 'set to do so', непонятно.
                      0
                      Если у вас несколько потоков используют одно и тоже соединение к SQLite — это нехорошо, но допустимо, если SQLite работает в режиме сериализации вызовов (по умолчанию — не работает).

                      Почитайте — тут.
                        0
                        Не несколько потоков, а несколько процессов. Насколько я понимаю, это не относится к тому, что написано в документе. Или относится?
                          0
                          Относится, но не в том смысле) Если лезет ошибка SQLITE_LOCKED, это означает неверную логику работы с соединением. Неважно сколько процессов.

                          То есть если логика неверна, то неважно сколько процессов — все равно вылезет этот SQLITE_LOCKED.
                            0
                            Я подозреваю, что это возникает в результате того, что я открываю два стейтмента и использую их одновременно. Может такое быть?
                              0
                              Может.
              0
              Реквестирую часть про работу с SQLite из Java.
              +3
              Используйте тег source для оформления кода — он красивее.
                0
                Кому-то может показаться интересным, а как именно там удваивается количество записей (SQL запрос не умещается в одну строку и обрезан).
                  0
                  INSERT INTO FOO SELECT * FROM FOO;
                  0
                  Также удобно работать с SQLite с помощью библиотеки POCO. Смотреть здесь pocoproject.org/docs/ POCO Data Library.
                    0
                    Приходилось ли Вам решать задачу загрузки sqlite-базы из буфера в памяти?

                    Знаю, что есть:
                    1. База :memory:
                    2. SQLite Backup API
                    Используя (2) можно загрузить базу из файла на диске в базу в оперативной памяти (1).

                    А вот как загрузить базу из массива char*? Пока приходится буфер писать во временный файл.

                    P.S. Пробовал spmemvfs. Но он от 2009 года, и пока у меня не получилось заставить его работать на последнем SQLite.
                      0
                      Мой фреймворк поддерживает репликацию таблиц, баз.

                      db1.CopyTo(db2)

                      Я затрону эти вопросы позже, если интересно.
                        0
                        Мне кажется, Вы не поняли вопроса. Реплицировать базу можно через SQLite Backup API, как я написал выше. Как получить db1 из буфера в памяти?
                          0
                          Посмотрите вот на это: spserver.googlecode.com/files/spmemvfs.tar.gz
                          В данном расширении реализована работа с БД которая при открытии целиком помещается в память, думаю если разобраться как там реализована VFS, то можно написать свое расширение, которое будет работать с базой, которая изначально была в памяти.
                            0
                            про spmemvfs я писал в своем сообщении
                            0
                            static void * load( void * arg, const char * path, int * len )
                            {
                            	//printf( "load ( %p, %s, %p )\n", arg, path, len );
                            
                            	char * ret = NULL;
                            	*len = 0;
                            
                            	FILE * fp = fopen( path, "r" );
                            	if( NULL != fp ) {
                            		struct stat filestat;
                            		if( 0 == stat( path, &filestat ) ) {
                            			*len = filestat.st_size;
                            			ret = (char*)sqlite3_malloc( filestat.st_size + 1 ); // <---!!!!! Смотрим сюда
                            			fread( ret, filestat.st_size, 1, fp ); // <-- потом сюда
                            			ret[ filestat.st_size ] = '\0';
                            		} else {
                            			printf( "cannot stat file %s\n", path );
                            		}
                            		fclose( fp );
                            	} else {
                            		printf( "cannot open file %s\n", path );
                            	}
                            
                            	return ret; // <--- и в итоге 
                            }
                            


                            А может даже и переписывать ничего не придется, только обточить напильником.
                              0
                              одной функцией load там дело не ограничивается
                          0
                          А как база оказалась внутри массива char?
                            0
                            Пришла по сети, например.
                              0
                              Мне кажется странной идея передавать базу по сети в формате файла. Если это действительно необходимо, то, пожалуй, через сохранение временный файл. Особенно, если база большая.

                              Если же базы небольшие, то может гнать через сеть, т.н. SQL дамп базы? И применять его к базе в памяти?..

                          0
                          > А вот как загрузить базу из массива char*?

                          Преобразовать их в insert'ы и выполнить через sqlite3_prepare2/sqlite3_step.
                            0
                            Это как? Под массивом подразумевается не массив значений (кортежей), а массив байт, представляющий из себя базу sqlite, как если бы мы прочитали ее из файла, например, через fopen, fread. Только в данном случае буфер приходит не из файла, а по сети например.
                              0
                              А, извините, я просто не понял вопрос. Я думал, что в массиве char* у вас исходные данные (значения полей в каком-то из традиционных текстовых форматов), а не бинарник файла БД. В этом случае тоже можно выкрутиться — формат известен www.sqlite.org/fileformat.html — но действительно намного проще будет просто записать массив в файл и открыть как файл штатным sqlite3_open.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое