База данных SQLite

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

Мы создадим пустую базу данных SQLite;

Создадим две пустые таблицы, с обозначенной структурой (две, просто для примера).

Реализуем функцию добавления данных;

Реализуем функцию проверки данных (пользователь пытается авторизоваться)

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

Как настроить проект и проверить его работоспособность рассказано вот тут:

часть1

часть2

Ну а теперь жесткий кодинг и только кодинг, конечно с комментариями и объяснением). На всякий случай, программируем на С++ из под Visual Studio 2015

Этап 1 — база данных SQLite — Создание

Как подключить нужные файлы уже рассказано, создаем указатели:

sqlite3 *db = 0;

Объявляем сразу после подключения:

#include "stdafx.h"
#include <stdio.h>
#include <string>
#include <wchar.h>
sqlite3 *db = 0;

Почему тут. Дело в том, что у нас есть большое желание, однажды получить хэндл, и дальше все время им пользоваться. В противном случае, нам придется объявлять его в каждой функции вновь.

Кроме того, добавим сюда еще одну строку:

sqlite3_stmt *stmtU

Указатель, которым мы будем пользоваться, вызывая запросы, да и прочие команды из группы sqlite.

Теперь в основной функции main, инициализируем и создадим нашу пустую базу данных.

//откртытие базы данных

 rc = sqlite3_open16(L»my_database.dblite», &db);

Обратите внимание, мы не используем команду open, мы используем более продвинутую версию open16. Не забываем, что перед именем базы, ставим литеру L

И так, как мы помним, если файла нет, он будет создан.

Этап 2 —  База данных SQLLite — создание таблиц

Для этого я решил создать отдельную функцию

void _sqlCreatePlayerTbale()
{
}

Функция ничего не возвращает, но вы можете сделать ее какой угодно. Признаюсь изначально функция возвращала результат выполнения, но после я отказался от этого.

В заголовках мы подключили заголовок  #include <wchar.h>

Это сделано не случайно, следующее что мы делаем, это объявляем переменную, в которую будем записывать запрос к базе данных.

const wchar_t* _SQLquery = L"CREATE TABLE IF NOT EXISTS 'Players'(num INTEGER PRIMARY KEY,name CHAR(30),pass CHAR(30));";

Я не случайно выбрал именно этот тип, так как команды sqlite работают именно с ним, такой вариант позволяет избежать лишних манипуляций с конвертацией.

Остановлюсь на запросе подробнее. Он явно отличается от описанного запроса из предыдущей статьи, хотя и выполняет идентичную функцию.

Сама команда CREATE TABLE IF NOT EXISTS не изменилась, но вот дальше, после именования создаваемой таблицы, идут именования полей и заметьте, указывается тип данных, используемых в полях. Кроме того, добавился тег PRIMARY KEY.

PRIMARY KEY — тег, позволяющий установить одно из полей как ключевое. Если вы не знакомы с устройством таблиц в базах, просто скажу, что в каждой таблице желательно иметь ключи, так сказать нумерацию строк. Так вот данный тег, позволяет автоматически формировать ключ строки, освобождая нас от лишнего кода. За что ему спасибо :) В итоге мы написали запрос, в котором первое поле имеет тип INTEGER и будет нести ключи, второе и последнее поле, имеет тип CHAR с длинной в 30 элементов. Так мы четко обозначили, какие данные куда следует грузить. Напомню, что поля = столбцы.

Запрос собрали, переменной присвоили, теперь начинаем формировать команду:

sqlite3_prepare16(db, _SQLquery, -1, &stmtU, 0);

Данная команда, как видите, берет указатель на базу, который мы назначили в функции main (первый плюс от выноса объявления за пределы функции), далее, мы взяли переменную с «вписанным» запросом.  четвертым параметром зацепиились за ранее объявленный stmtU. В целом, команда сформирована.

Теперь следует ряд операторов, которые будут всегда следовать при выполнении команды:

sqlite3_step(stmtU); //выполнить команду
sqlite3_reset(stmtU);//сбросить команду
sqlite3_finalize(stmtU);//очистить, завершить работу команды

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

Если все хорошо, команда будет выполнена, в базу данных добавится таблица с полями, после чего команда будет сброшена и очищена. После finalize, можно писать следующую команду и вновь запускать конвейер.

Что мы и сделаем, тут же в этой функции формируем новую команду на создание второй таблицы:

 

//создание второй таблицы, с параметрами игроков / номер, имя, жизни, скорость, сила

       _SQLquery = L"CREATE TABLE IF NOT EXISTS 'PlayerSkils'(num INTEGER PRIMARY KEY,name CHAR(30),life INTEGER, speed FLOAT, force FLOAT);";
      sqlite3_prepare16(db, _SQLquery, -1, &stmtс, 0);
       fprintf(stderr, "Error create table: %s\n", sqlite3_errmsg(db));
       //выполняем команду:
       sqlite3_step(stmtU);
       //сброс параметром команды (для реализации следующих запросов)
       sqlite3_reset(stmtU); 
       //завершение работы команды
       sqlite3_finalize(stmtU);

