Простой пример использования Udb

  !   Данная информация предназначена только только для IT-специалистов по системной интеграции модулей БИОСОФТ-М. (см. Руководства пользователя к программным продуктам)

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

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

Данный пример не демонстрирует типовую ООП инкапсуляцию публичного доступа к приватной БД (Iface/Impl). Такая инкапсуляция в большинстве случаев необходима на практике. Подразумевается что БД используется только локально в рамках пакета. Это непример на правильную структуру кода, а в большей степени туториал по операторам и шаблонным алгоритмам.

Весь код примера можно найти в Absform/Relademo, где он прописан в одном файле только для демонстрационных целей. (Любая реальная декларация простейшей БД в приложении потребует как минимум трех С++ классов.)

Постановка демо задачи

Предположим требуется БД со списком пациентов, каждый из которых обладает атрибутами "Имя" (ФИО), "Дата рождения", "Пол". Нужно добавлять пациентов в БД, задавать их атрибуты, получать полный список пациентов или отфильтрованный по заданному значению атрибута "Пол".

Классы реляционных таблиц

Каждая реляционная таблица должна быть представлена своим CDbXxxx классом, унаследованным от dbobject. Они располагаются в поддиректории /db в пакете и приватны в нем (не могут иметь окончаний Iface, Impl, Gp).

CDb классы декларируют таблицы

Для хранения пациента создаем класс с такими тремя пропертями:

class CDbDemoPatient : public dbobject
{
public:
 
    // "Smith John"
    str x_sDemoPatientName = "",
            auto(Get, Set);
 
    // demo time storage
    moment x_momentDemoPatientBirthDate = moment(),
            auto(Get, Set);
 
    // gender as type<>
    type<CPexApiSexTypeIfaceGp> x_typeDemoPatientSex,
            auto(Get, Set);
};

Все записи/объекты в БД должны иметь определенный контейнер, и каждая БД должна содержать одну особую корневую "Root" таблицу с единственной singleton записью, которая является контейнером на самой вершине иерархии.

В нашем случае все прикладные объекты CDbDemoPatient будем регистрировать в одном списке dblist в корневой записи:

class CDbDemoRoot : public dbobject
{
public:
 
    // Patients
    dblist<CDbDemoPatient> _x_dblistDemoPatient,
            auto(Get);
};

Добавление пациентов в БД выполняется только как добавление элементов списка _x_dblistDemoPatient и запросы к БД делаются тоже в контексте этого списка.

Соединение с БД

Контекст соединения с БД любого типа поддерживается объектом ref<CUdbConnectionIfaceGp>. Инкапсулятор хранилища должен хранить этот объект как проперти в течении всего сеанса работы с БД:

// Attributes
 
    // UdbConnection
    ref<CUdbConnectionIfaceGp> x_rDemoUdbConnection,
            auto(Get);
Декларация таблиц в БД

Перед инициализацией соединения нужно подготовить список всех таблиц БД на основании наших CDb классов:

    ref<CUdbConnectionIfaceGp> rUdbConnection =
        x_rDemoUdbConnection;
 
    // Declate our table classes
    rUdbConnection->DeclareUdbTableClass(ref<CDbDemoRoot>());
    rUdbConnection->DeclareUdbTableClass(ref<CDbDemoPatient>());

Не забываем указывать коннекшену все нужные классы! Сами они туда не залезут. Будет рантайм-еррор если пытаться использовать в контексте коннекшена CDb классы к нему не отнесенные (хотя Udb могла бы автоматически причислять упомянутые classinfo к лику Реляционных Таблиц но не делает этого из принципа).

Соединение с СУБД

В простейшем случае для инициализации локальной БД задаем тип драйвера, путь к файлам БД и получаем статус ошибки:

    // Init database connection
    str sError;
    rUdbConnection->
        OpenUdbConnection(
            typeUdbStorageType,
            sys::GGetTempFolder().GetAppendedPath("Demo.udb"),
            out sError);
    HandleError(sError);

