Шаг 125 - Немного о базах данных

Привет Артем. Несколько дней назад получил письмо следующего содержания:

From mpv@police.a-teleport.com Thu Feb 24 12:41:17 2000
From: "Pavel Mojseenko V." <mpv@police.a-teleport.com>
To: <lerick@mail.ru>
Subject: =Пару вопросов по MFC, VC и базам данных

    Как можно реализовать в VC c помощью MFC (или без нее) при работе с
	реляционной базой данных форму ввода по подобию  Access, где в одной
	форме реализован ввод информации сразу в 2 и более таблиц, связанные
	между собой отношением 1 ко многим. Во всех учебниках и у Вас на сайте
	рассматривается подключение с помощью AppWizard-a только к не 
	реляционной модели БД (т.е. представленных одной таблицей). Если есть
	возможность опишите это в одном из шагов.

Я конечно не гуру в VC++, но у меня созрел вариант как это можно реализовать средствами VC++. Кстати, я во многом основывался на Ваших шагах + немножко эксперимента. Не думаю, что он идеален. Просто как вариант, где используются только Мастер и немножко SQL.

Создаем БД в MS Access с именем TestDB.mdb. Моя подопытная БД состоит из двух таблиц: Customer и Product. Не буду объяснять как подобную БД можно создать с помощью среды Access. Просто создайте эту БД и запомните ее местонахождение.

Структура таблицы Customer:

ID - идентификационный номер клиента (тип: счетчик)

CustName - имя клиента (тип: текст)

Структура таблицы Product:

ID - идентификационный номер клиента (тип: число)
Product - название товара (тип: текст)

Смысл БД достаточно прост: в одной таблице находится список клиентов(Customer), а в другой список товаров(Product), которые покупает клиент. Они имеют связь по полю ID соответственно (один-ко-многим).

Допустим, нам надо добавить товар купленный клиентом. Как это можно сделать с помощью одной формы ?

Создаем с помощью AppWizard(exe) новый проект с именем DB.

Step 1 - Single document
Step 2 - Database view without file support

Жмем кнопку Data source, указываем как показано на рисунке:

125_1.gif (5020 b)

Каждый раз нас будут спрашивать: где искать БД ? Впрочем, можно и описать путь к БД, кому как удобнее (см. "Шаг 110 - Готовим данные для записной книжки"). Жмем OK. Затем спросят путь к БД и какие таблицы нас интересуют:

125_2.gif (3029 b)

Выбираем обе таблицы (держим Shift и кликаем по искомым таблицам)

Жмем на OK, а потом уже можно и на Finish. Итак, мы создали проект с использованием БД.

В ресурсах найдите диалоговое окно ID_DB_FORM (или что-то в этом роде) и приведите его примерно к такому же виду, как у меня. Впрочем, принципиально здесь только наличие элементов Edit. У меня получилось: IDC_EDIT1 (ID), IDC_EDIT2 (Customer), IDC_EDIT3 (Product name), IDC_EDIT4 (New Product)

125_3.gif (3216 b)

Слева у меня получились поля из таблицы Customer, а справа - из таблицы Product. Свяжите первые три элемента Edit с соответствующими полями таблиц (m_ID, m_CustName, m_ProdName соответственно). Как это сделать Артем уже достаточно подробно описывал в Шаге 111 (MFC). Скажите ему спасибо. С четвертым же элементом Edit (IDC_EDIT4) я связал две переменные: m_Add(Category: Value; Variable type: CString) и m_AddCtl(Category: Control; Variable type: CEdit). Пояснения как это можно сделать найдете в "Шаг 30 - Первый проект с AppWizard", "Шаг 34 - Понимание DDX". Надеюсь, что все пока получается.

Теперь обратимся к тому, что нам Мастер сделал. Он, как умный парень, считает, что нам может понадобиться только одна таблица. И результирующей у нас будет только одна таблица. Поэтому при запуске проекта Вы увидите в панели инструментов стрелочки для передвижения по записям только для одной таблицы. Можете поиграться с тем, что нам сотворил Мастер, но все равно это не то, что нужно. Я же, как человек достаточно ленивый, не захотел особо что-то переделывать, напрягаться, и решил применить свои скромные знания языка SQL. Я просто сделаю из двух таблиц одну с помощью запроса SQL.