Выполнена вторая команда на создание второй таблицы. Правила создания не изменились, только расширился список полей и заметьте типов тоже стало больше.

Хочу дополнить еще одним моментом. команда prepare16,в отличии от exec  не поддерживает несколько запросов в одной строке. Вспомним, в прошлой статье, в переменной, куда мы записывали запрос, через знак «;» мы могли записать два и более запросов. Первым мы создавали таблицу, вторым мы заполняли ее данными. Мне не удалось с текущей командой проделать тоже самое. Думаю это не страшно. Процесс создания таблиц и их заполнения все равно являются независимыми вещами.

На этом процесс создания завершен. Понимаю, две таблицы ни о чем это не круто, но наша задача лишь демонстрация. Но и этого нам хватит для реализации авторизации пользователей.

Этап 3 База данных SQLite заполняем таблицы.

Я хочу показать вам две функции. В первой, мы реализуем простое добавление данных с параметрами, а во второй добавим проверку, на существование аналогов. Проверка требуется в первую очередь, для запрета регистрации пользователей с одинаковыми именами (login).

Вариант первый, регистрация пользователя (добавление данных в базу) без проверок на существование аналогов.

void CreateUser(char* nameU, char* passU)
{
}

И так, объявляем функцию, вводим две переменные типа char. Как видно из названия, одна символизирует логин, вторая пароль.

Эти данные вы можете передавать откуда угодно, но думаю целесообразно передавать их из редактируемых полей, где пользователь сможет их указать.

const wchar_t* _SQLquery = L"INSERT INTO 'Players'(name,pass) VALUES(?,?);";

Далее, мы описываем наш запрос. Команда INSERT INTO отвечает за добавление данных в таблицу, в данном случае Players. Далее мы указываем какие поля мы заполняем, после знакомый оператор VALUE. А в скобках новшество — знаки вопроса.

Не углубляясь далеко, такой оборот позволяет вставлять нам любые значения в уже написанный запрос. Но для этого требуется новая команда. Прежде чем ее введем, сформируем команду запроса:

sqlite3_prepare16(db, _SQLquery, -1, &stmtU, 0);

А теперь нововведение:

sqlite3_bind_text(stmtU, 1, nameU, -1, 0 );
sqlite3_bind_text(stmtU, 2, passU, -1, 0 );

Краткое описание: Команда sqlite3_bind_text позволяет нам, заменить ранее примененный знак вопроса, конкретным значением в данном случае nameU — переменной, которая прописана в функции. А вот цифра, следующая за переменной, указывает, какой по счету вопрос в запросы мы подменяем. В нашем запросе два вопроса и команд на подмену тоже две. Первая как видно подменяет первый вопрос, а вторая следовательно второй. Так мы сформировали запрос, добавив туда «внешние данные».

Немного остановлюсь, для разных типов данных, разные команды.Вот так выглядит добавление значения типа INT:

sqlite3_bind_int( stmt, 1, number ); или sqlite3_bind_int( stmt, 1, 456);

Ну а дальше знакомые строки:

sqlite3_step(stmtU);
 sqlite3_reset(stmtU);
 sqlite3_finalize(stmtU);

Для порядка, в этой же функции реализуем дополнение данных в таблицу с параметрами:

_SQLquery = L"INSERT INTO 'PlayerSkils'(name,life, speed, force) VALUES(?, 100, 500, 750);";
       rc = sqlite3_prepare16(db, _SQLquery, -1, &stmtU, 0);
       sqlite3_bind_text(stmtU, 1, nameU, -1, 0);
       fprintf(stderr, "error create data: %s\n", sqlite3_errmsg(db));
       //выполняем команду:
       sqlite3_step(stmtU);
       //сброс параметром команды (для реализации следующих запросов)
       sqlite3_reset(stmtU);
       sqlite3_finalize(stmtU);

Тот же принцип.

Отлично, теперь пользователи могут смело плодиться в нашей базе, но есть проблема, нет проверки на одинаковые логины. Для этого, мы можем реализовать туже функцию, но с дополнением. Но мы сделаем иначе. Мы напишем новую, которая будет отвечать именно за проверку данных и в случае успеха, будет отдавать управление ранее написанной функции.

void _sqlCreateUserWH(char*nameU,char*passU)
{
}

Пишем запрос:

const wchar_t* _SQLquery = L"SELECT * FROM 'Players'where name=?;";

На этот раз, это запрос на выборку данных. Мы пройдемся по таблице пользователей, с целью найти запись, удовлетворяющую условию. Условие описывается командой where= после чего указываем поле, и значение этого поля.

Вновь ставим вопрос, формируем команду:

sqlite3_prepare16(db, _SQLquery, -1, &stmtU, 0);

Дополняем запрос данными:

 sqlite3_bind_text(stmtU, 1, nameU, -1, 0);

Вводим две переменные:

wchar_t* promName; //Сюда мы запишем временное значение полученное из базы

bool nameCTR = true;//тут мы введем флаг успешности операции

