TeamWox SDK: Взаимодействие с СУБД

Введение

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

В этой статье мы рассмотрим организацию постоянного хранения и изменения данных. Для этой цели в TeamWox применяется embedded версия СУБД Firebird. Продолжая работать с учебным модулем Hello World, в его менеджер мы добавим новые методы работы с данными, так чтобы они загружались не из статических массивов, а из базы данных.

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

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

Примечание: В статье описывается упрощенный способ взаимодействия с данными - чтение из БД каждый раз, когда нужны данные. Это не оптимально с точки зрения производительности.

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

 

Развертывание модулей TeamWox

Развертывание - это один из этапов жизненного цикла модуля TeamWox. Условно жизненный цикл модуля можно представить так:

  • Этап 1. Разработка модуля
  • Этап 2. Установка и настройка модуля на сервере TeamWox
  • Этап 3. Выявление ошибок, сбор замечаний и предложений от пользователей
  • Этап 4. Расширение функционала модуля, исправление ошибок
  • Этап 5. Обновление модуля на сервере

Затем этапы 3, 4 и 5 циклически повторяются в течение всего срока поддержки модуля его разработчиками.

Мы расширим функциональность модуля Hello World, добавив в него возможность работы с данными, которые будут храниться в СУБД. Но прежде всего, нам потребуется создать таблицу с определенной структурой (при необходимости, можно также проинициализировать таблицу данными). При каждом последующем обновлении модуля структура таблицы будет проверяться и при необходимости изменяться.

SDK предоставляет все необходимые средства для работы с СУБД TeamWox.

1. В проекте HelloWorld.vcproj в stdafx.h включите файл smart_sql.h. Этот инструмент представляет собой набор классов для удобной работы с БД средствами SQL (расположен в папке \TeamWox SDK\SDK\Common). Соблюдайте очередность подключаемых файлов. Также добавьте его в проект (папка \Header Files\Common).

#include "..\..\SDK\Common\SmartLogger.h"
#include "..\..\SDK\Common\smart_sql.h"
#include "..\..\SDK\Common\tools_errors.h"
#include "..\..\SDK\Common\tools_strings.h"
#include "..\..\SDK\Common\Page.h"

2. Опишите структуру таблицы. Для этого в менеджере создайте функцию с именем DBTableCheck.

//+------------------------------------------------------------------+
//| Менеджер модуля                                                  |
//+------------------------------------------------------------------+
class CHelloWorldManager
  {
private:
   static HelloWorldRecord m_info_records[];         // список общедоступной информации
   static HelloWorldRecord m_advanced_records[];     // список информации с ограниченным доступом
   //---
   IServer          *m_server;                       // ссылка на сервер
   //---
   CSync             m_sync;                         // синхронизация доступа к членам класса

public:
                     CHelloWorldManager();
                    ~CHelloWorldManager();
   //---
   TWRESULT          Initialize(IServer *server);
   //--- работа с информацией
   TWRESULT          InfoGet(const Context *context,HelloWorldRecord *records,int *count);
   TWRESULT          InfoAdvacedGet(const Context *context,HelloWorldRecord *records,int *count);

private:
   TWRESULT          DBTableCheck(ISqlBase *sql);    // проверка/создание/изменение структуры таблицы
  };
//+------------------------------------------------------------------+
//| Создание таблицы HELLOWORLD                                      |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::DBTableCheck(ISqlBase *sql)
  {
//--- проверки
   if(sql==NULL) ReturnError(RES_E_INVALID_ARGS);
//---
   TWRESULT res=RES_S_OK;
//---
   if(RES_FAILED(res=sql->CheckTable("HELLOWORLD",
       "ID     BIGINT        DEFAULT 0  NOT NULL,"
       "NAME   VARCHAR(256)  DEFAULT '' NOT NULL",
       "PRIMARY KEY (ID)",
       "DESCENDING INDEX IDX_HELLOWORLD_ID_DESC (ID)",
        NULL,NULL,0)))
      ReturnError(res);
//---   
   ExtLogger(NULL,LOG_STATUS_INFO) << "Table 'HELLOWORLD' checked";
//---
   return(RES_S_OK);
  }
Метод ISqlBase::CheckTable проверяет/создает/изменяет таблицу и ее структуру. Описания полей таблицы разделяются запятыми и экранируются кавычками, поскольку вся эта совокупность передается как второй аргумент функции CheckTable.

Текст, который описывает поля в CheckTable, соответствует синтаксису CREATE TABLE [имя таблицы] ([описание полей]) [параметры таблицы].
Логирование

Ключевые этапы работы модуля всегда рекомендуется записывать в лог, поскольку это существенно облегчает поиск ошибок при работе модуля на сервере у клиента. Для этой цели в SDK имеется специальный класс CSmartLogger.

В нашем случае, сообщение о работе метода CheckTable мы выводим в лог с помощью объекта ExtLogger этого класса. Сообщения в лог лучше выводить на английском языке из-за возможного конфликта кодировок.

3. Создайте подключение менеджера к БД сервера. Для этого в \Managers\HelloWorldManager.cpp добавьте следующие строчки кода.

//+------------------------------------------------------------------+
//| Инициализируем модуль                                            |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::Initialize(IServer *server)
  {
   if(server==NULL) ReturnError(RES_E_INVALID_ARGS);
//---
   TWRESULT res=RES_S_OK;
//---
   CLocker lock(m_sync);
//---
   m_server=server;
//---
   CSmartSql sql(server);
   if(sql==NULL) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to make sql connection");
//---
   if(RES_FAILED(res=DBTableCheck(sql))) ReturnErrorExt(res,NULL,"failed to check table");
//---
   return(RES_S_OK);
  }