Мастер при создании проекта, помимо всего прочего "классового добра", сгенерировал нам два класса CDBSet(родитель - CRecordset) и CDBView(родитель - CRecordView). С ними и поработаем.

Класс CDBView отвечает за то, что мы увидим на экране. В нем описана переменная m_pSet, которая является указателем на объект класса CDBSet. При конструировании объекта класса CDBView

CDBView::CDBView()
	: CRecordView(CDBView::IDD)
{
	//{{AFX_DATA_INIT(CDBView)
	m_pSet = NULL;
	m_Add = _T("");
	//}}AFX_DATA_INIT
}
я особо хочу отметить строку
	m_pSet = NULL;

Дело в том, что, как можно заметить, класс CDBSet наследован из класса CRecordset и работает абсолютно также, как и родительский класс. При открытии таблицы с помощью CRecordset используется обычный SQL запрос. При инициализации объекта так, как это показано в конструкторе CDBView::CDBView(), т.е. когда указателю присваивается значение NULL, соединение и запрос формируются с помощью функций CDBSet::GetDefaultConnect() и CDBSet::GetDefaultSQL(). Нас интересует функция CDBSet::GetDefaultSQL(). Я ее подправил так, чтобы она выглядела следующим образом:

CString CDBSet::GetDefaultSQL()
{
	return _T("[Customer] Left Outer Join [Product] ON Product].ID=[Customer].ID");
}

Это только часть SQL запроса, которая следует после служебного слова FROM запроса. С помощью этого запроса я связываю наши таблицы по полю ID. Я бы Вам объяснил как это работает, но это наверное намного лучше объяснено в любом описании языка SQL. Смотрите раздел про Outer Join. Лучше взгляните на результат. С помощью стрелочек перехода с записи на запись можно последовательно просматривать все товары, записанные на каждого клиента.

Наконец-то добрался до самого важного момента. У нас остался один незатронутый элемент управления - кнопка "Add New". Создайте для нее реакцию на событие OnClick. Артем много раз показывал как это можно сделать. При нажатии на эту кнопку произойдет добавление нового товара(название берется из введенного в элемент управления IDC_EDIT4) текущему клиенту.

У меня получилась следующая функция:

void CDBView::OnButton1() 
{
	UpdateData();   
if (m_Add!="")  
  {
  m_AddCtl.SetSel(-1,0);  // удаляем любое возможное выделение в элементе IDC_EDIT4
  m_AddCtl.SetSel(0,-1); // выделяем все символы в поле элемента IDC_EDIT4
  m_AddCtl.Clear(); // очищаем

   char buffer[4];
   CString str;
   _itoa(m_pSet->m_ID,buffer,10);
   str=buffer; 
   m_pSet->m_pDatabase->ExecuteSQL("insert into [Product] (ID,ProdName) Values ("+str+",'"+m_Add+"')");
   UpdateData();
   m_pSet->Requery();	
   m_pSet->MoveFirst();
   UpdateData(false);
  }
 else
	 AfxMessageBox("Поле ввода не должно быть пустым !!!");
}

Сначала обновляем значения таких переменных, как m_Add, m_AddCtl и всех остальных, которые описаны в функции CDBView::DoDataExchange(CDataExchange* pDX). Если переменная m_Add содержит не пустую строку, то производим операцию добавления. Затем производим очищение поля элемента IDC_EDIT4. Однако отмечу, что само очищение произойдет немножко позже, когда мы вызовем функцию UpdateData(). Преобразуем с помощью стандартной функции _itoa значение идентификатора ID типа long в символьный вид, но нам нужна переменная типа CString, чтобы применить полученное значение при формировании запроса SQL.

В переменной m_pDatabase хранится ссылка на БД(класс CDatabase), с которой мы работаем. В свою очередь, в классе CDatabase есть функция ExecuteSQL, с помощью которой можно выполнить любой запрос к БД(но без возвращения каких-либо значений). Этим мы и воспользовались и вставили с помощью запроса SQL новую запись в таблицу Product.

Затем обновляем(перечитываем) наш набор данных с помощью функции Requery, перемещаем указатель на первую запись с помощью функции MoveFirst и обновляем отображение с помощью UpdateData(false).

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

Прислал Valeri Khromov.


Предыдущий Шаг | Следующий Шаг | Оглавление
Автор Каев Артем - 29.02.2000