Базы данных ADO.NET на языке C++\CLI


Visual Studio работает с базами данных (БД) с помощью библиотеки ADO.NET, которая позволяет осуществлять доступ к различным типам серверов, одним из которых является Microsoft SQL Server (MSSQL). Он поставляется вместе с самой Visual Studio, и последняя обладает встроенной поддержкой для создания, удаления и редактирования БД на сервере MSSQL без использования дополнительных сторонних программ.

Первый шаг при разработке программы для работы с базами данных – это создание самой базы. Выберите в меню Вид \ Другие окна \ Обозреватель серверов, в появившемся окне нажмите правой кнопкой мыши по «Подключения данных» и выберите «Создать новую базу данных SQL Server».



В качестве имени сервера введите (LocalDB)\v11.0.

MSSQL поддерживает 2 типа авторизации: по имени пользователя Windows, или по своему собственному списку пользователей. При использовании авторизации SQL Server Authentication необходимо ввести имя пользователя и пароль. Выберите «Использовать аутентификацию Windows» и укажите имя для новой базы данных.

Если не возникнет ошибок подключения или авторизации, то будет создана новая БД, и в окне Обозреватель серверов в разделе Подключения данных появится новая ветка с именем базы. Если ее раскрыть, то появятся записи для работы с этой базой.

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

Сначала определим, какую информацию мы будем хранить о телефонах. В нашем случае, например, для упрощения будем учитывать 6 характеристик: производитель, модель, год выпуска, цвет, вес, изображение телефона. Если просто создать таблицу с заданными полями, то мы сможем хранить всю интересующую информацию, но это будет не оптимальное решение.

Все поля в общем случае будут иметь уникальное значение от телефона к телефону, за исключением производителя и года выпуска. Год можно кодировать целым числом типа int, поэтому данное поле не критично, но вот название производителя будет повторяться часто, тем более оно является строковым параметром, и его хранение в одной таблице с моделями телефонов нецелесообразно, так как попросту приведет к дублированию строк. А теперь представим, что производитель сменил свое наименование, а у нас в таблице уже содержатся тысячи моделей данной фирмы, которые потребуется все переименовывать!

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

Еще одним приемом при проектировании БД, позволяющим упростить и ускорить работу с базой, является наличие уникального номера-идентификатора в таблицах (ключа), однозначно определяющего строку таблицы. Гораздо удобнее и быстрее обращаться к записи в таблице в виде «номер 231», чем «фирма Nokia, модель …, год …, цвет …, вес …, …».

Начнем с создания таблицы производителей. Если раскрыть ветку с созданной базой данных в дереве в окне Обозреватель серверов, то появятся записи Таблицы, Представления, Типы и другие объекты базы. Нажмите правой кнопкой по Таблицы и выберите Добавить новую таблицу. Появится окно ввода имени полей таблицы, их типов и «Допустимы значения Nulls» (может ли поле не содержать значения, т.е. быть пустым). Создайте следующие поля: ID - тип int и Name - тип nvarchar(20).

Поле ID – уникальный числовой идентификатор типа int. Чтобы сервер MSSQL сам назначал номера при добавлении новых записей в таблицу, в свойствах поля в списке выберите Спецификация идентификатора и установите значения True для (Идентификатор).

Поле Name – название фирмы, nvarchar(20) означает строка длиной до 20 символов.

Измените имя таблицы на Firms и нажмите кнопку «Обновить».

Теперь создайте еще одну таблицу Models для моделей телефонов с полями:

Имя поля Тип
ID int
ID_Firm int
model nvarchar(20)
year int
color nvarchar(15)
weight int
photo image

Названия полей говорят сами за себя, поле ID_Firm будет содержать номер фирмы, т.е. значения из поля ID в таблице Firms.

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

Вызовите таким образом на редактирование таблицу Firms, нажмите правой кнопкой на поле Индексы, и в появившемся меню выберите «Добавить новый индекс». В свойствах созданного индекса установите параметр Уникальный в значение True. В параметре Столбцы выберите поле ID. Нажмите правой кнопкой мыши на поле ID таблицы, и в появившемся меню выберите «Задать первичный ключ». Т.е. поле ID теперь является уникальным ключом (идентификатором) для записей в таблице. Обновите базу данных.