4. В структуре HelloWorldRecord (файл \API\HelloWorld.h) приведите в соответствие типы данных для использования в SQL-запросе (идентификаторы записей в TeamWox рекомендуется делать 64 разрядными).

//+------------------------------------------------------------------+
//| Структура записи                                                 |
//+------------------------------------------------------------------+
struct HelloWorldRecord
  {
   INT64             id;
   wchar_t           name[256];
  };

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

Проверка создания таблицы

Теперь при установке модуля Hello World на любом сервере TeamWox будет автоматически создаваться таблица HELLOWORLD с полями ID и NAME. Все реализуется в исходном коде менеджера, и никаких дополнительных инструментов для этого не нужно.

 

Методы менеджера для работы с данными

Работа с любой СУБД базируется на четырех фундаментальных операциях CRUD:

  • Создание новых записей (Create)
  • Чтение записей (Retrieve)
  • Редактирование записей (Update)
  • Удаление записей (Delete)

Эти методы обеспечивают полноценную работу с данными. Их мы реализуем в менеджере модуля, а в странице мы создадим HTTP API, с помощью которого можно будет запрашивать данные в формате JSON.

В менеджере объявите основные, а также вспомогательные методы и члены.

//+------------------------------------------------------------------+
//| Менеджер модуля                                                  |
//+------------------------------------------------------------------+
class CHelloWorldManager
  {
private:
   static HelloWorldRecord m_info_records[];           // список общедоступной информации
   static HelloWorldRecordAdv m_advanced_records[];    // список информации с ограниченным доступом
   //---
   IServer          *m_server;                         // ссылка на сервер
   //---
   CSync             m_sync;                           // синхронизация доступа к членам класса
   //---
   INT64             m_next_id;                        // Следующий id записи  

public:
                     CHelloWorldManager();
                    ~CHelloWorldManager();
   //---
   TWRESULT          Initialize(IServer *server, int prev_build);
   //--- работа с информацией
   TWRESULT          InfoAdvacedGet(const Context *context,HelloWorldRecordAdv *records,int *count);
   
   //--- Методы C.R.U.D. (Create, Retrieve, Update и Delete)
   //--- ЧТЕНИЕ (Retrieve)
   //    Получение списка публичной информации
   TWRESULT          InfoGet(const Context *context,HelloWorldRecord *records,int start,int *count);
   //    Получение строки из таблицы HELLOWORLD
   TWRESULT          InfoGet(const Context *context,INT64 id,HelloWorldRecord *record);
   //    Получение количества записей в таблице HELLOWORLD
   TWRESULT          InfoCount(const Context *context,int *count);
   //--- СОЗДАНИЕ (Create) и РЕДАКТИРОВАНИЕ (Update)
   //--- Изменение записи или добавление новой записи в таблице HELLOWORLD
   TWRESULT          InfoUpdate(const Context *context,HelloWorldRecord *record);
   //--- УДАЛЕНИЕ (Delete)
   //--- Удаление записи из таблицы HELLOWORLD
   TWRESULT          InfoDelete(const Context *context,INT64 id); 

private:
   TWRESULT          DBTableCheck(ISqlBase *sql);      // проверка/создание/изменение структуры таблицы
   //--- Определение максимального id записи из таблицы HELLOWORLD
   TWRESULT          LoadMaxId(ISqlBase *sql);
   //--- Получение следующего идентификатора
   INT64             NextId();
   };

 

1. Чтение записей (Retrieve)

Начнем с чтения, которое включает в себя поиск, извлечение и просмотр записей из таблицы.

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

//+------------------------------------------------------------------+
//| Получение списка публичной информации                            |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::InfoGet(const Context *context,HelloWorldRecord *records,int start,int *count)
  {
   HelloWorldRecord rec_info_get          ={0};
//--- проверки (параметр start должен начинаться с 1, 
//    поскольку так же с 1 начинаются записи в таблицах СУБД Firebird)
   if(context==NULL || m_server==NULL || records==NULL || start<1 || count==NULL || *count<=0) 
     ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)                                                                      
     ReturnError(RES_E_INVALID_CONTEXT);
//--- Инициализация переменных для цикла получения результатов запроса
   int              max_count             =*count;
   int              index                 =0;
   int              end                   =start+max_count; // номер последней записи
   *count                                 =0;
//--- текст SQL-запроса на выборку записей из таблицы HELLOWORLD с сортировкой по полю ID. 
   char             query_select[]        ="SELECT id,name FROM helloworld ORDER BY id ROWS ? TO ?";
//--- "Привязываем" данные к параметрам запроса
   SqlParam         params_query_select[] ={
      SQL_INT64,&rec_info_get.id,  sizeof(rec_info_get.id),
      SQL_WTEXT, rec_info_get.name,sizeof(rec_info_get.name)
      };
//--- Параметры запроса
   SqlParam         params_rows[]         ={
      SQL_LONG,&start,sizeof(start),
      SQL_LONG,&end,  sizeof(end)
      };
//--- шлем запрос
   if(!context->sql->Query(query_select, 
                           params_rows, _countof(params_rows), 
                           params_query_select, _countof(params_query_select)))
      ReturnErrorExt(RES_E_SQL_ERROR,NULL,"HELLOWORLD records query failed");
