<% ASP на блюдечке %>. Часть 5
Виртуальный магазин (продолжение)
Создание и подготовка базы данных
Структура нашего Интернет-магазина
Связь с базами данных в C++Builder
Продолжение следует, читайте скоро…
В первых статьях серии <% ASP на блюдечке %> (<% ASP на блюдечке %> — части 1, 2, 3 и 4, КомпьютерПресс № 9, 10, 11 и 12 за 2000 год соответственно) мы ознакомились с ASP, с принципами построения простейшего интерфейса к базе данных с его помощью (газетный сайт со встроенными возможностями его пополнения новыми статьями, снабжаемыми фотографиями непосредственно с самого сайта и без программирования), с основами использования ActiveX компонентов в ASP, а также с азами разработки собственных компонентов с помощью известных всем Microsoft Visual Basic и Microsoft Visual C++ и с возможностями, предоставляемыми ASP в поддержку программирования WAP-сайтов (создание WAP-системы бронирования авиабилетов).
Далее была представлена система Интернет-торговли (базa данных перечня товаров гипотетического магазина компьютеров и комплектующих, средства предъявления товаров, выбoрки и добавления в «корзину» и посылки заказа по электронной почте), или, говоря попросту, Интернет-магазин.
Настоящая статья предназначена для тех, кто собирается автоматизировать процесс подготовки шаблонов конфигураций продаваемого оборудования. Как отмечалось, не все покупатели точно знают, что именно хотят приобрести, да и не во всех может параметрах ряда товаров разобраться обыватель (например, бытовая техника или компьютеры), зачастую требуется консультация эксперта.
Для тех, кто не знаком…
Введение
В предыдущей статье (КомпьютерПресс № 12 за 2000 год, <%ASP на блюдечке%>. Часть 4) такой эксперт-консультант был реализован программно. Было создано несколько шаблонов типовых конфигураций персональных компьютеров, к примеру «Графическая станция», «Мультимедийный компьютер для дома с Интернет-возможностями», «Сервер баз данных» и т.д. Шаблоны создавались вручную. Далее некомпетентным покупателям предлагалось выбрать наиболее подходящий шаблон и таким образом избавиться от необходимости подбора комплектующих, в ходе которого могут быть допущены ошибки, например выбраны два несовместимых устройства.
Однако, как отмечалось, если шаблоны будут меняться достаточно часто и если их много, то процесс их подготовки необходимо автоматизировать (для этого проще всего написать программу — построитель шаблонов при помощи Borland C++Builder-а). Кроме того, прайс-листы различных компаний, как правило, поставляются в электронном виде, например в виде *.xls- или *.doc-файлов, и необходимо средство систематического преобразования таких прайс-листов в базу данных.
Создание и подготовка базы данных
Для создания базы данных (назовем ее Ishop) нам понадобится Microsoft Access 2000. От процесса проектирования базы данных будет зависеть дальнейший объем программной логики нашего приложения, поэтому этот этап крайне ответственен. И еще один совет: при проектировании Интернет-магазина стремитесь к тому, чтобы ваши странички содержали как можно меньше строк и констант, выведите все что можно за их пределы, вплоть до наименований позиций, так как последние могут меняться довольно часто. Говоря другими словами, добейтесь от своего кода универсальности и независимости от данных. Однако, поскольку принципы проектирования баз данных выходят за рамки настоящей статьи, не будем останавливаться на них и представим структуру нашей базы данных так, как показано на рис. 1.
Создадим три управляющие таблицы для хранения информации о наименованиях всех категорий и их соответствия таблицам с позициями, ценами и условиями гарантий (таблицу _Components (рис. 2)), таблицы с перечнем дополнительных категорий (таблицу _Equipment (рис. 3) для компетентных покупателей и таблицу _PCType (рис. 4) для некомпетентных покупателей, желающих приобрести компьютер с определенной целью (к примеру, для дома или для офиса).
Следует отметить, что поле «IsPCComponent» в таблице _Components, имеющее булево значение, позволяет определить, является ли данный компонент обязательным составляющим персонального компьютера или нет (это понадобиться нам в дальнейшем).
Все остальные таблицы создадим по одному и тому же шаблону — с наименованием позиции (поле Title), розничной ценой (поле Price 1), мелкооптовой ценой (поле Price 2) и крупнооптовой ценой (поле Price 3), а также с условиями гарантии на товар (поле Description) (рис. 5).
Как видите, таблица _PCType осуществляет взаимосвязь всех остальных таблиц и позволяет организовать системы предложения оптимальной конфигурации компьютера. Пример такой взаимосвязи представлен на рис. 6.
То есть каждая строка таблицы _PCType содержит номера ключевых полей всех таблиц с позициями комплектующих с указанием количества каждой комплектующей той или иной типовой конфигурации.
Структура нашего Интернет-магазина
Как помните, структура нашего Интернет-магазина представляется так, как показано на рис. 7.
Формы и действия при выборе конфигурации компьютера автоматически представлены синим цветом, при выборе конфигурации самостоятельно — зеленым, при выборе дополнительного оборудования — сиреневым, а начало и конец алгоритма-схемы — красным.
В настоящей статье мы рассмотрим лишь одну ветвь этой схемы — синюю. А если быть точнее, попытаемся написать программу-конфигуратор, с помощью которой будут создаваться и редактироваться типовые конфигурации продаваемого оборудования (суть задачи не меняется от вида товара — будь то компьютеры или автомобили).
Уяснив структуру проектируемого приложения, мы можем приступать к написанию кода.
Что нам понадобится?
- Microsoft Access 97 или 2000, но лучше Microsoft SQL Server 7.0 или 2000
- Borland C++ Builder 5.0
Конечно, можно обойтись и Access’ом 97 или 2000, однако советую перевести Access- базу нашего магазина в Microsoft SQL Server. Причина здесь очевидна — колоссальная прибавка производительности Microsoft SQL Server’а по сравнению с Microsoft Access'ом. Но как быть? Ведь база уже подготовлена с помощью Access. Рассмотрим подробно процесс создания SQL-версии базы данных.
Итак, для того чтобы скопировать таблицы из базы данных формата Microsoft Access 2000 в Microsoft SQL Server 2000, необходимо запустить средство транспортировки данных DTS (Data Transformation Service), а для этого нужно запустить: Programs->Microsoft SQL Server->Import and Export Data.
Перед вами появится окно мастера импорта и экспорта данных, в котором необходимо последовательно задать источник и приемник данных. В качестве источника данных выберем исходный файл с Access’овской базой данных (рис. 8).
А в качестве приемника данных выберем Microsoft OLE DB Provider for SQL Server, введем пароль системного администратора, а также имя SQL-базы данных нашего магазина (можно либо создать базу данных заблаговременно, либо выбрать в списке элемент «<new>» и в появившемся окне впечатать имя создаваемой базы данных — в нашем случае IShop и продолжим процесс импорта нажатием на кнопку Next (рис. 9)
После этого следует еще раз нажать кнопку Next, выбирая из предлагаемых операций простое копирование таблиц, в появившемся списке таблиц проставить галочки слева от тех таблиц, которые должны быть скопированы (в нашем случае можно воспользоваться кнопкой Select All, выделив все таблицы Access’овской базы) и запустить процесс импорта данных (Run Immediately), запуская таким образом процесс копирования данных немедленно. После этого начнется процесс копирования данных (рис. 10).
По окончании процесса копирования можно запустить Enterprise Manager (Programs->Microsoft SQL Server-> Enterprise Manager) и убедиться в том, что таблицы попали в нужную базу данных (рис. 11)
Вроде бы все готово и можно приступать к процессу создания приложения — лиента к SQL-базе данных. Однако перед этим следует выполнить еще несколько действий. Во-первых, после перегона данных из Access’а в SQL Server следует внимательно просмотреть все типы полученных там полей данных и проверить их правильность. Если какие-либо типы данных не соответствуют требуемым, можно выполнить преобразование вручную, если же данные в ходе этого могут быть искажены (о чем вас предупредит Enterprise Manager), можно повторить процесс перегона данных, в ходе которого воспользоваться кнопкой Advanced в программе — трансформаторе данных, где задать правила перетипизации данных. Далее необходимо указать ключевое поле Set Primary Key и включить свойство счетчика Identity, выставив в нем значение Yes.
В результате каждая таблица данных в режиме редактирования типов полей Design должна выглядеть так, как показано на рис. 12.
И наконец, рекомендуется создать еще один псевдоним к нашей базе данных — для упрощения процесса создания приложения с помощью Borland C++Builder. Он называется алиасом (от англ. Alias) и служит для связи реальной базы данных с так называемым BDE (Borland Database Engine).
Связь с базами данных в C++Builder
Основой работы C++Builder с базами данных является Borland Database Engine (BDE) — процессор баз данных фирмы Borland. BDE служит посредником между приложением и базами данных. Он предоставляет пользователю единый интерфейс для работы, освобождающий пользователя от конкретной реализации базы данных. Благодаря этому отпадает необходимость менять приложение при смене реализации базы данных. Приложение C++ Builder никогда не обращается к базе данных непосредственно, а только к BDE.
Приложение C++Builder, когда ему нужно связаться с базой данных, обращается к BDE и обычно сообщает псевдоним базы данных и необходимую таблицу в ней. BDE реализован в виде динамически подключаемых библиотек DLL, которые, как и любые другие библиотеки, снабжены API (Application Program Interface — интерфейс прикладных программ), названным IDAPI (Integrated Database Application Program Interface). Это список процедур и функций для работы с базами данных, которым и пользуются приложения, создаваемые с помощью Borland C++Builder.
BDE по псевдониму находит подходящий для указанной базы данных драйвер. Драйвер — это вспомогательная программа, «понимающая», как общаться с базами данных определенного типа. Если в BDE имеется собственный драйвер соответствующей СУБД, то BDE связывается через него с базой данных и с нужной таблицей в ней, отрабатывает запрос пользователя и возвращает в приложение результаты обработки. BDE поддерживает естественный доступ к таким базам данных, как Microsoft Access, FoxPro, dBase и Paradox.
Если же собственного драйвера нужной СУБД в BDE нет, то можно воспользоваться ODBC (в предыдущей статье описано, как это делается для базы данных MS Access). ODBC (Open Database Connectivity) — DLL, аналогичная по функциям BDE, но разработанная компанией Microsoft. Поскольку Microsoft включила поддержку ODBC в свои офисные продукты и для ODBC созданы драйверы практически к любым СУБД, компания Borland включила в BDE драйверы, позволяющие использовать ODBC-псевдонимы.
В отличие от BDE работа через ODBC осуществляется гораздо медленнее, поэтому рекомендуется по возможности пользоваться именно BDE-алиасами и BDE-драйверами к базе данных. Однако, если это невозможно (по причине отсутствия таковых), можно воспользоваться и ODBC-алиасами и драйверами соответственно.
Теперь рассмотрим процесс создания BDE-алиаса к нашей базе данных во всех подробностях.
Для начала следует запустить программу-конфигуратор Database Desktop, входящую в состав пакета Borland C++Builder. Для этого выполним команду: Programs->Borland C++Builder->Database Desktop. Далее уже в Database Desktop выполним команду меню Tools->Alias Manager, в появившемся окне конструктора псевдонимов нажмите на кнопку «New» для создания нового псевдонима и заполните поля соответствующими значениями (см. рис. 13).
Теперь можно проверить правильность только что созданного псевдонима (кнопка Connect Now). При выходе из программы Database Desktop попросит сохранить созданный алиас и при этом обновит файл конфигурации всех алиасов системы.
Как видите, создавать ODBC-алиас при разработке приложений клиентов к базам данных с помощью Borland C++Builder’а в общем случае вовсе не обязательно. Однако не следует забывать, что в нашем случае клиентом к базе данных служит не только Windows-приложение, которое мы собираемся разрабатывать, но и Web-приложение (рассмотренное в деталях в предыдущей статье). Поэтому нам без создания ODBC-псевдонима не обойтись.
Для этого:
- запустите программу — конфигуратор источников данных (Data Sources ODBC) — Start->Settings->Control Panel->Administrative Tools->Data Sources ODBC;
- перейдите во вкладку System DSN и создайте новый источник данных, нажав на Add…;
- в появившемся списке драйверов выберите драйвер баз данных SQL Server и нажмите на Finish;
- в строке Data Source Name задайте имя базы данных, в нашем случае IShop (это то имя, по которому мы в дальнейшем будем обращаться к ней);
- в самой нижней строке задайте имя SQL-сервера, далее введите учетную запись и пароль доступа к серверу и нажмите OK.
Вы увидите появившуюся строку в списке источников данных в вашей системе (рис. 14).
Разберемся, каким образом следует сконфигурировать разрабатываемое приложение в зависимости от того, какой псевдоним к базе данных будет использоваться. Речь идет о свойствах объекта Tdatabase. В нашем случае следует заполнить его свойства следующими значениями:
- AliasName = 'IShop' — имя псевдонима (созданного с помощью Database Desktop);
- DatabaseName = 'IShop' —название базы данных;
- LoginPrompt = False — запрашивать ли пароль при подключении к базе данных.
Вся остальная информация о нашей базе данных уже содержится в псевдониме и поэтому может не указываться. Если же по каким-то причинам использование BDE-псевдонима невозможно, то свойства объекта TDatabase в нашем случае будут выглядеть следующим образом:
DatabaseName = 'ISHop' LoginPrompt = False Params.Strings = ( [ODBC] DRIVER=MSSQL — драйвер базы данных UID=sa — учетная запись DATABASE=ISHop — название базы данных APP=ISHManager — название приложения SERVER= Aquarius — название сервера USER NAME=sa — учетная запись пользователя PASSWORD=123 — пароль пользователя )
Что же необходимо изменить в ASP-коде магазина для того, чтобы все работало под управлением SQL-сервера. По сути, все изменения относятся к способу подключения к базе данных, а именно:
вместо строки: db.Open "DSN=IShop;UID=sa;PWD=;"
будем использовать строку: db.Open "DSN=ISHop; UID=sa;PWD=;database=ISHop"
Прежде чем приступать…
…Хочется дать один совет относительно способов проектирования приложений-клиентов к базам данных с помощью Borland C++ Builder. Речь идет о двух компонентах, без существования которых не обошлось бы ни одно СУБД-приложение, а именно о TTable и TQuery.
Понимание разницы между ними очень важно для дальнейшей работы. Оба эти компонента предоставляют доступ к базе данных. Компонент TTable предоставляет более удобный интерфейс к таблицам базы данных, однако в отличие от компонента TQuery усложняет разработку сетевых многопользовательских приложений. Дело в том, что компонент TTable в подключенном к таблице базы данных состоянии (Active = true) не дает двум или более пользователям одновременно редактировать таблицу в базе данных. Эту проблему можно решить, открывая и закрывая элемент типа TTable всякий раз, когда происходит обращение к таблице. Однако при этом, естественно, указатель активной записи в таблице теряет свое положение. Поэтому, открывая таблицу в последующем, придется снова находить нужную (последнюю) запись. Конечно, это не составляет труда, но тем не менее может серьезно замедлить работу многопользовательского приложения. В нашем случае многопользовательность — не самое главное, однако мы будем использовать как компонент TTable, так и TQuery.
Используемые компоненты
Теперь рассмотрим компоненты, используемые для визуализации данных и одновременной связи с ними. Такие компоненты расположены в палитре Data Controls Borland C++Builder, из которой нам понадобится лишь один — ТDBLookupComboBox — выпадающий список выбора, связанный с определенным полем заданной таблицы заданной базы данных. Это означает, что изменение текущего значения в таком поле приводит к изменению позиции указателя в связанной с ним таблице данных и наоборот. Согласитесь, что такой компонент избавляет нас от необходимости писать громоздкие функции-обработчики; надо всего лишь заполнить его свойства, а именно:
- ListSource — компонент типа TdataSet, с которым связан компонент типа TTable (связанный с заданной таблицей данных);
- ListField — поле таблицы, формирующее список.
Итак…
Прежде всего необходимо подготовить форму приложения, причем отметим сразу, что оно будет состоять из двух частей:
- программа обработки исходного прайс-листа (предоставляемого торговым отделом гипотетического магазина) в форме таблицы (например, *.xls файла);
- программа-конфигуратор типовых шаблонов состава оборудования.
Для этого удобнее всего воспользоваться компонентом TPageControl со вкладки Win32 палитры компонентов Borland C++Builder’а версии 5.0.
Создадим главное окно нашего приложения, поле типа TlistBox, в котором будут отображаться ключевые поля анализируемой таблицы (исходные данные) и поле с названиями соответствующих таблиц (также типа TListBox). Эти поля будут служить для установления соответствия названий ключевых полей исходного прайса таблицам базы данных, в которые программа будет вставлять значения.
Далее создадим компоненты типа TQuery для построения запросов к базам данных и определим процедуру инициализации формы:
//--------------------------------------------------------------------------- void __fastcall TMainForm::FormCreate(TObject *Sender) { AnsiString Str; Height = 386; ErrStr1 = ""; ErrStr2 = ""; ErrStr3 = ""; // Подключение базы данных Database1->Connected = true; //Подключение таблиц данных ConfTB->Active = true; MBTB->Active = true; CPTB->Active = true; SVTB->Active = true; IDEHTB->Active = true; SCSITB->Active = true; SndTB->Active = true; KBDTB->Active = true; MouseTB->Active = true; CaseTB->Active = true; MonTB->Active = true; SpeakerTB->Active = true; FaxTB->Active = true; RAMTB->Active = true; FDDTB->Active = true; CDTB->Active = true; PadTB->Active = true; OpenConfiguration(); // Функция загрузки текущего шаблона ReadKF(); // Функция чтения ключевых полей исходного прайс-листа Str = AnsiString(KeyFields->Items->Count); Label1->Caption = Label1->Caption + Str; Str = AnsiString(Map->Items->Count); Label5->Caption = Label5->Caption + Str; KeyFields->ItemIndex = 0; Map->ItemIndex = 0; KeyFieldsClick(Sender); MapClick(Sender); } //---------------------------------------------------------------------------
Кроме того, процедуру, вызываемую при закрытии формы:
//--------------------------------------------------------------------------- void __fastcall TMainForm::FormDestroy(TObject *Sender) { SaveConfiguration(); // Сохранение текущей конфигурации ConfTB->Active = false; MBTB->Active = false; CPTB->Active = false; SVTB->Active = false; IDEHTB->Active = false; SCSITB->Active = false; SndTB->Active = false; KBDTB->Active = false; MouseTB->Active = false; CaseTB->Active = false; MonTB->Active = false; SpeakerTB->Active = false; FaxTB->Active = false; RAMTB->Active = false; FDDTB->Active = false; CDTB->Active = false; PadTB->Active = false; Query1->Close(); Query2->Close(); Query3->Close(); Query4->Close(); Database1->Connected = false; } //---------------------------------------------------------------------------
Создадим две вкладки и назовем их «Прайс-процессор» и «Конфигуратор ПК» соответственно. Определим процедуру переключения вкладок следующим образом:
//--------------------------------------------------------------------------- void __fastcall TMainForm::PageControlChange(TObject *Sender) { if (PageControl->ActivePage == TabSheet1) Height = 386; else Height = 518; } //---------------------------------------------------------------------------
Таким образом, высота окна нашей формы будет изменяться в зависимости от того, какая из двух вкладок активна. Далее определим связанные с источниками данных списки выбора комплектующих (для этого воспользуемся компонентами DBLookupComboBox из палитры DataAccess) и функцию инициализации состояния списков выбора комплектующих:
//--------------------------------------------------------------------------- void TMainForm::OpenConfiguration() { Configuration->DataField = "PCType"; Configuration->KeyValue = ConfTB->FieldByName("ID")->AsInteger; MB->DataField = "Title"; MB->KeyValue = ConfTB->FieldByName("MainBoardID")->AsInteger; // Текущее значение выпадающего списка позиций совпадает с // заданной позицией указателя в соответствующей таблице MBPrc->Text = AnsiString(ConfTB->FieldByName("MainBoardNum")->AsInteger); // Занесем в поле типа TEdit значение количества экземпляров текущей позиции // … // И далее для всех таблиц данных // … CalcTotalPrice(); // Функция подсчета интегральной стоимости набора } //---------------------------------------------------------------------------
Прежде чем приступить к написанию функции подсчета интегральной стоимости всего набора, подготовим поля для указания стоимости (TDBEdit) и количества экземпляров (TEdit) каждого типа комплектующих. Теперь напишем функцию подсчета стоимости всего набора:
//--------------------------------------------------------------------------- void TMainForm::CalcTotalPrice() { AnsiString Str, dig = ""; float CurVal, TotalVal = 0.0; int CurNum; Str = MBPrc->Text; try { CurNum = Str.ToInt(); } // Проверка правильности ввода catch (Exception &E) { Str = "Допустимы только целые численные значения!\n" Str = Str + Str + "—- не верный формат целого числа."; Application->MessageBox(Str.c_str(), "Ошибка!", MB_ICONERROR|MB_OK); return; } Str = MBNum->Field->AsString; CurVal = Str.ToDouble(); TotalVal = TotalVal + CurVal * CurNum; // Подсчет общей стоимости // … // И далее для всех таблиц данных // … // Отсечем ненужные знаки после запятой (после второй цифры) Str = AnsiString (TotalVal); for (int i = 1; i <= Str.Length(); i++) { if (Str[i] == ',') { dig = dig + Str[i]; if (Str.Length() >= i+1) dig = dig + Str[i+1]; if (Str.Length() >= i+2) dig = dig + Str[i+2]; break; } dig = dig + Str[i]; } Total->Text = dig; // И выведем полученное значение суммарной стоимости в соответствующее поле } //---------------------------------------------------------------------------
Теперь настало время разобраться с ключевыми полями нашего прайса. Допустим, прайс-лист ежедневно поставляется в формате Excel-файла, причем все позиции в нем представлены в одной таблице в форме списка (это наиболее типичный случай). Для автоматизации его разбивки на соответствующие разделы необходимо выявить признаки, свойственные наименованиям этих разделов. Назовем эти разделы ключевыми полями и сымпортируем Excel-файл в SQL Server (это делается точно так же, как и в случае с Access’ом) в таблицу с именем Src. Далее от нас потребуется, «проходя» последовательно по всем записям этой таблицы и выявляя ключевые поля, вставлять записи в соответствующие названиям ключевых полей таблицы. Остается одно: определить то свойство, которое позволит нам отличить наименование раздела (например, «Процессоры» или «Материнские платы» от наименования позиций). Предположим, что в нашем случае такая разница заключается в отсутствии значения в столбцах «Цена» у наименования позиции.
//--------------------------------------------------------------------------- void TMainForm::ReadKF() { AnsiString sSQL, CurField, p1, p2, p3, p4, p5, descr; int i; sSQL = "SELECT * FROM Src"; // Выборка всех записей таблицы-исходного прайс-листа Query1->Close(); Query1->SQL->Clear(); Query1->SQL->Add(sSQL); Query1->Open(); // Выполнение запроса KeyFields->Items->Clear(); while (!Query1->Eof) { CurField = Query1->FieldByName("Title")->AsString; // Поле «Title» текущей записи p1 = Query1->FieldByName("Price1")->AsString; p2 = Query1->FieldByName("Price2")->AsString; p3 = Query1->FieldByName("Price3")->AsString; descr = Query1->FieldByName("Description")->AsString; // Если значение цен в столбцах отсутствует, значит это ключевое поле // В противном случае продолжаем… if (p1 != "" || p2 != "" || p3 != "" || descr != "") { Query1->Next(); continue; } int k; k = CurField.Length(); // Отсечем возможные порядковые номера раздела if (CurField[1] == '1' || CurField[1] == '2' || CurField[1] == '3' || CurField[1] == '4' || CurField[1] == '5' || CurField[1] == '6' || CurField[1] == '7' || CurField[1] == '8' || CurField[1] == '9' || CurField[1] == '0' || CurField[1] == '.') { for (i = 1; i <= k; i++) if (CurField[i] == '1' || CurField[i] == '2' || CurField[i] == '3' || CurField[i] == '4' || CurField[i] == '5' || CurField[i] == '6' || CurField[i] == '7' || CurField[i] == '8' || CurField[i] == '9' || CurField[i] == '0' || CurField[i] == '.') { CurField = CurField.SubString(i+1, CurField.Length()); i = 0; k = CurField.Length(); } else break; } if (CurField[1] == ' ') CurField = CurField.SubString(2, CurField.Length()); // Все изменения будем делать непосредственно в таблице Src Query1->Edit(); Query1->FieldByName("Title")->AsString = CurField; Query1->Post(); // И добавим полученное значение текущего распознанного ключевого поля к списку ключевых полей KeyFields->Items->Add(CurField); Query1->Next(); } Query1->First(); sSQL = "SELECT * FROM _Components ORDER BY CategoryTableName ASC"; // Выберем из управляющей таблицы с наименованиями таблиц данных все записи, // расположенные в алфавитном порядке по наименованиям таблиц данных Query4->Close(); Query4->SQL->Clear(); Query4->SQL->Add(sSQL); Query4->Open(); // Выполним запрос // И загрузим результат запроса в список выбора Map->Items->Clear(); while (!Query4->Eof) { Map->Items->Add(Query4->FieldByName("CategoryTableName")->AsString); Query4->Next(); } Query4->First(); } //---------------------------------------------------------------------------
Поскольку функция обновления всех таблиц данных производится оператором регулярно, то перед каждым добавлением новых значений все таблицы данных необходимо очистить от старых значений. Для этого:
//--------------------------------------------------------------------------- void __fastcall TMainForm::DropAllClick(TObject *Sender) { AnsiString sSQL; if (Application->MessageBox("Данная операция очистит содержимое ВСЕХ таблиц базы. Выполнить?", "Внимание!", MB_ICONQUESTION|MB_YESNO) == IDYES) { sSQL = "SELECT * FROM _Components"; // Из всех таблиц, чье название содержится в столбце // "CategoryTableName" управляющей таблицы _Components, // удалим все данные Query3->Close(); Query3->SQL->Clear(); Query3->SQL->Add(sSQL); Query3->Open(); while (!Query3->Eof) { sSQL = "DELETE FROM " + Query3->FieldByName("CategoryTableName")->AsString; Query2->Close(); Query2->SQL->Clear(); Query2->SQL->Add(sSQL); Query2->ExecSQL(); Query3->Next(); } Application->MessageBox("Содержимое таблиц данных уничтожено!", "Готово!", 0); } } //---------------------------------------------------------------------------
После того как содержимое всех таблиц данных уничтожено, можно вставить в них новые значения:
//--------------------------------------------------------------------------- void __fastcall TMainForm::UpdateAllClick(TObject *Sender) { AnsiString gSQL, CItem, NItem, CTitle; int i; gSQL = "SELECT * FROM Src"; // Выборка всех записей таблицы — исходного прайс-листа Query2->Close(); Query2->SQL->Clear(); Query2->SQL->Add(gSQL); Query2->Open(); // Выполнение запроса i = 0; while (i < KeyFields->Items->Count) { // Для каждого ключевого поля while (!Query2->Eof) { CItem = KeyFields->Items->Strings[i]; //Начиная со следующей за ключвым полем записи (до следующего ключевого поля) if (i < KeyFields->Items->Count — 1) NItem = KeyFields->Items->Strings[i+1]; else NItem = "NItem"; CTitle = Query2->FieldByName("Title")->AsString; // Если текущее поле совпадает с текущим ключевым полем if (CTitle == CItem) { // То вставим в таблицу с названием, совпадающим с названием текущего ключевого поля, // Строку"Выберите позицию" InsRecord(CItem, "Выберите позицию", NULL, NULL, NULL, NULL); // И далее в цикле все значения до следующего ключевого поля while (CTitle != NItem && !Query2->Eof) { if (CTitle == CItem) { Query2->Next(); CTitle = Query2->FieldByName("Title")->AsString; continue; } CTitle = Replace(CTitle); InsRecord(CItem, CTitle, Query2->FieldByName("Price1")->AsFloat, Query2->FieldByName("Price2")->AsFloat, Query2->FieldByName("Price3")->AsFloat, Query2->FieldByName("Description")->AsString); Query2->Next(); CTitle = Query2->FieldByName("Title")->AsString; } i++; Query2->First(); break; } else Query2->Next(); } } Application->MessageBox("Содержимое таблиц данных обновлено!", "Готово!", 0); } //---------------------------------------------------------------------------
Теперь разберемся с функциями InsRecord и Replace. Функция InsRecord, по сути, генерирует SQL-строку для ввода значений в заданную таблицу базы данных:
//--------------------------------------------------------------------------- void TMainForm::InsRecord(AnsiString TName, // Название таблицы AnsiString Title, // Наименование позиции float Price1, float Price2, float Price3, // Значения цен AnsiString Description) // Строка с дополнительным описанием { AnsiString Str, tmp, dig; Str = "INSERT INTO " + TName; Str = Str + "(Title, Price1, Price2, Price3, Description)"; Str = Str + " VALUES"; Текущее значение не должно содержать символов кавычек for (int j = 1; j<= Title.Length(); j++) if (Title[j] == '\'') Title[j] = ' '; Str = Str + "('" + Title; Str = Str + "', "; // Прежде чем вставить численные значения, заменим в строках символы «,» на символы «.» dig = ""; tmp = AnsiString (Price1); for (int j = 1; j <= tmp.Length(); j++) { if (tmp[j] == ',') { tmp[j] = '.'; dig = dig + tmp[j]; if (tmp.Length() >= j+1) dig = dig + tmp[j+1]; if (tmp.Length() >= j+2) dig = dig + tmp[j+2]; break; } dig = dig + tmp[j]; } Str = Str + dig; Str = Str + ", "; dig = ""; tmp = AnsiString (Price2); for (int j = 1; j <= tmp.Length(); j++) { if (tmp[j] == ',') { tmp[j] = '.'; dig = dig + tmp[j]; if (tmp.Length() >= j+1) dig = dig + tmp[j+1]; if (tmp.Length() >= j+2) dig = dig + tmp[j+2]; break; } dig = dig + tmp[j]; } Str = Str + dig; Str = Str + ", "; dig = ""; tmp = AnsiString (Price3); for (int j = 1; j <= tmp.Length(); j++) { if (tmp[j] == ',') { tmp[j] = '.'; dig = dig + tmp[j]; if (tmp.Length() >= j+1) dig = dig + tmp[j+1]; if (tmp.Length() >= j+2) dig = dig + tmp[j+2]; break; } dig = dig + tmp[j]; } Str = Str + dig; Str = Str + ", '"; Str = Str + Description + "')"; Query3->Close(); Query3->SQL->Clear(); Query3->SQL->Add(Str); Query3->ExecSQL(); // И выполним SQL-запрос } //---------------------------------------------------------------------------
Заметьте, что в случаях, когда результат выполнения запроса не формирует данные, вместо Query3->Open() применяется Query3->ExecSQL().
Функция же Replace попросту подменяет в строке один символ другим:
//--------------------------------------------------------------------------- AnsiString TMainForm::Replace(AnsiString S) { for (int i = 1; i <= S.Length(); i++) if (S[i] == '#') S[i] = ' '; return S; } //---------------------------------------------------------------------------
Еще одна полезная функция (без которой не представляет себе жизни бухгалтерия) предоставляет возможность умножения всех цен на задаваемый коэффициент (очень часто используется для «накрутки» цен). Давайте реализуем эту возможность:
//--------------------------------------------------------------------------- void __fastcall TMainForm::MulByFactorBtnClick(TObject *Sender) { AnsiString Str, sSQL; double factor; // Для начала проверим, ввел ли пользователь корректное значение try { factor = Factor->Text.ToDouble(); } catch(Exception &E) { // Если нет, то выдадим предупреждение и прекратим дальнейшие вычисления Str = "Допустимы только численные значения множителя!\n" + Factor->Text + " — неверный формат числа с плавающей точкой."; Application->MessageBox(Str.c_str(), "Ошибка!", MB_ICONERROR|MB_OK); return; } // Уверен ли пользователь? Str = "Данная операция умножит значения ВСЕХ столбцов \n с ценами таблиц данных базы на "; Str = Str + Factor->Text + ". Выполнить?"; if (Application->MessageBox(Str.c_str(), "Внимание!", MB_ICONQUESTION|MB_YESNO) == IDYES) { // Если да, то прочитаем значение фактора умножения в строке текста // И заменим в ней символ ',' на '.' Str = Factor->Text; for (int i = 1; i <= Str.Length(); i++) if (Str[i] == ',') Str[i] = '.'; sSQL = "SELECT * FROM _Components"; // Далее во всех таблицах с данными (чьи имена хранятся в таблице "_Components") Query3->Close(); Query3->SQL->Clear(); Query3->SQL->Add(sSQL); Query3->Open(); // Обновим значения цен while (!Query3->Eof) { sSQL = "UPDATE " + Query3->FieldByName("CategoryTableName")->AsString + " SET "; sSQL = sSQL + "Price1 = Price1 * " + Str + ", "; sSQL = sSQL + "Price2 = Price2 * " + Str + ", "; sSQL = sSQL + "Price3 = Price3 * " + Str; Query2->Close(); Query2->SQL->Clear(); Query2->SQL->Add(sSQL); Query2->ExecSQL(); Query3->Next(); } Application->MessageBox("Содержимое таблиц данных пересчитано!", "Готово!", 0); } } //---------------------------------------------------------------------------
И наконец, функция сохранения текущей конфигурации, по сути, будет записывать состояние указателей во всех таблицах данных в управляющую таблицу _PCType:
//--------------------------------------------------------------------------- void TMainForm::SaveConfiguration() { ConfTB->Edit(); ConfTB->FieldByName("MainBoardID")->AsInteger = MB->KeyValue; ConfTB->FieldByName("MainBoardNum")->AsInteger = MBPrc->Text.ToInt(); ConfTB->FieldByName("CPUID")->AsInteger = CPU->KeyValue; ConfTB->FieldByName("CPUNum")->AsInteger = CPPrc->Text.ToInt(); ConfTB->FieldByName("RAMID")->AsInteger = RAM->KeyValue; ConfTB->FieldByName("RAMNum")->AsInteger = RAMPrc->Text.ToInt(); ConfTB->FieldByName("SVGAID")->AsInteger = SVGA->KeyValue; ConfTB->FieldByName("SVGANum")->AsInteger = SVPrc->Text.ToInt(); ConfTB->FieldByName("IDEHDDID")->AsInteger = IDEH->KeyValue; ConfTB->FieldByName("IDEHDDNum")->AsInteger = IDPrc->Text.ToInt(); ConfTB->FieldByName("SCSIHDDID")->AsInteger = SCSIH->KeyValue; ConfTB->FieldByName("SCSIHDDNum")->AsInteger = SCPrc->Text.ToInt(); ConfTB->FieldByName("SBID")->AsInteger = Sound->KeyValue; ConfTB->FieldByName("SBNum")->AsInteger = SBPrc->Text.ToInt(); ConfTB->FieldByName("KBID")->AsInteger = KBD->KeyValue; ConfTB->FieldByName("KBNum")->AsInteger = KBPrc->Text.ToInt(); ConfTB->FieldByName("MOUSEID")->AsInteger = Mouse->KeyValue; ConfTB->FieldByName("MouseNum")->AsInteger = MouPrc->Text.ToInt(); ConfTB->FieldByName("CASEID")->AsInteger = Case->KeyValue; ConfTB->FieldByName("CaseNum")->AsInteger = CSPrc->Text.ToInt(); ConfTB->FieldByName("MONITORID")->AsInteger = Monitor->KeyValue; ConfTB->FieldByName("MONITORNum")->AsInteger = MonPrc->Text.ToInt(); ConfTB->FieldByName("SpeakerID")->AsInteger = Speakers->KeyValue; ConfTB->FieldByName("SpeakerNum")->AsInteger = SPKPrc->Text.ToInt(); ConfTB->FieldByName("FaxID")->AsInteger = Fax->KeyValue; ConfTB->FieldByName("FaxNum")->AsInteger = FaxPrc->Text.ToInt(); ConfTB->FieldByName("FDDID")->AsInteger = FDD->KeyValue; ConfTB->FieldByName("FDDNum")->AsInteger = FDDPrc->Text.ToInt(); ConfTB->FieldByName("CdromID")->AsInteger = CDROM->KeyValue; ConfTB->FieldByName("CDRomNum")->AsInteger = CDPrc->Text.ToInt(); ConfTB->FieldByName("PadID")->AsInteger = Pad->KeyValue; ConfTB->FieldByName("PadNum")->AsInteger = PadPrc->Text.ToInt(); ConfTB->Post(); } //---------------------------------------------------------------------------
Теперь поговорим о событиях. Фактически интегральная стоимость всего набора должна пересчитываться всякий раз, когда выбирается позиция из любого списка выбора или вводится значение в поле — указатель количества единиц. В первом случае для каждого из списков выбора необходимо написать функцию, которая будет отрабатываться всякий раз, как только из соответствующего списка выбора позиции будет выбираться значение:
//--------------------------------------------------------------------------- void __fastcall TMainForm::FaxCloseUp(TObject *Sender) { CalcTotalPrice(); } //---------------------------------------------------------------------------
А во втором случае пересчет интегральной стоимости целесообразно выполнить непосредственно после того, как оператор переместит фокус ввода в другой компонент интерфейса программы (используя для этого сообщение OnExit):
//--------------------------------------------------------------------------- void __fastcall TMainForm::CPPrcExit(TObject *Sender) { CalcTotalPrice(); } //---------------------------------------------------------------------------
Приложение-клиент к базам данных в нашем случае является инструментом подготовки базы данных. Им будут пользоваться операторы. А поскольку операторы могут ошибаться, рекомендуется свести возможность допущения таких ошибок к минимуму. Для этого необходимо спроектировать интерфейс программы весьма тщательным образом, по возможности минимизируя действия, выполняемые оператором при редактировании и пополнении базы данных.
Вот собственно и все. Откомпилируем и запустим полученное приложение, которое будет выглядеть следующим образом: первая вкладка («Прайс процессор») — рис. 15 и. вторая вкладка («ПК конфигуратор») — рис. 16.
Заключение
Существует множество Интернет-магазинов. От большинства из них требуется мгновенная реакция на изменение ценовой политики, перечня товаров или услуг. А все это невозможно без надежных средств автоматизации обработки огромного массива данных. Ведь перечень позиций в некоторых крупных Интернет-супермаркетах переваливает за 2-3 тысячи, а количество ежечасных транзакций (обращений) зачастую намного больше. Здесь и от сервера, и от серверной СУБД требуется недюжинное быстродействие (обычным настольным ПК и Access’ом здесь не обойтись). Да и сам ASP-код должен быть написан таким образом, чтобы исключить неоправданные циклы, задержки или лишние обращения к базе данных. ASP-код, по сути, является лишь средством визуализации содержимого базы данных, позволяющим клиентам приобретать или заказывать товары или услуги. Вся «подводная часть» айсберга манипуляций с базой данных (автоматизация обновления, редактирования, реструктуризации и т.д.) ложится именно на вспомогательные средства. Разработка таких средств требует, как правило, гораздо больше времени и сил, чем разработка самих магазинов, хотя в последнее время все чаще и чаще под последними профессионалы подразумевают именно симбиоз собственно магазинов, и средств их поддержки, профилактики, наполнения, систематизации и т.д.
Полный архив исходных текстов и проекта программы к настоящей статье лежит здесь.
Автор выражает благодарность Архангельскому А.Я. за материалы, предоставленные для подготовки настоящей статьи.
Продолжение следует, читайте скоро…
<% ASP на блюдечке %>. Часть 6. Знакомство с ASP+ и ADO+. Создаем свою инструментальную систему доступа к базам данных с помощью ASP+ и ADO+.
КомпьютерПресс 1'2001