Теперь отредактируем таблицу Models, по аналогии нажмите правой кнопкой на пункте «Внешние ключи», и в появившемся меню выберите «Добавить новый внешний ключ». Внешние ключи позволяют задавать связи между таблицами, в нашем случае надо сообщить серверу, что поле ID_Firm связано с полем ID из таблицы Firms.



Обновите базу данных.

Теперь можно занести данные в таблицу. Нажмите правой кнопкой по имени таблицы, выберите меню Показать таблицу данных.

Для таблицы Firms:



Для таблицы Models:



Теперь, когда БД готова, можно приступать к написанию программы. Создайте проект Windows Forms, поместите на форму компонент SplitContainer. На его левую панель поместите DataGridView (поменяйте имя на dgvFirms, выравнивание Dock на Fill). На правую панель поместите компонент Panel c выравниваем по низу Dock = Bottom, на эту панель поместите 3 кнопки «Добавить», «Редактировать», «Удалить» с именами btnModelAdd, btnModelEdit и btnModelDel.

На оставшееся место в правой панели SplitContainer добавьте еще один компонент DataGridView с именем dgvModels и выравниваем Fill:



Чтобы появились заголовки в компонентах DataGridView (далее будем называть просто грид) как показано на рисунке, нажмите сначала правой кнопкой мыши на левом гриде, и выберите Правка столбцов:



Данный грид будет содержать имена фирм, поэтому добавьте в него 2 столбца с именами (св-во Name) colFirmID и colFirmName. Header text это то, что будет отображаться в заголовке столбца в самом гриде, поэтому желательно занести соответствующие русские названия. Столбец colFirmID будет хранить идентификатор записи и отображаться не будет, поэтому установите у него свойство Visible в false.

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



Имена для столбцов соответственно: colModelID, colModelName, colModelYear, colModelColor, colModelWeight, colModelPhoto. Последний столбец будет отображать фото телефона, поэтому поменяйте его тип на DataGridViewImageColumn. Первый столбец будет содержать идентификатор ID моделей, поэтому сделайте его невидимым Visible = false.

Установите у обоих гридов свойства AllowUserToAddRows = false, AllowUserTDeleteRows = false и ReadOnly = true, что отключит непосредственное редактирование в гридах (мы будем пользовать отдельные кнопки). SelectionMode = FullRowSelect и ColumnHeadersHeightSizeMode = AutoSize, AutoSizeColumnsMode = Fill чтобы выбирать курсором строки целиком и растягивать столбцы по ширине грида соответственно.

Отдельно у правого грида dgvModels установите AutoSizeRowsMode = AllCellsExceptHeaders, чтобы высота строк в нем растягивалась по высоте изображения телефона.

Всего компонент DataGridView содержит более 200 свойств и событий для настройки отображения данных, начиная со смены цвета фона ячеек до их ручной прорисовки с помощью библиотеки GDI+ и т.п., но это выходит за рамки нашей статьи.

Все классы для работы с MSSQL находятся в соответствующем пространстве имен, поэтому добавьте к остальным директивам using в начале файла:

using namespace System::Data::SqlClient;
Запустите программу и проверьте на наличие ошибок.

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

Добавьте внутрь класса формы переменные:

public:
	SqlConnection^ Con;
	DataTable ^dtFirms;
	DataTable ^dtModels;
	bool UpdatingFirms;
	int id_firm;
Переменная Con будет хранить соединение к базе данных, объекты класса DataTable используются для данных таблиц, булева переменная-флаг UpdateFirms пригодится позже, переменная id_firm – идентификатор текущей выбранной фирмы.

Добавьте в конструктор формы код (отмечен жирным шрифтом):

	public:
		Form1(void)
		{
			InitializeComponent();
			//
			//TODO: Add the constructor code here
			//
			try {
				Con = gcnew SqlConnection("Data Source=.\\SQLEXPRESS;Initial Catalog=mobile;User ID=sa;Password=111");
				Con->Open();
			} catch (SqlException ^e) {
				MessageBox::Show("Ошибка при подключении к базе данных: " + e->Message);
				Application::Exit();
			}
            dtFirms = gcnew DataTable();
            dtModels = gcnew DataTable();

		 dgvFirms->DataSource = dtFirms;
    		 dgvFirms->AutoGenerateColumns = false;
      	 colFirmID->DataPropertyName = "ID";
	       colFirmName->DataPropertyName = "name";

		 dgvModels->DataSource = dtModels;
    		 dgvModels->AutoGenerateColumns = false;
      	 colModelID->DataPropertyName = "ID";
	       colModelName->DataPropertyName = "model";
	       colModelYear->DataPropertyName = "year";
	       colModelColor->DataPropertyName = "color";
	       colModelWeight->DataPropertyName = "weight";
	       colModelPhoto->DataPropertyName = "photo";
		}