//---
   CLocker lock(m_sync);
//--- в цикле получим все элементы результатов запроса
   while(context->sql->QueryFetch())
     {
      if(*count<max_count)
        {
         memcpy(&records[*count],&rec_info_get,sizeof(records[*count]));
         //---
         (*count)++;
        }
      //---
      index++;
     }
//---
   context->sql->QueryFree();
//---
   return(RES_S_OK);
  }
Метод ISqlBase::Query выполняет запрос (с указанными параметрами) на чтение данных из таблицы HELLOWORLD без их изменения. В тексте SQL-запроса SELECT с помощью операнда ROWS ограничивается диапазон извлекаемых из таблицы строк.

Метод ISqlBase::QueryFetch по одной извлекает строки из выборки после запроса на чтение. Эти строки записываются в массив records и, таким образом, подготавливаются для последующего отображения на странице.

С помощью метода ISqlBase::QueryFree запрос и его результаты освобождаются.

1.2. Функция InfoCount будет использоваться для получения количества записей из БД.

Примечание: с точки зрения производительности, получение количества записей без кеширования не эффективно. При большом объеме данных это приведет к резкому замедлению работы сервера.
//+------------------------------------------------------------------+
//| Получение количества записей в таблице HELLOWORLD                |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::InfoCount(const Context *context,int *count)
  {
   HelloWorldRecord rec_page_count            ={0};
//--- проверки
   if(context==NULL || m_server==NULL || count==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)                             ReturnError(RES_E_INVALID_CONTEXT);
//--- текст SQL-запроса количества записей ID в таблице HELLOWORLD
   char             query_page_count[]        ="SELECT COUNT(id) FROM helloworld";
//---"Привязываем" данные к параметрам запроса
   SqlParam         params_query_page_count[] ={
      SQL_LONG,count,sizeof(*count)
   };
//--- шлем запрос
   if(!context->sql->Query(query_page_count,
                           NULL,0,
                           params_query_page_count,_countof(params_query_page_count)))
      ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to query helloworld records");
//--- обеспечим атомарность
   CLocker lock(m_sync);
//--- в цикле получим все элементы
   if(!context->sql->QueryFetch()) 
      ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to get count");
//---
   context->sql->QueryFree();
//---
   return(RES_S_OK);
  }
В контексте запроса (Context) доступно уже созданное соединение, и дополнительно создавать его не надо. В сервере есть пул соединения с СУБД.

Соединение с базой данных создается только в случае вызова методов для запроса. Иными словами, если вы просто запросили SQL соединение, но не воспользовались им, то реальное соединение с базой данных не будет запрошено.

 

2. Создание новых (Create) и редактирование существующих (Update) записей

2.1. Проинициализируйте переменную m_next_id:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CHelloWorldManager::CHelloWorldManager():m_server(NULL),m_next_id(0)
  {
//---
//---
  }

2.2. Функция LoadMaxId с помощью SQL-запроса извлекает максимальный номер id (т.е. номер последней записи) из таблицы HELLOWORLD.

//+------------------------------------------------------------------+
//| Получение максимального id записи из таблицы HELLOWORLD          |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::LoadMaxId(ISqlBase *sql)
  {
//--- проверки
   if(sql==NULL) ReturnError(RES_E_INVALID_ARGS);
//--- текст SQL-запроса на получение максимального значения поля ID
   char     query_select_max[]    ="SELECT MAX(id) FROM helloworld";
//--- Параметр запроса
   SqlParam params_max_id[]       ={
      SQL_INT64,&m_next_id,sizeof(m_next_id)
   };
//--- шлем запрос
   if(!sql->Query(query_select_max,NULL,0,params_max_id,_countof(params_max_id)))
      ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed query to get max id from helloworld");
//--- получим значение
   sql->QueryFetch();
   sql->QueryFree();
//---
   return(RES_S_OK);
  }

2.3. Функция NextId увеличивает значение переменной m_next_id на единицу. Она будет использоваться при добавлении новых записей.

//+------------------------------------------------------------------+
//| Получение следующего id записи                                   |
//+------------------------------------------------------------------+
INT64 CHelloWorldManager::NextId()
  {
//---
   CLocker lock(m_sync);
   return(++m_next_id);   
//---
  }
Внимание: Синхронизацию нужно выполнять обязательно при изменении/чтении m_next_id. TeamWox - это сервер, который одновременно обрабатывает множество запросов.

2.4. Добавьте проверку на результат выполнения этой функции в инициализацию менеджера.

//+------------------------------------------------------------------+
//| Инициализируем модуль                                            |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::Initialize(IServer *server, int prev_build)
  {
   if(server==NULL) ReturnError(RES_E_INVALID_ARGS);
//---
   TWRESULT res=RES_S_OK;
//---
   CLocker lock(m_sync);
//---
   m_server=server;
//---
   CSmartSql sql(server);
   if(sql==NULL) ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to make sql connection");
//---
   if(RES_FAILED(res=DBTableCheck(sql))) ReturnErrorExt(res,NULL,"failed to check table");
//--- проверка максимального ID для записи в таблице HELLOWORLD
   if(RES_FAILED(res=LoadMaxId(sql))) ReturnErrorExt(res,NULL,"failed to load max id for HELLOWORLD table");
//---
   if(prev_build<101)
     {
//--- При необходимости внесем изменения в данные при обновлении старой версии модуля
     }
//---
   return(RES_S_OK);
  }

