DBRE - Руководство разработчика


/ Английский / Русский /

Оглавление:

1. Язык и компиляторы
2. Отношения классов
3. DB::Db
4. DB::Tr
5. DB::Query
6. Как добавить поддержку своей СУБД
7. DF::Df
8. PA::Params
9. RE::Replicator
10. RE::Reader
11. RE::Writer
12. MSG::Messager
13. EXC::Exception

Язык и кмопиляторы

DBRE написан на C++ с использованием продвинутых идиом, таких как: полиморфный интерфейс, pimpl, singleton, factory. В программе повсеместно используются shared_ptr и STL контейнеры. Все базовые классы имеют закрытые конструкторы, для создания объектов используются только фабричные методы.
Для сборки DBRE могут быть использованые любые современные компиляторы поддерживающие tr1. Для unix подобных систем мы рекомендуем gcc, для MS Windows - Visual C++ 2008. В качестве генератора makefile'ов используется automake.

Отношения классов


На рисунке показана декомпозиция системы. Классы
DB::Db, DB::Tr, DB::Query, RE::Reader и RE::Writer являются абстрактными базовыми классами. Они предоставляют только интерфейсы для своих сущностей. Классы DB::Db и DB::Tr кроме этого, инкапсулируют в себе физический интерфейс СУБД через указатель на классы DbImpl и TrImpl соответвенно.
После старта программы, создается объект PA::Params который читает параметры коммандной строки и файл конфигурации. Если эти операции прошли успешно, создается объект класса RE::Replicator которому передается указатель на объект класса PA::Params. В зависимости от настроек конфига создаются объекты (члены RE::Replicator) классов DB::Db, RE::Reader, RE::Writer и если надо DF::Df.
Если внимательно присмотреться к декомпозиции, может показаться излишнесть наличия класса DF::Df и реализаций DfReader и DfWriter, поскольку файл данных мог бы быть одним из вариантов DB::Db. Однако, тогда пришлось бы фактически написать свою файловую СУБД, что вобщем-то оказалось бы излишне сложным и недостаточно быстрым решением. Для сравнения запись в DF::Df производится примерно на порядок быстрее чем запись в СУБД, аналогично чтение из файла данных так-же на порядок быстрее чтения из СУБД. Это объясняется тем, что специализированный и относительно простой бинарный формат не является реляционной СУБД, в отличие от DB::Db которая обязана предоставлять стандартный sql интерфейс.
Исходя из вышесказанного мы остановились на приведенной структуре системы, как на наиболее простой и быстрой.
Более подробно отношения классов можно понять, рассмотрев их интерфейсы.

DB::Db

DB::Db - это абстрактный базовый класс, реализующий идиому pimpl - pointer to implementation. Он предоставляет обобщенный интерфейс для настройки подключения к СУБД и указатель pimpl для доступа объектов DB::Tr и DB::Query к физическому интерфейсу СУБД. Для создания объектов класса, как и для всех базовых классов в системе, нужно использовать фабричный метод make с типом СУБД в качестве простого символьного параметра. Фабричный метод класса Db использует фабрику DB::DbFactory которая является синглтоном. Так-же DB::Db содержит указатели на функции производящие наследников Tr и Query.
Рассмотрим определение класса Db:
namespace DB
{
	class Db;
	typedef std::tr1::shared_ptr DbPtr;
	
	class Db
	{
	public:
		void SetServer(const std::string &value) { server = value; }
		std::string GetServer() { return server; }
		void SetDatabase(const std::string &value) { database = value; }
		std::string GetDatabase() { return database; }
		void SetUser(const std::string &value) { user = value; }
		std::string GetUser() { return user; }
		void SetPassword(const std::string &value) { password = value; }
		std::string GetPassword() { return password; }
		void SetRole(const std::string &value) { role = value; }
		std::string GetRole() { return role; }
		void SetCharset(const std::string &value) { charset = value; }
		std::string GetCharset() { return charset; }
		virtual bool Connected() = 0;
		virtual void Connect() = 0;
		virtual void Disconnect() = 0;
		
