! | Данная информация предназначена только только для 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)
Для добавления новых записей в БД нам обязательно нужна будет ссылка на корневой контейнер которую нужно проинициализировать после открытия соединения с БД и сохранить вместе с 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>. Однако шаблон его реализации позволяет легко усовершенствовать его для поддержки более сложной инкапсуляции доступа к БД.
Мы лекларировали метод, способный работать в трех эпостасях: как 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 }
Разберем подробней каждый элемент шаблонной реализации Query()-метода приведенной выше.
Инициализация и выполнение запроса происходит через CQueryIfaceGp который хранится в iter и берется из него на каждой итерации запроса через out_i.AdvanceQuery(). Вызов AdvanceQuery() поддерживает правитьный счетчик итераций в iter и должен вызываться только однажды в Query()-методе:
{ ref<CQueryIfaceGp> rQuery = out_i.AdvanceQuery();
Таблицы с исходными данными для запроса инкапсулируются теплейтом 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()) уже не будет выполнятся и запрос будет происходить с изначально заданными параметрами.
На каждой итерации (включая первую после инициализации) мы получаем результаты запроса в виде объектов, если таковые поступают:
if (rQuery->Next()) { out_rPatient = fromPatient.FetchRef(); }
В нашем примере мы возвращаем не инкапсулированные объекты CDb, в более реалистичных реализациях понадобится создание соответствующих Iface/Impl и приватное присоединение записей ref<CDb> к Impl.
Для внешних JOIN используется tryfrom.FetchRp.
Завершаем 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 к выборке пациентов.
Мы уже наблюдали особое условие фильтрации key keyPrimary в QueryPatients(), которое должно предусматриваться всегда и дает вырожденную выборку из одного объекта.
Вслед за keyPrimary мы можем добавить альтернативные условия фильтрации, например по полу пациента:
queryorcount CAbsformRelademoImpl::QueryPatientsBySex( out iter& out_i, out ref<CDbDemoPatient>& out_rPatient, key keyPrimary, type<CPexApiSexTypeIfaceGp> typePexApiSexType) {
Реализация выглядит очень похожей, идентичной в инициализации запроса и получении результата, за исключением задания условия в 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);) { ... }
Точно так же можно получить 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,