2.5. Метод InfoUpdate будет добавлять новые записи в таблицу HELLOWORLD, либо изменять существующие.

//+------------------------------------------------------------------+
//| Добавление, сохранение записи в таблицу HELLOWORLD               |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::InfoUpdate(const Context *context,HelloWorldRecord *record)
  {
//--- проверки
   if(context==NULL || record==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)            ReturnError(RES_E_INVALID_CONTEXT);
//--- текст SQL-запроса на добавление новой записи в таблицу HELLOWORLD
   char     query_insert[] ="INSERT INTO helloworld(name,id) VALUES(?,?)";
//--- текст SQL-запроса на изменение существующей записи в таблице HELLOWORLD
   char     query_update[] ="UPDATE helloworld SET name=? WHERE id=?";
//--- "Привязываем" данные к параметрам запроса
   SqlParam params_query[] ={
      SQL_WTEXT, record->name,   sizeof(record->name),
      SQL_INT64,&record->id,     sizeof(record->id)
      };
//--- новая запись?
   if(record->id<=0)
     {
      record->id=NextId();
      //---
      if(!context->sql->QueryImmediate(query_insert, params_query, _countof(params_query)))
         ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to insert helloworld record");
     }
   else
     {
      if(!context->sql->QueryImmediate(query_update, params_query, _countof(params_query)))
         ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to update helloworld record");
     }
//---
   return(RES_S_OK);
  }
Метод ISqlBase::QueryImmediate используется в случаях, когда не требуется получать результаты запроса (в отличие от ISqlBase::Query).

2.6. Во второй версии метода InfoGet выполняется SQL-запрос на получение строки из таблицы HELLOWORLD по указанному ID.

//+------------------------------------------------------------------+
//| Получение строки из таблицы HELLOWORLD                           |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::InfoGet(const Context *context,INT64 id,HelloWorldRecord *record)
  {
//--- проверки
   if(context==NULL || record==NULL || id<1) ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)                    ReturnError(RES_E_INVALID_CONTEXT);
//--- текст SQL-запроса на получение строки из таблицы HELLOWORLD по указанному ID. 
   char query_select_string[]="SELECT id,name FROM helloworld WHERE id=?";
//--- Параметр запроса
   SqlParam params_string[]={
      SQL_INT64,&id,sizeof(id)
      };
//--- "Привязываем" данные к параметрам запроса
   SqlParam params_query_select_string[] ={
      SQL_INT64,&record->id,      sizeof(record->id),
      SQL_WTEXT, record->name,    sizeof(record->name)
      };
//---
   ZeroMemory(record,sizeof(*record));
//--- шлем запрос
   if(!context->sql->Query(query_select_string, 
                           params_string, _countof(params_string), 
                           params_query_select_string, _countof(params_query_select_string)))
      ReturnErrorExt(RES_E_SQL_ERROR,NULL,"helloworld record query failed");
//--- получим элемент
   context->sql->QueryFetch();
   context->sql->QueryFree();
//---
   if(record->id!=id)
      return(RES_E_NOT_FOUND);
//---
   return(RES_S_OK);
  }

 

3. Удаление записей (Delete)

Метод InfoDelete реализуется проще всего.

//+------------------------------------------------------------------+
//| Удаление записи из таблицы HELLOWORLD                            |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldManager::InfoDelete(const Context *context,INT64 id)
  {
//--- проверки
   if(context==NULL || id<=0) ReturnError(RES_E_INVALID_ARGS);
   if(context->sql==NULL)     ReturnError(RES_E_INVALID_CONTEXT);
//--- текст SQL-запроса на удаление строки из таблицы HELLOWORLD по указанному ID
   char delete_string[]="DELETE FROM helloworld WHERE id=?";
//--- Параметр запроса
   SqlParam params_delete_string[] ={
      SQL_INT64,&id,sizeof(id)
      };
//---
   if(!context->sql->QueryImmediate(delete_string,params_delete_string,_countof(params_delete_string)))
      ReturnErrorExt(RES_E_SQL_ERROR,NULL,"failed to delete helloworld record");
//---
   ExtLogger(context,LOG_STATUS_INFO) << "Delete record #" << id;
//---
   return(RES_S_OK);
  }

 

HTTP API для работы с данными

Теперь нам нужно создать HTTP API, который будет использовать реализованные методы C.R.U.D. Отображение и удаление записей будет выполняться на уже существующей странице PageNumberTwo, а для создания и редактирования записей мы отведем новую страницу PageEdit. Данные, полученные страницей из менеджера, будут выводиться как объекты JSON.

 

Вывод и удаление данных

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