		static DbPtr make(const std::string &);
		
		virtual ~Db() {}
		
	protected:
		std::string server, database, user, password, role, charset;
		bool connectdue;
		
		typedef std::tr1::shared_ptr ImplPtr;
		ImplPtr pimpl;
		void MakeImpl();
		Db() {}
	
	public:
		ImplPtr GetImpl() { return pimpl; }
		typedef Tr* (*CreateTrCallback)(DbPtr);
		typedef Query* (*CreateQueryCallback)(DbPtr, TrPtr);
		CreateTrCallback CreateTr;
		CreateQueryCallback CreateQuery;
	};
}
Пользовательский интерфейс очевиден, нам как разработчикам интересна реализация механизма pimpl, а также производство наследников. Pimpl реализован очень просто. Имеется класс (описанный в хидере db/dbimpl.h) DbImpl содержащий объекты БД (например IBPP::Database) для каждой СУБД:
namespace DB
{
	class DbImpl
	{
	public:

#ifdef USE_FB
		IBPP::Database fbdb;
#endif
#ifdef USE_MYSQL
		MYSQL *mysqldb;
#endif
		
		DbImpl() { ++guid; id = guid; }
		int GetId() { return id; }
	private:
		static int guid;
		int id;
	};
}
Конструктор класса задает уникальный id для объекта, этот id нужен для определения при создании DB::Query принадлежит ли объект транзакции объекту БД. Поскольку создание объектов физических БД в DbImpl происходит в члене Db Connect() введена переменная connectdue. Её нужно использовать в реализации Connected() и вызывать pimpl->mydb->Connected() только если она true. Для создания pimpl в Db, конструкторы наследников должны вызвать метод MakeImpl().
Производство наследников Tr и Query осуществляется при помощи указателей на функции CreateTr и CreateQuery.

DB::Tr

DB::Tr реализован аналогично DB::Db, так-же используется идиома pimpl и класс TrImpl. Определение класса:
namespace DB
{
	class Tr;
	std::tr1::shared_ptr TrPtr;
	
	class Tr
	{
	public:
		virtual void Start() = 0;
		virtual void Commit() = 0;
		virtual void Rollback() = 0;
		
		static TrPtr make(DbPtr);
		virtual ~Tr() {}
		
	protected:
		typedef std::tr1::shared_ptr ImplPtr;
		ImplPtr pimpl;
		void MakeImpl(const int&);
		Tr() {}
		
	public:
		ImplPtr GetImpl() { return pimpl; }
	};
}
Пользовательский интерфейс стандартен для sql транзакций. TrImpl реализован аналогично DbImpl, единственное отличие, он запоминает id объекта Db. Интересна реализация производящей функции (она использует указатель на функцию, член Db, CreateTr):
using namespace DB;
void CheckDBConnected(DbPtr db)
{
	if (!db->Connected())
	{
		throw EXC::exception("Database instance not connected.");
	}
}

TrPtr Tr::make(DbPtr db)
{
	CheckDBConnected(db);
	
	return TrPtr(db->CreateTr(db));
}

DB::Query

DB::Query - основной интерфейс к sql данным. Он обрабатывает sql запросы и позволяет получить доступ к результирующей выборке, а так-же используется для выполнения запросов update, insert и delete. В отличие от Db и Tr идиома pimpl здесь не используется, класс является обычным абстрактным базовым с невиртуальным интерфейсом. Наследники должны реализовать только виртуальные функции. Через pimpl Db и Tr имеется возможность получить доступ к физическому интерфейсу БД и транзакции. Поскольку все наследники должны реализовать определенное поведение, класс будет рассмотрен максимально подробно. Определение класса:
namespace DB
{
	struct Date
	{
		int day, month, year;
	};
	
	struct Time
	{
		int sec, min, hour;
	};
	
	struct DateTime
	{
		int day, month, year, sec, min, hour;;
	};
	