При создании соединения с БД используется строка соединения (Connection String), содержащая параметры подключения: имя сервера, базы, логин, пароль и др. Ее можно посмотреть, если нажать правой кнопкой в окне Обозреватель серверов на соединении с БД в ветке Подключения данных и выбрать Свойства. Хранение строки непосредственно в коде программы не лучшее решение, так как при смене настроек сервера БД, программу придется перекомпилировать, поэтому чаще ее хранят во внешних конфигурационных файлах.

Конструкция try .. catch используется для перехвата ошибок при исполнении программы. Если в коде, расположенном в блоке try {} произойдет ошибка (исключение Exception), то управление будет немедленно передано в блок catch {}. Исключение является классом, тип класса определяет вид произошедшей ошибки (в нашем случае ошибка класса SqlException), и объект данного класса будет помещен в переменную e для дальнейшей обработки программой.

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

После подключения создаются таблицы для данных, они назначаются гридам на форме в качестве источников для отображения информации, и идет присоединение столбцов к именам столбцов таблиц в БД. Свойство грида AutoGenerateColumns определяет, будет ли грид сам создавать столбцы. Так как мы их уже создали сами, то отключаем.

Добавьте в класс формы 2 функции:

void RefreshFirms() {
		UpdatingFirms = true;
		SqlCommand ^c = gcnew SqlCommand("select * from Firms", Con);
        	SqlDataReader^ r = c->ExecuteReader();
		dtFirms->Rows->Clear();
		dtFirms->Load(r);
		UpdatingFirms = false;
	}

	void RefreshModels(int id_firm) {
		if (UpdatingFirms || id_firm == -1) return;
		try {
			SqlCommand ^c = gcnew SqlCommand("select * from Models where ID_Firm=" + id_firm, Con);
			SqlDataReader^ r = c->ExecuteReader();
			dtModels->Rows->Clear();
			dtModels->Load(r);
		} catch (Exception ^e) {
		}
	}
Первая делает запрос к БД для получения списка фирм из таблицы Firms и заносит данные в объект dtFirms. Вторая проделывает похожую операцию, но для моделей. При этом выбираются не все модели, а только те, которые относятся к текущей выбранной фирме (выборка select по идентификатору фирмы). Если фирма не выбрана (id_firm == -1), или в данный момент выполняется ф-ция RefreshFirms(), то выборка не происходит. Дело в том, что класс SqlDataReader используемый при получении данных не может работать параллельно с другим таким же классом, поэтому для исключения такой вероятности используется флаг UpdatingFirms.

Назначьте для события SelectionChanged компонента dgvFirms обработчик:

private: System::Void dgvFirms_SelectionChanged(System::Object^  sender, System::EventArgs^  e) {
			 try {
				 id_firm = Convert::ToInt32(dgvFirms->CurrentRow->Cells[0]->Value);
			 } catch (Exception ^e) {
				 id_firm = -1;
			 }
			 RefreshModels(id_firm);
		 }
Событие возникает, когда происходит перемещение курсора по фирмам, при этом нам необходимо получить ID выбранной фирмы (CurrentRow – выбранная строка в гриде, Cells[0] – первый столбец, содержащий идентификаторы), и обновить список моделей телефонов. Если при получении ID фирмы возникнет ошибка, то id_firm = -1 и ф-ция RefreshModels обновлять список не будет.

Осталось заполнить гриды фирмами и моделями при первом запуске программы, для этого воспользуемся событием Shown формы:

private: System::Void Form1_Shown(System::Object^ sender, System::EventArgs^ e) { RefreshFirms(); dgvFirms_SelectionChanged(nullptr, nullptr); } Запустите программу, попробуйте выбирать разные фирмы для смены моделей. Фактически мы добились просмотра нашей БД сотовых телефонов, пора приступать к редактированию, для чего уже заранее были созданы 3 кнопки в нижней части окна.