//+------------------------------------------------------------------+
//| Вторая страница                                                  |
//+------------------------------------------------------------------+
class CPageNumberTwo : public CPage
  {
private:
   IServer          *m_server;
   //---
   HelloWorldRecord     m_info[5];     // количество выводимых на странице записей
   HelloWorldRecord    *m_info_current;
   CHelloWorldManager  *m_manager;     // подключение к менеджеру
   int                  m_info_count;
   //--- для нумератора таблицы HELLOWORLD
   int                  m_start;       // номер текущей записи
   int                  m_info_total;  // общее количество записей
   //---
   HelloWorldRecordAdv  m_info_advanced[5];
   HelloWorldRecordAdv *m_info_advanced_current;
   int                  m_info_advanced_count;

public:
                        CPageNumberTwo();
                       ~CPageNumberTwo();
   //--- обработчик страницы
   TWRESULT             Process(const Context *context,
                                IServer *server,
                                const wchar_t *path,
                                CHelloWorldManager *manager);
   //--- функция показа
   bool                 Tag(const Context *context,const TagInfo *tag);
   //--- ЧТЕНИЕ (Read)
   //--- Вывод данных на страницу в формате JSON
   TWRESULT             JSONDataOutput(const Context *context,CHelloWorldManager *manager);
   //--- УДАЛЕНИЕ (Delete)
   //--- Удаление строки
   TWRESULT             OnDelete(const Context *context,
                                 IServer *server,
                                 const wchar_t *path,
private:
   //--- Подготовка списка страниц нумератора
   TWRESULT             PreparePages(const Context *context,CHelloWorldManager *manager);

  };

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CPageNumberTwo::CPageNumberTwo() : m_server(NULL),m_start(0),m_info_total(0),
                                   m_info_count(0),m_info_advanced_count(0),
                                   m_info_advanced_current(NULL),m_info_current(NULL)
  {
//---
   ZeroMemory(m_info,         sizeof(m_info));
   ZeroMemory(m_info_advanced,sizeof(m_info_advanced));
//---
  } 

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

//+------------------------------------------------------------------+
//| Подготовка списка страниц нумератора                             |
//+------------------------------------------------------------------+
TWRESULT CPageNumberTwo::PreparePages(const Context *context,CHelloWorldManager *manager)
  {
   TWRESULT res = RES_S_OK;
//---
   if(context==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL)         ReturnError(RES_E_INVALID_CONTEXT);
//---
   if(RES_FAILED(res=manager->InfoCount(context,&m_info_total)))
      ReturnErrorExt(res,context,"failed get count of records");
//---
   m_start=max(1,context->request->GetInt32(IRequest::GET,L"from")+1);
   int max_page   =max(0,int((m_info_total+_countof(m_info))/_countof(m_info))-1);
//---
   if(m_start>max_page*_countof(m_info)+1)
      m_start=max_page*_countof(m_info)+1;
   return(res);
  }

4. Метод вывода записей в виде массива в формате JSON.

//+------------------------------------------------------------------+
//| Вывод данных на страницу в формате JSON                          |
//+------------------------------------------------------------------+
TWRESULT CPageNumberTwo::JSONDataOutput(const Context *context,CHelloWorldManager *manager)
  {
   TWRESULT res = RES_S_OK;
//---
   if(context==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->response==NULL)        ReturnError(RES_E_INVALID_ARGS);
//---
   m_info_count=_countof(m_info);
   if(RES_FAILED(res=manager->InfoGet(context,m_info,m_start,&m_info_count)))
      ReturnErrorExt(res,context,"failed get info records");
//---
   CJSON json(context->response);
   json << CJSON::ARRAY;
   for(int i=0;i<m_info_count;i++)
     {
      json << CJSON::OBJECT;
      //---
      json << L"id"   << m_info[i].id;
      json << L"name" << m_info[i].name;
      //---
      json << CJSON::CLOSE;
     }
//---
   json << CJSON::CLOSE;
   return(RES_S_OK);
  }

5. В CPageNumberTwo::Tag поместите вызов этого метода в токен info_list.

TWRESULT CPageNumberTwo::Tag(const Context *context,const TagInfo *tag)
  {
..............................
//---
   if(TagCompare(L"info_list",tag))
     {
      JSONDataOutput(context,m_manager);
      //---
      return(false);
     }
..............................
  }

6. Измените блок обработки запроса, добавив туда вызов JSONDataOutput и PreparePages.

//+------------------------------------------------------------------+
//| Обработка запроса                                                |
//+------------------------------------------------------------------+
TWRESULT CPageNumberTwo::Process(const Context *context,
                                 IServer *server,
                                 const wchar_t *path,
                                 CHelloWorldManager *manager)
  {
//--- проверки
   if(context==NULL || path==NULL || manager==NULL || server==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL || context->user==NULL)                ReturnError(RES_E_INVALID_CONTEXT);
//---
   TWRESULT res=RES_S_OK;
//---
   m_server=server;
   m_manager = manager;
//--- Подготовка списка страниц нумератора
   if(RES_FAILED(res=PreparePages(context,manager)))
      return(res);
//--- 
   if(context->user->PermissionCheck(HELLOWORLD_MODULE_ID,HELLOWORLD_PERM_ADVANCED))
     {
      m_info_advanced_count=_countof(m_info_advanced);
      if(RES_FAILED(res=manager->InfoAdvacedGet(context,m_info_advanced,&m_info_advanced_count)))
         ReturnErrorExt(res,context,"failed get advanced info records");
     }
//--- AJAX-запрос данных для вывода на страницу
   if(context->request->AJAXRequest())
     {
      return(JSONDataOutput(context,manager));
     }
//--- отображаем страницу
   return(server->PageProcess(context, L"templates\\number2.tpl", this, TW_PAGEPROCESS_NOCACHE));
  }

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

//+------------------------------------------------------------------+
//| Удаление записи                                                  |
//+------------------------------------------------------------------+
TWRESULT CPageNumberTwo::OnDelete(const Context *context,
                                  IServer *server,
                                  const wchar_t *path,
                                  CHelloWorldManager *manager)
  {
   INT64    id =0;
   TWRESULT res=RES_S_OK;
//--- проверки
   if(context==NULL || path==NULL || manager==NULL)      ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL || context->response==NULL) ReturnError(RES_E_INVALID_CONTEXT);
//---
   if(!context->request->Exist(IRequest::POST,L"id") || 
     (id=context->request->GetInt64(IRequest::POST,L"id"))<=0)
      return(RES_E_NOT_FOUND);
//---
   if(RES_FAILED(res=manager->InfoDelete(context,id)))
      ReturnErrorExt(res,context,"failed to delete record");
//---
   if(RES_FAILED(res=PreparePages(context,manager)))
      return(res);
//---
   CJSON json(context->response);
   json << CJSON::OBJECT
        << L"from" << m_start
        << L"count" << m_info_total
        << L"data" << CJSON::DATA;
   if(RES_FAILED(res = JSONDataOutput(context,manager)))
      return(res);
   json << CJSON::CLOSE << CJSON::CLOSE;
   return(res);
  }

 