	enum FieldType
	{
		int_,
		float_,
		double_,
		string_,
		date_,
		time_,
		timestamp_,
		blob_
	};

	class Query;
	typedef std::tr1::shared_ptr QueryPtr;
	
	class Query
	{
	public:
		virtual void SetSQL(const std::string &sql) = 0;
		virtual void SetParam(const size_t ord, const int value) = 0; //ord - param order, start from 0
		virtual void SetParam(const size_t ord, const float &value) = 0;
		virtual void SetParam(const size_t ord, const double &value) = 0;
		virtual void SetParam(const size_t ord, const std::string &value) = 0;
		virtual void SetParam(const size_t ord, const Date &value) = 0;
		virtual void SetParam(const size_t ord, const Time &value) = 0;
		virtual void SetParam(const size_t ord, const DateTime &value) = 0;
		virtual void SetBlobParam(const size_t ord, const std::string &value) = 0;
		virtual void ExecSQL() = 0;
		virtual void Open() = 0;
		virtual void Close() = 0;
		virtual size_t GetFieldsCount() = 0;
		virtual std::string GetFieldName(const size_t field_n) = 0;
		virtual FieldType GetFieldType(const size_t field_n) = 0;
		virtual bool Fetch() = 0;
		virtual int GetIntFieldValue(const size_t field_n) = 0;
		virtual float GetFloatFieldValue(const size_t field_n) = 0;
		virtual double GetDoubleFieldValue(const size_t field_n) = 0;
		virtual std::string GetStrFieldValue(const size_t field_n) = 0;
		virtual Date GetDateFieldValue(const size_t field_n) = 0;
		virtual Time GetTimeFieldValue(const size_t field_n) = 0;
		virtual DateTime GetDateTimeFieldValue(const size_t field_n) = 0;
		virtual std::string GetBlobFieldValue(const size_t field_n) = 0;
		
		static QueryPtr make(DbPtr, TrPtr);
		virtual ~Query() {}
	
	protected:
		Query() {}
	};
}
Расмотрим подробнее каждую функцию член:
void SetSQL(const std::string &sql) - Устанавливает sql запрос. Запрос может быть параматерическим. Для обозначения параметра используется символ ?. Например: select * from table where id = ?
void SetParam(const int ord, const TYPE &value) - Устанавливает значение параметра с номером ord. Нумерация параметров начинается с нуля. Имеется 7 перегруженных функций для каждого поддерживаемого типа поля.
void SetBlobParam(const int ord, const std::string &value) - Аналогично, SetParam, но предназначена специально для blob полей.
void ExecSQL() - Запуск не возвращающего данных sql запроса.
void Open() - Запуск sql запроса, возвращающего данные.
void Close() - Закрытие набора данных и очистка кэша.
int GetFieldsCount() - Возвращает количество полей в выборке.
std::string GetFieldName(const int n) - Возвращает имя поля, по номеру в выборке. Нумерация с 0.
FieldType GetFieldType(const int n) - Возвращает тип поля
bool Fetch() - Перемещает "курсор" в выборке на одну позицию вниз и возвращает true если не достигнут конец выборки.
int GetIntFieldValue(const int n) - Возвращает значение поля типа int с номером n (нумерация с 0). Для остальных типов аналогично.
std::string GetStrFieldValue(const int n) - Возвращает значение поля в виде строки. Эта функция должна возвращать данные для ВСЕХ типов в преобразованном к string виду.

Реализация фабричного метода аналогична Tr::make:
using namespace DB;
QueryPtr Query::make(DbPtr db, TrPtr tr)
{
	CheckDBConnected(db);
	
	if (db->GetImpl()->GetId() != tr->GetImpl()->GetDbId())
	{
		throw EXC::exception("Transaction belongs to another object Db.");
	}
	
	return QueryPtr(db->CreateQuery(db, tr));
}
Как видно, здесь осуществляется проверка подключена ли БД и соответсвует ли транзакция указанному объекту БД.