Код для кнопки «Добавить»:

private: System::Void btnModelAdd_Click(System::Object^  sender, System::EventArgs^  e) {
			 String ^sql = String::Format("insert into Models (ID_Firm, model) values ({0}, N'{1}')",
				 id_firm, "Новая модель");
			 SqlCommand ^c = gcnew SqlCommand(sql, Con);
			 c->ExecuteNonQuery();
			 RefreshModels(id_firm);
		 }
С помощью sql запроса insert добавляется новая запись в таблицу моделей с идентификатором текущей выбранной фирмы. Так как sql команда insert (как и все последующие) не возвращает данные в программу, то используется метод ExecuteNonQuery(). После добавления обновляем список моделей, чтобы изменения отразились в гриде.

Код для кнопки «Удалить»:

private: System::Void btnModelDel_Click(System::Object^  sender, System::EventArgs^  e) {
			 if (dgvModels->RowCount == 0) return;
			 int id = Convert::ToInt32(dgvModels->CurrentRow->Cells[0]->Value);
			 String ^sql = "delete from Models where id=" + id;
			 SqlCommand ^c = gcnew SqlCommand(sql, Con);
			 c->ExecuteNonQuery();
			 RefreshModels(id_firm);
		 }
Если в гриде моделей нет строк, то удалять нечего и сразу выходим. Иначе получаем идентификатор выбранной модели и удаляем запись с помощью sql запроса delete.

Для редактирования моделей потребуется создать новую форму, назовите ее EditModel:



Присвойте текстовым полям имена tbModel, tbYear, tbColor и tbWeight. Поле под кнопкой «Загрузить» – компонент PictureBox, задайте для него имя pbPhoto, свойство SizeMode = CenterImage. Для кнопок загрузить, отменить, сохранить – имена btnLoadPhoto, bntCancel и btnSave соответственно.

Для свойства формы CancelButton установите кнопку btnCancel, что позволит ей закрывать окно без написания какого-либо кода. Так как в этой форме будем работать с БД, то добавьте в начало файла using namespace System::Data::SqlClient;. В класс формы добавьте переменные:

public:
	SqlConnection ^con;
	DataGridView ^dgv;
	int id;
Через них будет передавать в эту форму из главной объект SqlConnection чтобы иметь возможность выполнять sql команды, и грид моделей чтобы заполнить поля формы информацией о модели. Переменная id для хранения идентификатора модели.

Чтобы при появлении формы на экране в полях уже содержалась информация, создадим событие Shown:

private: System::Void EditModel_Shown(System::Object^  sender, System::EventArgs^  e) {
			 id = Convert::ToInt32(dgv->CurrentRow->Cells[0]->Value);
			 tbModel->Text = Convert::ToString(dgv->CurrentRow->Cells[1]->Value);
			 tbYear->Text = Convert::ToString(dgv->CurrentRow->Cells[2]->Value);
			 tbColor->Text = Convert::ToString(dgv->CurrentRow->Cells[3]->Value);
			 tbWeight->Text = Convert::ToString(dgv->CurrentRow->Cells[4]->Value);

			 using namespace System::Drawing;
			 using namespace System::IO;
			 try {
				 array<Byte> ^b = (array^)dgv->CurrentRow->Cells[5]->Value;
				 MemoryStream ^ms = gcnew MemoryStream(b->Length);
				 ms->Write(b, 0, b->Length);
				 pbPhoto->Image = Image::FromStream(ms);
				 pbPhoto->Refresh();
			 } catch (Exception ^e) {

			 }
		 }
Получение текстовой информации о цвете, модели и др. не представляет труда, они хранятся в столбцах грида по порядку. С полем фото сложнее, так как оно представляет собой двоичную информацию, т.е. массив байт. Вдобавок к этому, объект Image в PictureBox может считывать информацию из файла, потока, или другого такого же объекта, но не из массива. Поэтому чтобы представить массив байт в виде изображения, приходится предварительно записывать его в MemoryStream – поток, хранящий данные в памяти.

Код для кнопки «Загрузить»:

private: System::Void btnLoadPhoto_Click(System::Object^  sender, System::EventArgs^  e) {
			 OpenFileDialog ^d = gcnew OpenFileDialog();
			 d->Filter = "JPEG файлы (*,.jpg)|*.jpg|Bitmap файлы (*.bmp)|*.bmp|Все файлы (*.*)|*.*";
			 if (d->ShowDialog() == Windows::Forms::DialogResult::OK)
				pbPhoto->ImageLocation = d->FileName;
		 }
Ничего особенного нет, занесение имени выбранного изображения в PictureBox. Код для кнопки «Сохранить»:

private: System::Void btnSave_Click(System::Object^  sender, System::EventArgs^  e) {
			 String ^sql = "update Models set model=N'{0}', year='{1}', "
				 "color=N'{2}', weight='{3}' where ID=" + id;
			 sql = String::Format(sql, tbModel->Text, tbYear->Text,
				 tbColor->Text, tbWeight->Text);
			 SqlCommand ^c = gcnew SqlCommand(sql, con);
			 c->ExecuteNonQuery();
			 
			 if (pbPhoto->ImageLocation) {
				 using namespace System::IO;
				 FileStream ^fs = File::OpenRead(pbPhoto->ImageLocation);
				 array<Byte> ^b = gcnew array(fs->Length);
				 fs->Read(b, 0, fs->Length);
				 fs->Close();
				 sql = "update Models set photo = @photo where ID=" + id;
				 SqlCommand ^c2 = gcnew SqlCommand(sql, con);
				 c2->Parameters->Add(gcnew SqlParameter("photo", SqlDbType::Image));
				 c2->Parameters["photo"]->Value = b;
				 c2->ExecuteNonQuery();
			 }
			 DialogResult = ::DialogResult::OK;
		 }
Сохранение информации начинается с текстовых полей. Используется sql команда update, но стоит отметить, что, несмотря на то, что год и вес являются числами, внутри запроса их значения все равно помещаются внутри одиночных кавычек ‘’. Если бы их не было, то в случае, например, если после редактирования значение года выпуска окажется пустым, в sql запросе будет year= что вызовет ошибку на сервере. С кавычками же year=’’, сервер сам выполняет преобразование типов, и пустая строка будет расценена как 0, и ошибки не возникнет.

К сожалению, сохранить подобным образом изображение не получится, так как представить в виде строки массив байт длиной в десятки, сотни килобайт или больше, достаточно сложно. Для таких случаем в классе SqlCommand предусмотрены «параметры» - именованные переменные, начинающиеся в sql запросе с символа @, значения которых можно задавать практически любым способом, включая и просто двоичные массивы. Для получения массива байт изображения из файла используется файловый поток FileStream.

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

Теперь, когда форма редактирования готова, можно вернуться к основному окну, и добавить код для последней оставшейся кнопки:

private: System::Void btnModelEdit_Click(System::Object^  sender, System::EventArgs^  e) {
			 if (dgvModels->RowCount == 0) return;
			 EditModel ^form = gcnew EditModel();
			 form->con = Con;
			 form->dgv = dgvModels;
			 if (form->ShowDialog() == ::DialogResult::OK)
				RefreshModels(id_firm);
		 }
Не забудьте про #include "EditModel.h" в начале файла.

Запустите программу и проверьте на наличие ошибок. Теперь можно редактировать модели, загружать фото телефонов в базу, понажимать на заголовки столбцов в гриде для сортировки (встроенная возможность DataGridView). Можно и дальше дорабатывать программу, расширяя ее функциональность, например:

  • Редактирование фирм – можно сделать аналогично моделям
  • Поиск – при заполнении гридов запросом select добавить в строку запроса условия where для заданных критериев поиска
  • Добавить проверку вводимых данных для исключения ошибок
  • Печать отчетов
  • Экспорт данных
  • И многое другое…
Тем не менее, написанный код уже дает достаточное представление о принципах работы с ADO.NET на языке C++\CLI. Если же вам что-то непонятно, требуется консультация по базам данных либо C++, необходимо сделать практическую работу, то вы можете написать мне. За небольшую плату я все сделаю.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, си плюс плюс, базы данных, visual studio




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




Урок 17. Сборки .NET
Таёжный детектив, или браконьеры против властей на Дальнем Востоке
Доверяйте напарнику, или продолжаем карточный скрипт