Добавление и изменение данных

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

//+------------------------------------------------------------------+
//| Страница редактирования                                          |
//+------------------------------------------------------------------+
class CPageEdit : public CPage
  {
private:
   IServer          *m_server;
   HelloWorldRecord  m_record;

public:
                     CPageEdit();
                    ~CPageEdit();
   //--- обработчик
   TWRESULT          Process(const Context *context,
                             IServer *server,
                             const wchar_t *path,
                             CHelloWorldManager *manager);
   //--- функции показа
   bool              Tag(const Context *context,const TagInfo *tag);
   //--- СОЗДАНИЕ (Create) и РЕДАКТИРОВАНИЕ (Update)
   TWRESULT          OnUpdate(const Context *context,
                              IServer *server,
                              const wchar_t *path,
                              CHelloWorldManager *manager);
};

2. При запросе данных методом InfoGet получите строку из таблицы и вызовите для нее метод OnUpdate.

//+------------------------------------------------------------------+
//| Обработка запроса                                                |
//+------------------------------------------------------------------+
TWRESULT CPageEdit::Process(const Context *context,
                            IServer *server,
                            const wchar_t *path,
                            CHelloWorldManager *manager)
  {
   TWRESULT res=RES_S_OK;
   INT64    id =0;
//--- проверки
   if(context==NULL || path==NULL || server==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL)                                       ReturnError(RES_E_INVALID_CONTEXT);
//---
   m_server=server;
//--- запросим данные
   if(PathCompare(L"number_two/edit/",path))
     {
      id=_wtoi64(path+16); // 16 - длина пути 'number_two/edit/'
      //---
      if(id>0 && RES_FAILED(res=manager->InfoGet(context,id,&m_record)))
         ReturnErrorExt(res,context,"failed to get record");
     }
//--- AJAX-запрос данных для вывода на страницу
   if(context->request->AJAXRequest())
     {
      return(OnUpdate(context,server,path,manager));
     }
   return(server->PageProcess(context, L"templates\\edit.tpl", this, TW_PAGEPROCESS_NOCACHE));
  }

3. Реализуйте метод OnUpdate.

//+------------------------------------------------------------------+
//| Сохранение записи в таблице HELLOWORLD                           |
//+------------------------------------------------------------------+
TWRESULT CPageEdit::OnUpdate(const Context *context,
                             IServer *server,
                             const wchar_t *path,
                             CHelloWorldManager *manager)
  {
   TWRESULT res=RES_S_OK;
   INT64    id =0;
//--- проверки
   if(context==NULL || path==NULL || manager==NULL) ReturnError(RES_E_INVALID_ARGS);
   if(context->request==NULL)                       ReturnError(RES_E_INVALID_CONTEXT);
//--- заполняем поля
   StringCchCopy(m_record.name, _countof(m_record.name), context->request->GetString(IRequest::POST,L"name"));
//--- сохраняем
   if(RES_FAILED(res=manager->InfoUpdate(context,&m_record)))
      ReturnErrorExt(res,context,"failed to update record");
//---
   return(RES_S_OK);
  }

 

Правила маршрутизации

1. В модуль к существующим добавьте два новых правила маршрутизации: одно - на удаление записей, второе - на добавление/редактирование записей.

//+------------------------------------------------------------------+
//| Роутинг по URL                                                   |
//+------------------------------------------------------------------+
TWRESULT CHelloWorldModule::ProcessPage(const Context *context, IServer *server, const wchar_t *path)
  {
   if(context==NULL || path==NULL) ReturnError(RES_E_INVALID_ARGS);
//---
   if(PathCompare(L"index",path))             return(CPageIndex().Process(context,m_server,path));
   if(PathCompare(L"number_one",path))        return(CPageNumberOne().Process(context,m_server,path));
   if(PathCompare(L"number_two/delete",path)) return(CPageNumberTwo().OnDelete(context,m_server,path,&m_manager));
   if(PathCompare(L"number_two/edit",path))   return(CPageEdit().Process(context,m_server,path,&m_manager));
   if(PathCompare(L"number_two",path))        return(CPageNumberTwo().Process(context,m_server,path,&m_manager));
//--- по умолчанию
   return(CPageIndex().Process(context,m_server,path));
  }

Обратите внимание, что, выполняя проверки, мы продвигаемся от частного к общему. Т.е. если бы у нас сначала проверялся путь number_two, а затем number_two/delete и number_two/edit, то на number_two проверка бы уже закончилась, и дальнейшей проверки путей не осуществлялось бы. Имейте это в виду.