Как добавить поддержку своей СУБД

1. Создать файлы mydb.h mydb.cpp myquery.h и myquery.cpp в директории db в которых написать определения и реализации наследников Db, Tr и Query. Сделать #include "db/mydb.h" и "db/myquery.h" в db.cpp
2. Добавить в DbImpl и TrImpl члены - указатели на физическую реализацию своей СУБД.
3. Cделать директивы препроцессора для возможности отключения сборки с вашей СУБД и #include в db/dbimpl.h, а так-же сгенерировать новый Makefile
4. Выбрать символьный идентификатор для фабрики DbFactory (например "mydb") (он же будет использоваться в файле параметров)
5. В реализации mydb.cpp сделать производителей для myDb, myTr и myQuery.
6. Зарегистрировать производителя myDb в фабрике DbFactory
7. В конструкторе myDb вызвать MakeImpl(); для создания pimpl и инициализировать CreateTr и CreateQuery указателями на производителей.
namespace
{
        DB::Db* CreateMyDb()
        {
                return new myDb;
        }

        DB::Tr* CreateMyTr(DB::DbPtr db)
        {
                return new myTr(db);
	}
	
	DB::Query* CreateMyQuery(DB::DbPtr db, DB::TrPtr tr)
	{
	        return new myQuery(db, tr);
	}
	
	const bool registered = DB::DbFactory::Instance().RegisterDb("mydb",  &CreateMyDb);
};

myDb::myDb()
{
        MakeImpl();
	
        id_type = "mydb";
	CreateTr = &CreateMyTr;
	CreateQuery = &CreateMyQuery;
}

DF::Df

Df - это класс работы с файлом данных. Данные в файле представлены в виде блоков, которые имеют следующую структуру:
tsize data
t - символ определяющий тип блока, t - таблица, f - поле, d - данные
size - размер данных блока. После размера всегда должен быть пробел
data - непосредственно данные блока. Для блоков с типом t и f в них содержится название таблицы либо поля, для блоков типа d - собственно данные.
Блоки следуют в файле следующим образом:
<таблица><поле><данные><поле><данные> <таблица><поле><данные><поле><данные>
Собственно класс DF::Df позволяет последовательно читать блоки данных из файла и последовательно их добавлять. Рассмотрим его интерфейс:
namespace DF
{
	class Df;
	struct DataBlock;
	typedef boost::shared_ptr DfPtr;
	typedef boost::shared_ptr DataBlockPtr;

	struct DataBlock
	{
	public:
		enum BlockType
		{
			t,
			f,
			d
		};
		BlockType type;
		std::string data;
		DataBlock() {}
	private:
		DataBlock(const DataBlock&) {}
		DataBlock& operator=(const DataBlock&) {}
	};
	
	enum IOMode
	{
		read,
		write
	};

	class Df
	{
	public:
		void WriteBlock(DataBlockPtr db);
		bool Fetch();
		DataBlockPtr ReadBlock();
		void Open(const IOMode &mode);
		void Close();
		
		static DfPtr make(const std::string &filename);
		virtual ~Df() {}
	};
}
Как видно для оперирования блоками данных имеется структура DF::DataBlock доступная только через shared_ptr.
Рассмотрим функции класса:
void Open(const IOMode &mode) - Открывает файл на чтение либо на запись. При открытии на запись, файл затирается.
void Close() - Закрывает файл. Если файл был открыт на запись, буферы сбрасываются на диск.
void WriteBlock(DataBlockPtr db) - Добавляет блок в конец файла.
bool Fetch() - Возвращает true если не достигнут конец файла. Курсор не перемещается.
DataBlockPtr ReadBlock() - Возвращает блок данных и перемещает курсор к следующему.

PA::Params

PA::Params - это класс инкапсулирующий всё необходимые для работы программы параметры. Он принимает параметры коммандной строки, а так-же читает XML конфиг. Для парсинга XML используется библиотека
pugixml.