Блог → Знакомимся с удобным и уникальным классом CODBCRecordset

Те, кому приходилось возиться с базами данных из-под Visual C++, думаю, знают, что есть масса способов это делать, и все они не особенно удобные. Есть хороший стандарт ODBC (Open DataBase Connectivity), предоставляющий нормальный SQL-интерфейс к "абстрактным" базам данных (независимо от их конкретной реализации). Но соответствующая ему библиотека API достаточно громоздка и для изучения и для использования, а "удобная" и "невероятно easy-to-use" MFC-обёртка, как обычно, реализует дай бог если половину API-шных функций, и при этом имеет массу проблем и недочетов.

В частности, стандартный MFC'шный способ доступа к содержимому БД такой: для каждого конкретного запроса SQL заводим класс, унаследованный от CRecordset. При его создании в ClassWizard'e выбираем одну или несколько таблиц БД, и связываем некоторое подможество их полей (колонок) с полями нашего класса. Например, для какой-нибудь несчастной таблички Tovar:

Tovar_ID: INTEGER
Tovar_Name: VARCHAR (4B)
Tovar_Price: DECIMAL (10,3)


из которой мы просто хотим выбрать всё, что есть (запрос типа SELECT *), получим целый

class CTovarRecordSet: public CRecordset
{public:
CTovarRecordSet(CDatabase* pDatabase = NULL);
DECLARE_DYNAMIC(CTovarRecordSet)

// Field/Param Data
//{{AFX_FIELD(CTovarRecordSet, CRecordset)
int m_Tovar_ID;
CString m_Tovar_Name;
CString m_Tovar_Price;
//})AFX_FIELD

// Overrides
// ClasslDizard generated virtual function ouerrides
//{{AFX_VIRTUAL(CTovarRecordSet)
virtual CString GetDefaultConnect(); // Default connection string
virtual CString GetDefaultSQL(); // Default SOL for Recordset
virtual void DoFieldExchange(CFieldExchange* pFX); // AFX support
//})AFX_VIRTUAL

// Implementation
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
};


Дальше можем поправить сам SQL-запрос (SELECT), посредством которого будем выбирать данные, a MFC заботливо возьмёт на себя обмен данными с ODBC-драйвером (который, в свою очередь, будет общаться с конкретной БД). В документации всё это выглядит красиво, но на практике - сразу же появляется масса проблем!

Во-первых, связывание запроса с классом происходит абсолютно статично. При изменениях структуры БД (а в ходе почти любого проекта такие изменения будут, даже и не сомневайтесь), порой приходится подолгу мучать ClassWizard и код, чтобы привести всё в божеский вид. Во-вторых, сами Recordset-классы являются довольно-таки бессмысленной и пустой обёрткой к соответствующим таблицам и запросам. Прикиньте количество таблиц, форм и запросов в среднем БД-проекте, и заметьте, что для каждого придется завести отдельный класс (читай - два файла, .h и .срр). Представляете, сколько мусора приходится терпеть в проекте - и в среде (IDE), и на диске, и в результирующем коде? Наконец, в третьих - статичность мэппинга полей CRecordset-классов приводит к тому, что доступ, например, к переменным БД (не прописанным ни в одной таблице) становится занятием крайне нетривиальным. Например, ужас как хочется узнать значение autoincrement-поля (через ту же @@identity) - но MFC понятия не имеет, как представить данные, возвращенные БД по соответствующему SQL-запросу.

К счастью, свет не без умных людей. И все перечисленные проблемы, как выяснилось, легко решаются с помощью одного нового класса - "универсальной" обёртки к MFC-шному RecordSet'y, которая динамически разбирается в структуре данных, возвращаемых ODBC-драйвером, сама распознает имена полей и формат их значений. Всё, что требуется от программиста, это включить в свой проект один-единственный класс CODBCRecordset, и забыть о десятках никому не нужных MFC-шных Recordset'ов.

Экземпляры нового класса инициализируются явным запросом SQL, а к полям сответствующего Recordset'а можно обращаться прямо по имени (или alias'у) - все необходимые преобразования данных выполняются автоматически (хотя можно их задать и в явном виде). Итак, аплодисменты Стефану Чеканову (Stefan Tchekanov, stefant@iname.com) - и представляем просвещенной публике его класс CODBCRecordset.

По сути, CODBCRecordset унаследован от стандартного CRecordset, и использует "легальный" MFC-шный механизм обмена данными (RFX, Record Field exchange). Соответственно, он полностью совместим с интерфейсом ODBC и его MFC-оберткой (классом CDatabase). Значения полей хранятся в объектах типа CDBField - который тоже унаследован от стандартного MFC-класса CDBVariant (с некоторыми полезными и удобными усовершенствованиями). Полный список методов CODBCRecordset и CDBField мы, разумеется, приводить не будем (скачайте исходник и посмотрите сами, help там тоже имеется). Пройдемся по наиболее интересному.

Инициализатор Open(...) - берёт в качестве основного аргумента запрос (CString IpszSQL), написанный на чистом SQL (в отличие от MFC-шного "дробления" запроса на части - что, по-моему, только мешает восприятию). "Что это нам такое вернули" - можно разобраться с помощью функций short GetODBCFieldCount() - возвращает количество полей (т.е. колонок) в Recordset'e; int GetFieldlD( LPCTSTR ) и CString GetFieldName( int ) - возвращают номер поля по его (символьному) имени и наоборот.

Конечно, предусмотрена масса способов запросить значение конкретного поля - по его имени или номеру (CDBFieldS operator(LPCTSTR), CDB Fields operator(int)), в любом мыслимом виде: GetBool(), GetChar(), GetBinary(), со всеми дозволенными проверками и преобразованиям типов и т.д. Использовать класс CODBCRecordset действительно элементарно:

// Конструктор требует указатель на (уже открытую) ODBC-базу, CDatabase*
CODBCRecordset rs(&db);
// далее - просим исполнить наш запрос
rs.0pen(
"SELECT * FROM Tovar \
WHERE Tovar_Price < '100.00' \
ORDER BY Tovar_Name");

for(; ! rs.IsEOF(); rs.MoveNext() )
{// и перебираем множество возвращенных записей:
double price;
// обращаться к полям можно так: (совсем явно и официально)
price = rs.Field("Tovar_Price").RsDouble();
price = rs.GetDouble("Tovar_Price");
// а можно вот так запросто: (необходимые преобразования вызовутся автоматически)
price = rs.Field("Tovar_Price");
// и даже
price = rs( "Tovar_Price");
// так же запросто редактируем: rs.Edit();
rs("Tovar_Price") = 0; // халява!
rs.Updated;
}


В сложных случаях (например, когда мы берем значение поля одного Recordset'а и тут же присваиваем его полю другого) лучше, конечно, записывать преобразования типов в явном виде. Но в общем и целом, налицо невиданная легкость и гибкость обращения с ODBC-БД вам обеспечена!

Пара замечаний напоследок. В случае, если в результате запроса возвращаются несколько колонок с одинаковым именем (всяческие сложные JOIN'ы) - CODBC Recordset слегка переименует дубликаты (добавив к "повторяющемуся" имени поля "1", "2" и т.д.). В такой ситуации безопаснее и нагляднее, конечно, переименовать дубликаты сразу в тексте запроса ("SELECT NewPrices.Tovar_Price AS New_Tovar_Price ..."). В отличие от других подобных реализаций, CODBCRecordset позволяет корректно открывать несколько (больше одного) recordset'ов в рамках одного ODBC-соединения с БД MSSQL Server. Ну и самое главное - скачать исходники CODBCRecordset можно здесь. Ура!