2. Подключите новую страницу в модуль.

//+------------------------------------------------------------------+
//|                                                          TeamWox |
//|                 Copyright © 2006-2008, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net |
//+------------------------------------------------------------------+
#include "stdafx.h"
#include "HelloWorldModule.h"
#include "Pages\PageIndex.h"
#include "Pages\PageNumberOne.h"
#include "Pages\PageNumberTwo.h"
#include "Pages\PageEdit.h"

 

Пользовательский интерфейс

Теперь нам нужно подготовить пользовательский интерфейс для управления данными. В странице PageNumberTwo мы добавим элементы управления, вызывающие методы C.R.U.D., а также нумератор - элемент управления, отображающий вкладки с номерами страниц под таблицей с данными. В страницу PageEdit мы добавим редактор записей.

 

Страница PageNumberTwo: отображение, создание и удаление записей

1. Добавьте новый элемент управления PageNumerator.

//--- Создание таблицы и заполнение ее данными
var table = TeamWox.Control("ViewTable",table_cfg.id,table_cfg.header, RecordToTable(<tw:info_list/>))
               .AddHandler(top.TableHandlers.Ordering);
var pages = TeamWox.Control('PageNumerator',true,<tw:item_from/>,<tw:items_total/>,<tw:items_per_page/>)
       .Append('onchange',PageNumeratorChanged);

Первым его аргументом является флаг местоположения снизу от границы таблицы. Второй аргумент (токен item_from) - порядковый номер первого элемента на странице. Третий аргумент (токен items_total) - общее количество элементов в таблице. Последний аргумент (токен items_per_page) - количество отображаемых элементов на странице. Как видите, три последних параметра заданы в виде пользовательских токенов, которые мы реализуем ниже.

2. Методом Append добавляется обработчик на событие. В данном случае, событие onchange мы обрабатываем с помощью функции PageNumeratorChanged:

function PageNumeratorChanged(startFrom,perPage)
  {
   TeamWox.Ajax.get('/helloworld/number_two/',{from:startFrom},
     {
      onready:function (text)
        {
         var data;
         data = TeamWox.Ajax.json(text);
         table.SetData(RecordToTable(data));
         pages.Show(startFrom,pages.Total(),perPage);
        },
      onerror:function (status)
        {
         alert(status);
        }
     });
  }

Метод TeamWox.Ajax.get отправляет данные на сервер фоновым запросом методом GET. Первый параметр - это URL, на который идет отправка. Второй - параметры для отправки. Имена ключей будут использованы как параметры, значения - как их значения. Третий - объект, используемый для обратных вызовов.

Этот объект, в свою очередь, создается с помощью метода TeamWox.Ajax.json, который переводит строку в формате JSON в объект и наоборот. Если передается строка, метод пытается ее распознать как JSON объект, если передан объект - он кодируется в строку.

3. В CPageNumberTwo::Tag реализуйте токены item_from, items_total и items_per_page:

//---
   if(TagCompare(L"item_from",tag))
     {
      StringCchPrintf(tmp,_countof(tmp),L"%d",m_info_current);
      context->response->Write(tmp);
      return(false);
     }
//---
   if(TagCompare(L"items_total",tag))
     {
      StringCchPrintf(tmp,_countof(tmp),L"%d",m_info_total);
      context->response->Write(tmp);
      return(false);
     }
//---
   if(TagCompare(L"items_per_page",tag))
     {
      StringCchPrintf(tmp,_countof(tmp),L"%d",_countof(m_info));
      context->response->Write(tmp);
      return(false);
     }

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

//+----------------------------------------------+
//| Шапка страницы                               |
//+----------------------------------------------+
var header = TeamWox.Control("PageHeader","#41633C")
   .Command("<lngj:MENU_HELLOWORLD_LIST>","/helloworld/index",           "<lngj:MENU_HELLOWORLD_LIST>")
   .Command("<lngj:MENU_HELLOWORLD_NEW>", "/helloworld/number_two/edit/","<lngj:MENU_HELLOWORLD_NEW_DESCR>")
   .Help("/helloworld/index")
   .Search(65536);

5. В таблицу добавьте панель с двумя кнопками - редактирования и удаления, а также обработчик их нажатия.

//--- Функция записи данных из менеджера в таблицу (массив)
function RecordToTable(data)
  {
   var records = [];
   for(var i in data)
     {
      //--- Записываем данные в массив records
      records.push([
            {id:'number', content:data[i].id},
            {id:'name',   content:data[i].name,toolbar:[
                                                        ['edit',top.Toolbar.Edit],
                                                        ['delete',top.Toolbar.Delete]
                                                       ]}
      ]);
     }
   //---
   return records;
  }
//--- Создание таблицы и заполнение ее данными
var table = TeamWox.Control("ViewTable",table_cfg.id,table_cfg.header, RecordToTable(<tw:info_list/>))
            .AddHandler(top.TableHandlers.Ordering)
            .AddHandler(top.TableHandlers.Toolbar)
            .Append('ontoolbar',ToolbarCommand);
var pages = TeamWox.Control('PageNumerator',true,<tw:item_from/>,<tw:items_total/>,<tw:items_per_page/>)
            .Append('onchange',PageNumeratorChanged);               

6. Функция ToolbarCommand при наступлении события ondelete будет открывать заданный в маршрутизации URL, который будет вызывать функцию удаления записи OnDelete.