OpenUdbConnection откроет существующую БД если она есть, добавит в нее недостающие декларированные таблицы и обновит список колонок на основании проперти CDb классов. Либо создаст новую БД если ее не существует. (http://www.biosoft-m.ru/19334)

Ссылка на Root запись

Для добавления новых записей в БД нам обязательно нужна будет ссылка на корневой контейнер которую нужно проинициализировать после открытия соединения с БД и сохранить вместе с rUdbConnection как проперти инкапсулятора хранилища:

// Attributes
 
    ...
 
    // Root record
    ref<CDbDemoRoot> x_rDbDemoRoot,
            auto(Get);

и после rUdbConnection->OpenUdbConnection():

    // Init root record
    this->x_rDbDemoRoot =
        dbref<CDbDemoRoot>().
            InitRoot(
                rUdbConnection);

Только один объект CDbDemoRoot может существовать как единственная запись в таблице которая будет создана InitRoot() при первом соединении с новой БД.

Добавление записей

dblist::AddNewRow() выполняет роль SQL INSERT и в отличие от SQL требует указания не только таблицы, в которую добавляется запись но и ссылки на контейнер к которому эта запись логически относится.

В нашем простом примере один уровень иерархии с контейнером x_rDbDemoRoot->_x_dblistDemoPatient. Добавим пару тестовых пациентов с разными атрибутами:

    // Add patients
    {
        ref<CDbDemoPatient> rPatient;
 
        rPatient->x_sDemoPatientName =
            "Sheen Charlie";
        rPatient->x_momentDemoPatientBirthDate =
            moment::GGetLocal();
 
        x_rDbDemoRoot->
            _x_dblistDemoPatient.Get().
                AddNewRow(
                    rPatient);
    }
    {
        ref<CDbDemoPatient> rPatient;
 
        rPatient->x_sDemoPatientName =
            "Handler Chelsea";
        rPatient->x_typeDemoPatientSex =
            type<CPexApiSexTypeIfaceGp>()->
                AsPexApiSexTypeAsFemale();
 
        x_rDbDemoRoot->
            _x_dblistDemoPatient.Get().
                AddNewRow(
                    rPatient);
    }

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

Список всех записей

Каждый запрос к БД должен быть инкапсулирован отдельным методом QueryXxxx() соответствующим типовому шаблону, описанному ниже.

Во избежание дублирования кода каждый метод QueryXxxx() должен уметь и возвращать нам список объектов, соответствующих запросу, и подсчитывать их количество и выбирать один из объектов по первичному ключу. Причем все это должно быть представлено в форме, совместимой со стандартными итераторами, удобной для реализации гибкой инкапсуляции записей в виде Iface/Impl объектов для публичных интерфейсов и исключающей не инкапсулированную циркуляцию массивов ("коллекций").

Рассмотрим реализацию простейшего запроса не фильтрующего пациентов и разберем ее детально:

queryorcount CAbsformRelademoImpl::QueryPatients(
        out iter& out_i,
        out ref<CDbDemoPatient>& out_rPatient,
        key keyPrimary)
{
    ref<CQueryIfaceGp> rQuery =
        out_i.AdvanceQuery();
 
    from<CDbDemoPatient> fromPatient(rQuery);
 
    if (rQuery->Init())
    {
        fromPatient.
            BeginFrom(
                x_rDbDemoRoot->
                    _x_dblistDemoPatient,
                keyPrimary);
 
        //rQuery->x_qxboolWhere = ...; no filter, get all
    }
 
    if (rQuery->Next())
    {
        out_rPatient = fromPatient.FetchRef();
    }
 
    return rQuery;
}

В нашем простом примере QueryPatients() предназначен только для приватного использования в рамках пакета хранилища, возвращает приватный ref<CDbDemoPatient>. Однако шаблон его реализации позволяет легко усовершенствовать его для поддержки более сложной инкапсуляции доступа к БД.

Универсальное использование метода queryorcount

Мы лекларировали метод, способный работать в трех эпостасях: как Iterate, как GetCount, и как LookupByKey:

queryorcount CAbsformRelademoImpl::QueryPatients(
        out iter& out_i,
        out ref<CDbDemoPatient>& out_rPatient,
        key keyPrimary)

1. Мы можем использовать его как обычный итератор для перебора результатов запроса:

    ref<CDbDemoPatient> rIterPatient;
    for (
        iter i;
        QueryPatients(
            out i,
            out rIterPatient,
            key());)
    {
        TESTLOG(
            "",
            ""
                "Patient: " + Str(rIterPatient->GetKey()) + ", "
                "\"" +
                    rIterPatient->x_sDemoPatientName +
                    "\", " +
                rIterPatient->x_momentDemoPatientBirthDate.Get().
                    ToStr("yyyy-mm-dd") +
                    ", " +
                rIterPatient->x_typeDemoPatientSex.Get()->
                    GetDicomSexIdOr("?") +
                    ", "
                "\n");
    }

таким образом избавляя клиента от каких бы то ни было реляционностей, предоставляя привычные объекты как если бы итерация происходила по array<>.

2. А можем использовать этот же метод как GetCount() для получения SELECT COUNT(*) в форме:

    int nPatients =
        QueryPatients(
            out_COUNT(),
            out_IGNORE(ref<CDbDemoPatient>),
            key());

Здесь out_COUNT() заменяет iter как управляющий аргумент, а out_IGNORE() упрощает игнорирование неиспользуемого выходного параметра.

3. И наконец эта же сигнатура метода позволяет выбрать одного из пациентов по известному первичному ключу (keyPatient):

        ref<CDbDemoPatient> rPatient;
        if (!QueryPatients(
                out_ATKEY(),
                out rPatient,
                keyPatient))
        {
            // Missing patient at key keyPatientToUpdate
        }
Универсальная реализация метода queryorcount

Разберем подробней каждый элемент шаблонной реализации Query()-метода приведенной выше.

rQuery управляет запросом

Инициализация и выполнение запроса происходит через CQueryIfaceGp который хранится в iter и берется из него на каждой итерации запроса через out_i.AdvanceQuery(). Вызов AdvanceQuery() поддерживает правитьный счетчик итераций в iter и должен вызываться только однажды в Query()-методе:

{
    ref<CQueryIfaceGp> rQuery =
        out_i.AdvanceQuery();
from<> сначала задает таблицы

Таблицы с исходными данными для запроса инкапсулируются теплейтом from<>:

    from<CDbDemoPatient> fromPatient(rQuery);

В сложных запросах таких может быть несколько (в том числе и ссылающихся на одну и ту же таблицу под разными алиасами).

Для внешних JOIN используется tryfrom<>.

Инициализация запроса на первой итерации

Метод Query() работает в двух отличных режимах на первой и на последующих итерациях. На первой итерации, отличаемой rQuery->Init:

    if (rQuery->Init())
    {

задаются исходные таблицы:

        fromPatient.
            BeginFrom(
                x_rDbDemoRoot->
                    _x_dblistDemoPatient,
                keyPrimary);

Здесь BeginFrom() выполнил тройную функцию, и не только как оператор SQL FROMпроассоциировал алиас "fromPatient" с нужной таблицей, но и задал два фильтра:

1. Выбираемые объекты должны принадлежать контейнеру x_rDbDemoRoot-> _x_dblistDemoPatient

2. В случае если keyPrimary задан (keyPrimary != key()) он используется для выборки только одного объекта.

Здесь же можно задать JOIN между таблицами и критерии фильтрации, которых пока нет в нашем простом примере:

        //rQuery->x_qxboolWhere = ...; no filter, get all
    }

На последующих итерациях блок инициализации if (rQuery->Init()) уже не будет выполнятся и запрос будет происходить с изначально заданными параметрами.

from<> возвращает результаты

На каждой итерации (включая первую после инициализации) мы получаем результаты запроса в виде объектов, если таковые поступают:

    if (rQuery->Next())
    {
        out_rPatient = fromPatient.FetchRef();
    }

В нашем примере мы возвращаем не инкапсулированные объекты CDb, в более реалистичных реализациях понадобится создание соответствующих Iface/Impl и приватное присоединение записей ref<CDb> к Impl.

Для внешних JOIN используется tryfrom.FetchRp.

Возврат queryorcount

Завершаем Query() метод таким образом чтобы результат его работы можно было правильно интерпретировать для Iterate/GetCount/Lookup режимов. Объект queryorcount нужно инициализировать rQuery которая задаст необходимый bool или int результат:

    return rQuery;
}
Обновление записей

Мы можем изменить проперти полученных от запросов CDb-объектов и перезаписать их в БД (SQL UPDATE).

Модификация пропертей CDb-объектов происходит обысными автоматическими Get/Set, ничем не отличается от обычных x-пропертей и непосредственно при вызове Set() ничего особенного не происходит.

После завершения модификации пропертей нужно обязательно вызывать CommitRef() для того чтобы новые значения отправились в БД.

    ref<CDbDemoPatient> rDbPatient;
    ... // получаем rDbPatient от Lookup или итератора
 
    // Change a property and commit
 
    moment momentNew;
    momentNew.SetYear(1975);
    momentNew.SetMonth(2);
    momentNew.SetDay(25);
 
    rDbPatient->x_momentDemoPatientBirthDate = momentNew;
 
    rPatient->CommitRef();

Забывать CommitRef() или откладывать его, отрывая от кода, меняющего проперти запрещено. Не определено будет ли обновлена БД или нет если CommitRef() не вызвать!

Запросы с фильтром

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

Условия фильтрации в параметрах Query() метода

Мы уже наблюдали особое условие фильтрации key keyPrimary в QueryPatients(), которое должно предусматриваться всегда и дает вырожденную выборку из одного объекта.

Вслед за keyPrimary мы можем добавить альтернативные условия фильтрации, например по полу пациента:

queryorcount CAbsformRelademoImpl::QueryPatientsBySex(
        out iter& out_i,
        out ref<CDbDemoPatient>& out_rPatient,
        key keyPrimary,
        type<CPexApiSexTypeIfaceGp> typePexApiSexType)
{
Задание условий WHERE

Реализация выглядит очень похожей, идентичной в инициализации запроса и получении результата, за исключением задания условия в rQuery->x_qxboolWhere:

    ref<CQueryIfaceGp> rQuery =
        out_i.AdvanceQuery();
 
    from<CDbDemoPatient> fromPatient(rQuery);
 
    if (rQuery->Init())
    {
        fromPatient.
            BeginFrom(
                x_rDbDemoRoot->
                    _x_dblistDemoPatient,
                keyPrimary);
 
        rQuery->x_qxboolWhere =
            Qx
                fromPatient->x_typeDemoPatientSex == typePexApiSexType;
    }
 
    if (rQuery->Next())
    {
        out_rPatient = fromPatient.FetchRef();
    }
 
    return rQuery;
}

Здесь в Qx мы сослались на таблицу fromPatient, в ней колонку x_typeDemoPatientSexзначение которой надо сравнивать со значением локальной переменной С++ typePexApiSexType.

Вызов итератора с фильтром

Итератор по такому запросу будет выглядеть как обычно:

    type<CPexApiSexTypeIfaceGp> typeFilterSex =
        type<CPexApiSexTypeIfaceGp>()->
            AsPexApiSexTypeAsFemale();
 
    ref<CDbDemoPatient> rIterPatient;
    for (
        iter i;
        QueryPatientsBySex(
            out i,
            out rIterPatient,
            key(),
            typeFilterSex);)
    {
        ...
    }
COUNT с фильтром

Точно так же можно получить GetCount с учетом фильтра:

    int nCountFiltered =
        QueryPatientsBySex(
            out_COUNT(),
            out_IGNORE(ref<CDbDemoPatient>),
            key(),
            typeFilterSex);
Первичный ключ плюс фильтр

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

Закрытие

БД надо всегда закрывать не подразумевая никаких способностей некоторых бакендов сохранять изменения автоматически:

    // Cleanup
    rUdbConnection->
        CloseUdbConnection();

После этого нельзя выполнять никаких запросов и использовать CDb объекты.

Весь код целиком
// Patient record
class CDbDemoPatient : public dbobject
{
public:
 
    // "Smith John"
    str x_sDemoPatientName = "",
            auto(Get, Set);
 
    // demo time storage
    moment x_momentDemoPatientBirthDate = moment(),
            auto(Get, Set);
 
    // gender as type<>
    type<CPexApiSexTypeIfaceGp> x_typeDemoPatientSex,
            auto(Get, Set);
};
 
// Global singleton database record containing everything else
class CDbDemoRoot : public dbobject
{
public:
 
    // Patients
    dblist<CDbDemoPatient> _x_dblistDemoPatient,
            auto(Get);
};
 
queryorcount CAbsformRelademoImpl::QueryPatients(
        out iter& out_i,
        out ref<CDbDemoPatient>& out_rPatient,
        key keyPrimary)
{
    ref<CQueryIfaceGp> rQuery =
        out_i.AdvanceQuery();
 
    from<CDbDemoPatient> fromPatient(rQuery);
 
    if (rQuery->Init())
    {
        fromPatient.
            BeginFrom(
                x_rDbDemoRoot->
                    _x_dblistDemoPatient,
                keyPrimary);
 
        //rQuery->x_qxboolWhere = ...; no filter, get all
    }
 
    if (rQuery->Next())
    {
        out_rPatient = fromPatient.FetchRef();
    }
 
    return rQuery;
}
 
queryorcount CAbsformRelademoImpl::QueryPatientsBySex(
        out iter& out_i,
        out ref<CDbDemoPatient>& out_rPatient,
        key keyPrimary,
        type<CPexApiSexTypeIfaceGp> typePexApiSexType)
{
    ref<CQueryIfaceGp> rQuery =
        out_i.AdvanceQuery();
 
    from<CDbDemoPatient> fromPatient(rQuery);
 
    if (rQuery->Init())
    {
        fromPatient.
            BeginFrom(
                x_rDbDemoRoot->
                    _x_dblistDemoPatient,
                keyPrimary);
 
        rQuery->x_qxboolWhere =
            Qx
                fromPatient->x_typeDemoPatientSex == typePexApiSexType;
    }
 
    if (rQuery->Next())
    {
        out_rPatient = fromPatient.FetchRef();
    }
 
    return rQuery;
}
 
void CAbsformRelademoImpl::OnTestClass()
{
    debug::GRedirectTestLog("+Udb", "RelademoUdb");
 
    CModule::GPreloadCommonDll("Datrans");
 
    // Database connection is stored as our property
    ref<CUdbConnectionIfaceGp> rUdbConnection =
        x_rDemoUdbConnection;
 
    // Declate our table classes
    rUdbConnection->DeclareUdbTableClass(ref<CDbDemoRoot>());
    rUdbConnection->DeclareUdbTableClass(ref<CDbDemoPatient>());
 
    // Init database connection
    str sError;
    rUdbConnection->
        OpenUdbConnection(
            type<CUdbStorageTypeIfaceGp>()->
                AsUdbStorageTypeForSqlite(),
                //AsUdbStorageTypeForBijou(),
            sys::GGetTempFolder().GetAppendedPath("Demo.udb"),
            out sError);
    HandleError(sError);
 
    // Init root record
    this->x_rDbDemoRoot =
        dbref<CDbDemoRoot>().
            InitRoot(
                rUdbConnection);
 
    // Add patients
    key keyPatientToUpdate;
    {
        ref<CDbDemoPatient> rPatient;
 
        rPatient->x_sDemoPatientName =
            "Sheen";
        rPatient->x_momentDemoPatientBirthDate =
            moment::GGetLocal();
 
        x_rDbDemoRoot->
            _x_dblistDemoPatient.Get().
                AddNewRow(
                    rPatient);
 
        // change after add
        rPatient->x_sDemoPatientName =
            "Sheen Charlie";
        rPatient->CommitRef();
    }
    {
        ref<CDbDemoPatient> rPatient;
 
        rPatient->x_sDemoPatientName =
            "Handler Chelsea";
        rPatient->x_typeDemoPatientSex =
            type<CPexApiSexTypeIfaceGp>()->
                AsPexApiSexTypeAsFemale();
 
        x_rDbDemoRoot->
            _x_dblistDemoPatient.Get().
                AddNewRow(
                    rPatient);
 
        keyPatientToUpdate = rPatient->GetKey();
    }
 
    //
    // List all patients
    //
    {
        ref<CDbDemoPatient> rIterPatient;
        for (
            iter i;
            QueryPatients(
                out i,
                out rIterPatient,
                key());)
        {
            TESTLOG(
                "",
                ""
                    "Patient: " + Str(rIterPatient->GetKey()) + ", "
                    "\"" +
                        rIterPatient->x_sDemoPatientName +
                        "\", " +
                    rIterPatient->x_momentDemoPatientBirthDate.Get().
                        ToStr("yyyy-mm-dd") +
                        ", " +
                    rIterPatient->x_typeDemoPatientSex.Get()->
                        GetDicomSexIdOr("?") +
                        ", "
                    "\n");
        }
 
        //
        // Count patients
        //
 
        int nPatients =
            QueryPatients(
                out_COUNT(),
                out_IGNORE(ref<CDbDemoPatient>),
                key());
 
        TESTLOG(
            "",
            ""
                "------------------------------\n"
                "total " + Str(nPatients) + " patients."
                    "\n");
    }
 
    //
    // Update a patient
    //
    {
        // Lookup patient by key
        rASSERT(keyPatientToUpdate != null());
        ref<CDbDemoPatient> rPatient;
        if (!QueryPatients(
                out_ATKEY(),
                out rPatient,
                keyPatientToUpdate))
        {
            HandleError("Missing demo patient at key == " + Str(keyPatientToUpdate));
        }
 
        // Change a property and commit
 
        moment momentNew;
        momentNew.SetYear(1975);
        momentNew.SetMonth(2);
        momentNew.SetDay(25);
 
        rPatient->x_momentDemoPatientBirthDate = momentNew;
 
        rPatient->CommitRef();
    }
 
    //
    // Filter list by sex
    //
    {
        type<CPexApiSexTypeIfaceGp> typeFilterSex =
            type<CPexApiSexTypeIfaceGp>()->
                AsPexApiSexTypeAsFemale();
 
        TESTLOG(
            "",
            ""
                "\n\n"
                "Filtered by sex == " +
                typeFilterSex->GetDicomSexId() +
                " total " +
                Str((int)
                    QueryPatientsBySex(
                        out_COUNT(),
                        out_IGNORE(ref<CDbDemoPatient>),
                        key(),
                        typeFilterSex)) + " patients:"
                    "\n");
 
        ref<CDbDemoPatient> rIterPatient;
        for (
            iter i;
            QueryPatientsBySex(
                out i,
                out rIterPatient,
                key(),
                typeFilterSex);)
        {
            TESTLOG(
                "",
                ""
                    "Patient: " + Str(rIterPatient->GetKey()) + ", "
                    "\"" +
                        rIterPatient->x_sDemoPatientName +
                        "\", " +
                    rIterPatient->x_momentDemoPatientBirthDate.Get().
                        ToStr("yyyy-mm-dd") +
                        ", " +
                    rIterPatient->x_typeDemoPatientSex.Get()->GetDicomSexIdOr("?") +
                        ", "
                    "\n");
        }
    }
 
    // Cleanup
    rUdbConnection->
        CloseUdbConnection();
}
 
void CAbsformRelademoImpl::HandleError(
        str sError)
{
    if (sError == "")
    {
        return;
    }
 
    rFAIL(sError);
 
    CProject::GGetProjectCriticalErrorFloater()->
        LogNewEvent(
            Enru_VL(
                "Database error",
                "Сбой базы данных"),
            "Relog :: " + sError);
}

и отчет:

Patient: 3102400010001, "Sheen Charlie", 2040-01-02, ?,
Patient: 3102400010002, "Handler Chelsea", 1800-01-01, F,
------------------------------
total 2 patients.
Filtered by sex == F total 1 patients:
Patient: 3102400010002, "Handler Chelsea", 1975-02-25, F,