И запустим цикл, который позволит пройтись по всей таблице

while (sqlite3_step(stmtU) == SQLITE_ROW)
       {           
             promName = (wchar_t*)sqlite3_column_text(stmtU, 1);//тут мы присваиваем временное значение
             fprintf(stderr, "LINE select query: %10s%s \n", promName, "|");//Можно не надо
             if (promName != NULL)//Если найдено значение удовлетворяющее условию, переменная станет не NULL
             {
                    fprintf(stderr, "LINE #: %s%s \n", promName, "== NAMEUSER!");//Пишем что такой пользователь уже есть
                    nameCTR = false;//флаг успешности в ложь
                    break;//прерываем цикл.
             }
       } 
sqlite3_reset(stmtU);
sqlite3_finalize(stmtU);

И реакция на результат, если флаг остался правдой:

if (nameCTR)
       {
             //добавить перса
             CreateUser(nameU, passU);//Передали управление функции, которая создает данные пользователя в базе данных SQLite

             fprintf(stderr, "LINE #: %s \n", "add user!"); //Сообщили что все хорошо
       }

Вот и все, простое решение, для весьма важной задачи.

Теперь мы имеем базу данных с уникальными именами пользователей.

Добавив пользователя, следует применить данные.

Этап 4 Проверка логина пароля при авторизации в игре.

Пишем новую функцию

int correctUser(char* nameU, char* passU)

{

}

На этот раз, функция будет возвращать нам числовое значение. Таким образом мы организуем собственную обратную связь, добавив варианты ошибок, разбитых по номерам. Конечно можно не делать этого, но думаю будет хорошо, когда пользователю будет сообщение о неправильном логине/ пароле.

Я не буду подробно останавливаться на описании, так как функция собрала в себе все, что мы только что реализовали. Думаю беглого взгляда будет вам достаточно, для понимания принципа работы:

const wchar_t* _SQLquery = L"SELECT * FROM 'Players'where name=? AND pass=?;";
      sqlite3_prepare16(db, _SQLquery, -1, &stmtU, 0);
       sqlite3_bind_text(stmtU, 1, nameU, -1, 0);
       sqlite3_bind_text(stmtU, 2, passU, -1, 0);
       wchar_t* promName = NULL;      
       while (sqlite3_step(stmtU) == SQLITE_ROW)
       {
             promName = (wchar_t*)sqlite3_column_text(stmtU, 1);    
             if (promName != NULL)
             {
                    fprintf(stderr, "LINE #: %s%s \n", promName, "== USER LOGIN PASSWORD OK!");
                    //сброс параметром команды (для реализации следующих запросов)
                    sqlite3_reset(stmtU);
                    sqlite3_finalize(stmtU);
                    return 0;
             }
       }
       //сброс параметром команды (для реализации следующих запросов)
       sqlite3_reset(stmtU);
       sqlite3_finalize(stmtU);
       fprintf(stderr, "LINE #: %s%s \n", promName, "== ERROR USER LOGIN OR PASSWORD!");
       return 1; //ошибка!

Возвращенное значение скажет программе о результате. И мы сможем отобразить результат.

Осталось теперь вызвать нужные функции (из окна регистрации, или ввода учетных данных при входе в игру, и радоваться. Но так как я реализовывал конструкцию в консоле, то я просто вызову нужную функцию и увижу результат.

Но чуть не забыл главное, по завершению работы, требуется закрыть соединение с базой, по этому в main после вызова функций я написал:

sqlite3_close(db);

Этап 5 отображение данных из таблицы пользователей

Но между делом, мне было интересно наблюдать за процессом добавления данных, и прямо в теле main я написал набор команд, позволяющий мне более или менее отображать таблицу с игроками:

//тестовая проверка проверяем поля обоих таблиц:
             int rc=0;
             rc = sqlite3_prepare16(db, L"SELECT * FROM 'Players'", -1, &stmt, 0);
             //sqlite3_step(stmt);

             int dat=-1;  
             wchar_t *buf;
             wchar_t *buf2;
             while (sqlite3_step(stmt) == SQLITE_ROW)
             {
                    dat = (int)sqlite3_column_int(stmt, 0);
                    buf = (wchar_t*)sqlite3_column_text(stmt, 1);
                    buf2 = (wchar_t*)sqlite3_column_text(stmt, 2);
                    fprintf(stderr, "LINE #: %5d%s%10s%s%10s%s \n", dat,"|",buf,"|",buf2,"|");
             }            
             sqlite3_reset(stmt);           
             sqlite3_finalize(stmt);

 

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

Вот примерный вид:

Отображение данных таблицы в консоле

Отображение данных таблицы в консоле

Скриншот ранний, когда в базу данных можно было вводить одинаковые записи

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

Не относитесь слишком критично к примерам и описаниям, это не истина в последней инстанции, это один из вариантов реализации. На самом деле, база данных SQLite, имеет много возможностей, но я думаю, можно уже обратиться к документации и попробовать самостоятельно разобраться.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Проверка * Лимит времени истёк. Пожалуйста, перезагрузите CAPTCHA.