function ToolbarCommand(id,data)
  {
   switch(id)
     {
      case 'delete':
        TeamWox.Ajax.post('/helloworld/number_two/delete',{id:data[0].content,from:pages.Item()},false,
          {
           onready:function (text)
              {
               var data;
               data = TeamWox.Ajax.json(text);
               table.SetData(RecordToTable(data.data));
               pages.Show(data.from,data.count,pages.PerPage())
              },
            onerror:function (status)
              {
               alert(status);
              }
           });
        break;
      case 'edit':
        document.location = '/helloworld/number_two/edit/'+data[0].content;
        break;
     }
  }

 

Страница PageEdit: редактирование записей

1. Создайте в проекте шаблон edit.tpl для страницы PageEdit, добавьте в него необходимые HTML-теги и подключите библиотеку TeamWox для формирования базового окружения.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
   <link href="<tws:filestamp path='/res/style.css' />" rel="stylesheet" type="text/css" />
</head>
<body>
   <script type="text/javascript">
   top.TeamWox.Start(window);
   </script>
</body>
</html>

2. Создайте шапку страницы, в которой будет одна команда, возвращающая нас на страницу PageNumberTwo.

TeamWox.Control("PageHeader","#41633C")
       .Command("<lngj:MENU_HELLOWORLD_LIST>","/helloworld/number_two","<lngj:MENU_HELLOWORLD_LIST>");

3. Создайте новый объект - форму ввода.

TeamWox.Control('Form',
  {
   action:'/helloworld/number_two/edit/<tw:id />',
   method:'post',
   type:'table',
   buttons:'save',
   items:[
          [TeamWox.Control('Label','Name','name'),TeamWox.Control('Input','text','name','<tw:name />')]
         ]
  }).Style({'width':'500px','margin':'5px'})
    .Append('onformsend',function (){document.location='/helloworld/number_two/'})
    .Append('onformerror',function (){alert('Network error');});

Это элемент управления с табличным типом отображения (type: 'table') и кнопкой (buttons: 'save'), которая методом POST (method: 'post') отправляет HTTP-запрос по URL, указанному в параметре action.

В этой таблице одна строка с двумя столбцами. В первом столбце отображается текстовая метка (тип объекта Label) с текстом Name, которая привязана к полю name. Соответственно, вторым столбцом идет форма ввода (объект типа Input) с именем text, в которой вводится значение name. Если это новая запись в таблице, то поле будет пустым, если редактируется существующая запись - четвертым параметром с помощью токена tw:name в поле вставляется текущее значение записи.

При успешной отправке данных через форму (событие onformsend) выполнится автоматический переход на страницу PageNumberTwo. Если при отправке данных произойдет ошибка (событие onformerror), например сбой в подключении, то браузер оповестит нас об этом стандартным методом.

4. Реализуйте использованные ранее токены id и name в исходном коде страницы PageEdit.

//+------------------------------------------------------------------+
//| Обработка тэга                                                   |
//+------------------------------------------------------------------+
bool CPageEdit::Tag(const Context *context,const TagInfo *tag)
  {
   wchar_t str[64]={0};
//--- проверки
   if(context==NULL || tag==NULL || m_server==NULL)  ReturnError(false);
   if(context->request==NULL || context->user==NULL) ReturnError(false);
//---
   if(TagCompare(L"id",tag))
     {
      if(m_record.id>0)
        {
         StringCchPrintf(str,_countof(str),L"%I64d",m_record.id);
         context->response->WriteSafe(str,IResponse::REPLACE_JAVASCRIPT);
        }
      return(false);
     }
//---
   if(TagCompare(L"name",tag))
     {
      if(m_record.name!=NULL && m_record.name[0]!=NULL)
        {
          context->response->WriteSafe(m_record.name,IResponse::REPLACE_JAVASCRIPT);
        }
      return(false);
     }
//--- 
   return(false);
  }
//+------------------------------------------------------------------+

 

Демонстрация работы пользовательского интерфейса

Мы реализовали необходимый функционал C.R.U.D. Теперь осталось проверить, как все это работает. Скомпилируйте модуль, скопируйте шаблоны на сервер и запустите TeamWox.

 

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

1. Перейдите на страницу PageNumberTwo нажмите команду создания новой записи.

Создание новой записи

2. Введите текст и нажмите кнопку сохранения.

Ввод текста для записи

3Запись добавилась в таблицу.

Новая запись в таблице

 

Редактирование записей

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

Редактирование записи

2. Отредактируйте запись, введя новые данные.

Изменение записи

Запись изменилась.

Запись изменилась

 

Нумератор

1. Добавьте в таблицу несколько записей. В массиве m_info[5] мы указали количество записей на одну вкладку нумератора. Если записей в таблице станет больше 5, то создадутся вкладки, на каждой из которых будет выводиться максимум по 5 записей.

Первые 5 записей

2. При переходе на следующую вкладку будет отображаться следующая порция данных.

Следующая порция данных

 

Удаление записей

1. Протестируйте удаление данных. Для этого в таблице выберите кнопку удаления.

Удаление записи

2. Запись удаляется, после чего данные в таблице автоматически обновляются.

Запись удалена

 

Заключение

Итак, мы рассмотрели как с помощью средств SDK взаимодействовать с СУБД TeamWox. Статья получилась довольно объемной, поскольку в ней детально изложены изменения в исходных кодах модуля.


2010.10.28