Общие принципы и методы работы с УБД по технологии СТИКРИЗ.

  Банников Н.А. www.stikriz.narod.ru Почта На главную страницу  

Рейтинг@Mail.ru

Общие предпосылки перехода к УБД.

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

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

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

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

Базовые принципы.

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

    Главный базовый принцип очень прост: все атрибуты объекта хранятся в БД по группам, согласно низкоуровневому типу данных. Получается, что целые числа хранятся в таблице целых чисел, дробные – дробных, строки – строк и т.д. Получается некий винегрет, в котором как-то нужно разобраться. Для этого, существует справочник типов. Этот справочник описывает структуру данных, которые хранятся в УБД (здесь и далее по тексту УБД – это Универсальная База Данных). Какая структура этого справочника? В простейшем приближении справочник состоит, по меньшей мере, из двух таблиц. В первой мы будем хранить типы (описания) данных. Это номер типа и его название. Во второй таблице, которая подчинена первой – свойства типа (описания). В этой таблице есть поле с номером типа, номером свойства, названием свойства и низкоуровневым типом свойства. Допустим, у нас в УБД есть пять типов данных: целые числа, ссылка (указатель на строку в БД), дробные числа, строки и текст. Низкоуровневый тип данных будет представлен перечислением, соответственно: 1, 2, 3, 4, 5. Теперь, нам нужно создать некую ось, на которую можно будет нанизывать наши данные – экземпляры типов, или объекты.

    Давайте, немного отвлечемся, чтобы поразглагольствовать о способах связи данных в БД. Насколько мне известно, надеюсь, что и Вам, существует три типа отношений: один к одному, один ко многим и много ко многим. В нашем примере осью можно сделать некую таблицу, которая реализует древовидную структуру. В простейшем случае такую структуру можно организовать таблицей с двумя столбиками. В первом будем хранить номер строки, во второй номер главной строки. В таком случае, связь один ко многим естественным образом будет представлена в нашей БД. Т.е. каждая строка может иметь несколько подчиненных строк. Подчиненные строки удобно представлять в виде как бы слоя данных. Слой данных естественным образом может представлять собой как бы строки одной как бы таблицы. Не слишком много «как бы»? Дело в том, что все, без исключения данные будут завязаны на наше дерево данных. Человеку удобнее представлять БД в виде набора таблиц. Наверное, это сила привычки или плоскость мышления, связанная с историческим развитием мышления вокруг и с помощью печатного слова, или слишком большой радиус сферы земли, но факт остается фактом: все, даже самые сложные трехмерные модели человек может воспринимать, и воспринимает лучше, если эта модель проецируется на плоскость. Далее, отношение один к одному легко реализуется ссылкой на другую запись. Это есть, по сути, ссылочная целостность. Отношение многие ко многим, как всегда, реализуется не менее чем двумя ссылками в одном типе, т.е. как бы строке.

    Теперь, можно разобраться со структурой дерева данных поподробнее. Помимо двух столбиков, нужно хранить номер типа из справочника типов. Атрибуты, т.е. как бы столбики для строк мы договорились хранить в отдельных таблицах. Эти таблицы имеют простую схожую структуру. Есть столбик, в котором хранится номер строки из дерева данных. Столбик, который хранит номер свойства типа из справочника свойств типа. И столбик, который хранить значение атрибута. Вот приблизительная структура для БД, построенной на InterBase:

CREATE DOMAIN "TYPE_NAME" AS VARCHAR(40) CHARACTER SET WIN1251

DEFAULT '' COLLATE PXW_CYRL;

CREATE DOMAIN "TYPE_NUM" AS INTEGER NOT NULL;

CREATE DOMAIN "PROP_NUM" AS INTEGER NOT NULL;

CREATE DOMAIN "TYPE_INTEGER" AS INTEGER;

CREATE DOMAIN "TREE_DATA_NUM" AS INTEGER NOT NULL;

CREATE DOMAIN "TYPE_FLOAT" AS DOUBLE PRECISION;

CREATE DOMAIN "TYPE_STRING" AS VARCHAR(252) CHARACTER SET WIN1251

DEFAULT '';

CREATE DOMAIN "TYPE_BLOB" AS BLOB SUB_TYPE 0 SEGMENT SIZE 80;

CREATE TABLE "TYPES_LIST" /* справочник типов данных */

(

"ID_NUM" "TYPE_NUM",

"ID_PARENT" "TYPE_NUM", /* Да. Здесь подразумевается наследование */

"NAME" "TYPE_NAME",

PRIMARY KEY ("ID_NUM")

);

ALTER TABLE "TYPES_LIST" ADD FOREIGN KEY ("ID_PARENT") REFERENCES TYPES_LIST ("ID_NUM") ON DELETE CASCADE;

CREATE TABLE "PROPERTY_LIST" /* справочник свойств типа */

(

"ID_NUM" "PROP_NUM",

"ID_TYPE" "TYPE_NUM",

"ID_PARENT_TYPE" "TYPE_NUM", /* это следы оптимизации */

"ID_PARENT_PROP" "PROP_NUM",

"POSITION_NUM" "TYPE_INTEGER", /* позиция столбика в таблице данных */

"PROPERTY_TYPE" "TYPE_INTEGER", /* низкоуровневый тип: 1, 2, 3, 4, 5 */

PRIMARY KEY ("ID_NUM")

);

ALTER TABLE "PROPERTY_LIST" ADD FOREIGN KEY ("ID_PARENT_PROP") REFERENCES PROPERTY_LIST ("ID_NUM") ON DELETE CASCADE;

ALTER TABLE "PROPERTY_LIST" ADD FOREIGN KEY ("ID_TYPE") REFERENCES TYPES_LIST ("ID_NUM");

ALTER TABLE "PROPERTY_LIST" ADD FOREIGN KEY ("ID_PARENT_TYPE") REFERENCES TYPES_LIST ("ID_NUM");

CREATE TABLE "DATA_LIST" /* дерево данных */

(

"ID_NUM" "TREE_DATA_NUM",

"ID_OWNER" "TREE_DATA_NUM",

"ID_TYPE" "TYPE_NUM",

PRIMARY KEY ("ID_NUM")

);

ALTER TABLE "DATA_LIST" ADD FOREIGN KEY ("ID_TYPE") REFERENCES TYPES_LIST ("ID_NUM");

ALTER TABLE "DATA_LIST" ADD FOREIGN KEY ("ID_OWNER") REFERENCES DATA_LIST ("ID_NUM") ON DELETE CASCADE;

CREATE TABLE "Q1VL" /* целые числа */

(

"ID_NUM" "TREE_DATA_NUM",

"ID_PROP" "PROP_NUM",

"VAL" "TYPE_INTEGER",

PRIMARY KEY ("ID_NUM", "ID_PROP")

);

ALTER TABLE "Q1VL" ADD FOREIGN KEY ("ID_NUM") REFERENCES DATA_LIST ("ID_NUM");

ALTER TABLE "Q1VL" ADD FOREIGN KEY ("ID_PROP") REFERENCES PROPERTY_LIST ("ID_NUM");

CREATE TABLE "Q2VL" /* ссылка */

(

"ID_NUM" "TREE_DATA_NUM",

"ID_PROP" "PROP_NUM",

"VAL" "TYPE_INTEGER",

PRIMARY KEY ("ID_NUM", "ID_PROP")

);

ALTER TABLE "Q2VL" ADD FOREIGN KEY ("ID_NUM") REFERENCES DATA_LIST ("ID_NUM");

ALTER TABLE "Q2VL" ADD FOREIGN KEY ("ID_PROP") REFERENCES PROPERTY_LIST ("ID_NUM");

ALTER TABLE "Q2VL" ADD FOREIGN KEY ("VAL") REFERENCES DATA_LIST ("ID_NUM");

CREATE TABLE "Q3VL" /* дробные числа */

(

"ID_NUM" "TREE_DATA_NUM",

"ID_PROP" "PROP_NUM",

"VAL" "TYPE_FLOAT",

PRIMARY KEY ("ID_NUM", "ID_PROP")

);

ALTER TABLE "Q3VL" ADD FOREIGN KEY ("ID_NUM") REFERENCES DATA_LIST ("ID_NUM");

ALTER TABLE "Q3VL" ADD FOREIGN KEY ("ID_PROP") REFERENCES PROPERTY_LIST ("ID_NUM");

CREATE TABLE "Q8VL" /* строки */

(

"ID_NUM" "TREE_DATA_NUM",

"ID_PROP" "PROP_NUM",

"VAL" "TYPE_STRING",

PRIMARY KEY ("ID_NUM", "ID_PROP")

);

ALTER TABLE "Q8VL" ADD FOREIGN KEY ("ID_NUM") REFERENCES DATA_LIST ("ID_NUM");

ALTER TABLE "Q8VL" ADD FOREIGN KEY ("ID_PROP") REFERENCES PROPERTY_LIST ("ID_NUM");

CREATE TABLE "Q11VL" /* текст. Я использую реальные наименования таблиц из УБД */

(

"ID_NUM" "TREE_DATA_NUM",

"ID_PROP" "PROP_NUM",

"VAL" "TYPE_BLOB",

PRIMARY KEY ("ID_NUM", "ID_PROP")

);

ALTER TABLE "Q11VL" ADD FOREIGN KEY ("ID_NUM") REFERENCES DATA_LIST ("ID_NUM");

ALTER TABLE "Q11VL" ADD FOREIGN KEY ("ID_PROP") REFERENCES PROPERTY_LIST ("ID_NUM");

    Как видите, структура УБД довольна проста. Осталось не забыть, что при добавлении строки в дерево данных нужно добавить необходимое количество строк в таблицы значений, а при удалении – удалить эти строки из нужных таблиц. В InterBase эту работу можно переложить на триггеры. Запрос Select данных из УБД строится автоматически по номеру слоя, т.е. строки, которой принадлежит слой с данными и номеру типа из справочника типов. В результате мы покажем пользователю таблицу, которая содержит данные одного типа в одном слое. Вот примерный запрос для типа, состоящего из целого числа, двух строк и текста:

SELECT D.ID_NUM,

V0.VAL, V1.VAL, V2.VAL, V3.VAL

FROM DATA_LIST D

LEFT JOIN Q1VL V0 ON ( D.ID_NUM = V0.ID_NUM AND V0.ID_PROP = 38 )

LEFT JOIN Q4VL V1 ON ( D.ID_NUM = V1.ID_NUM AND V1.ID_PROP = 39 )

LEFT JOIN Q4VL V2 ON ( D.ID_NUM = V2.ID_NUM AND V2.ID_PROP = 40 )

LEFT JOIN Q11VL V3 ON ( D.ID_NUM = V3.ID_NUM AND V3.ID_PROP = 41 )

WHERE D.ID_TYPE = 8 AND D.ID_OWNER = :IdOwner

    Здесь IdOwner – это параметр, в котором хранится целое число с номером слоя. D.ID_TYPE = 8 – это номер типа по справочнику, а VX.ID_PROP = XX – это номер свойства по справочнику. Как видно из примера, запрос легко создать автоматически по описанию типа. Так и будем поступать, т.е. при первом обращении к типу создавать для него нужный запрос и хранить его в памяти, чтобы не пересоздавать в следующий раз. С удалением все обстоит значительно проще – нужно удалить строку из дерева данных, а триггер позаботится об остальном. При редактировании – сложнее. Эта сложность присуща только InterBase. Придется посылать несколько запросов на обновление таблиц значений. Понятно, что все обновления должны быть в одной транзакции.

    Если нам нужно выбрать все данные этого типа, то можно отказаться от AND D.ID_OWNER = :IdOwner в конструкции WHERE, тогда выберем все. Если нужно выбрать тип и всех его потомков, то можно использовать UNION с запросами для всех типов, но только со свойствами, наследованными от предка.

    Допустим, Вы скажете, что нужно выбрать все данные одного типа, но в выборочном количестве слоев. И начнутся дебаты об ограниченности модели данных в УБД. Помните головоломку о шести спичках, из которых нужно собрать четыре треугольника? В УБД, тоже, нужно мыслить немного объемно. Не забывайте, что есть такая вещь как наследование. Если даже данные одинаковы по структуре, то это еще не значит, что они должны быть одного типа. Вводите в нужные слои данные одного типа, а в ненужные – другого. В конце концов, для каждого такого слоя можно создать по отдельному типу, а объединять данные с разных слоев можно через UNION.

    Еще, хочу заметить, что ничего Вам не мешает в одном слое данных хранить разные объекты сколько угодно большого количества типов.

Структура программного комплекса.

    Программный комплекс «Универсальная База Данных по технологии СТИКРИЗ» построен по классической трехзвенной архитектуре. И этому есть ряд причин. Есть причины концептуальные и технологические – никакого субъективизма. Технологические причины – это необходимость трансформации данных, т.к. структура данных, с которыми работает пользователь, мягко говоря, совсем не та, что хранится в БД. Второе – это сокращение обмена информацией с SQL Server по сети, т.к. описания типов, запросы и некоторые другие важные объекты совсем необязательно хранить на стороне клиента. Главной же концептуальной причиной является то, что процесс проверяющий права пользователя должен работать на сервере, а не на машине клиента. Проверять права пользователей средствами SQL Server не получится, т.к. права давать нужно на отдельные строки дерева данных. Более того, нужно исключить всякую возможность доступа к БД посредством стандартных инструментов, например IbConsole, т.к. в УБД вручную делать вообще-то нечего – слишком велика возможность ошибки, из-за сложности структуры данных. Перефразируя пословицу: «То, что для русского забава – китайцу смерть», можно сказать: «То, что для программы легко – человеку не под силу».

    Итак, у нас есть какой-нибудь SQL Server, например InterBase, УБД сервер (сервер приложений) и УБД клиент («тонкий» клиент J ). Клиент и сервер УБД общаются через COM интерфейс.

Обзор возможностей УБД.

    На самом деле, структура справочника типов значительно более объемная, чем я Вам показал. Это не со зла – посмотрите реальную структуру в IbConsole: Database | View Metadata, где нужно найти таблицы с именами, которые представлены в данной статье. Структура этого справочника позволяет описать интерфейс пользователя при работе с данными и множество тонких настроек. Более того, описания типов содержат не только описания свойств, но и методы, которые срабатывают по событиям, происходящим при работе с данными. Например, есть конструктор и деструктор, события на редактирование, подтверждение или отказ, нажатие кнопки и т.д. Все эти методы выполняются в клиентской части комплекса, и представляют собой программы встроенного калькулятора. Калькулятор – это от скромности. На самом деле он является полноценным двухпроходным интерпретатором, построенным на алгоритме рекурсивного спуска. Синтаксис языка программирования калькулятора оптимизирован так, чтобы программы, написанные на нем, выполнялись как можно быстрее, поэтому его синтаксис не похож ни на один из языков программирования, хотя это не было бы проблемой, например С. Калькулятор имеет большое количество встроенных функций общего назначения, например, все, которые есть в 1С и более, есть функции доступа к объектам базы данных. Скорость исполнения программ калькулятора настолько высока, что позволяет использовать его для обработки событий вычисления вычисляемых полей таблицы, или обрабатывать нажатие кнопки пользователем, и т.д.

    Благодаря возможности тонкой настройки в справочнике типов, клиент УБД может работать с любыми данными, которые есть в УБД. Т.е. Вам никогда не потребуется перекомпилировать клиента УБД, если Вы меняете настройки. Это важное свойство способствовало тому, чтобы появился новый класс программ для УБД. В самом деле, откуда нам знать, что захочет пользователь, а главное, как он собирается в будущем обрабатывать данные в УБД. Можно предположить, что пользователь, конечно же захочет их печатать, или запускать сложные расчеты, например, пересчет зарплаты, или налогов… А если он захочет их посылать по e-mail, да чтобы прямо из программы? Да, мало ли чего. Чтобы любые потребности, и даже прихоти пользователя можно было удовлетворить, создан стандарт для подключения к УБД т.н. роботов – отдельных серверов приложений, выполняющих какой-нибудь класс действий над данными в УБД. Например, сейчас разработаны два робота: робот печатник и робот мигратор. Печатник может печатать любой документ в УБД. Он построен на основе Fast Report, который дополнен компонентами доступа к данным в УБД через УБД сервер. Мигратор – это робот способный делать рутинную работу в УБД по пересчету. Копированию и т.д. данных. Главный компонент мигратора – это калькулятор. Все эти роботы имеют возможность создавать настройки. Печатник – стандартное визуальное средство Fast Report, а мигратор – редактор программ калькулятора. В разработке находятся и другие роботы, например, робот, отсылающий письма по e-mail, и робот, способный воспроизводить презентации, на основе данных в УБД. Интерфейсы этих роботов одинаковы и довольно просты. Роботы могут получать от клиентской программы необходимые параметры – все это настраивается. В дальнейшем, на моем сайте, я познакомлю Вас с ними, и Вы сможете сами разрабатывать роботов.

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

Оценка производительности.

    Мне часто задают вопрос о скорости работы УБД. А быстро ли она работает? А подойдет ли она для автоматизации крупного предприятия?
    Возьмем «Мир ПК» #4 за 2002 год. На стр. 51 читаем победную новость уважаемой компании о своей замечательной системе:
«Предприятие работает с 3 тыс. контрагентов, выпускает около 1 тыс. видов товаров и материалов, ежегодно вводит 20 тыс. операционных документов. Естественно, очень сложно управлять этим хозяйством, не создав единую информационную среду и не построив современную корпоративную систему управления.» Это цитата. Что мы можем от сюда почерпнуть? Справочник контрагентов составляет около 3 тыс. строк – это более чем допустимо. Справочник видов товаров и материалов – 1 тыс. строк – мелочь. 20 тыс. операционных документов, разбитых по месяцам, или даже по годам – нет проблем. Если по месяцам, то открытие любого месяца – пара секунд.

    Возьмем бухгалтерию. К примеру, ОАО Novoship. Списочный состав – не более 3.5 тыс. человек в одной компании (а там их много). Единственная кажущаяся непреодолимым препятствием – это база с проводками, которых уйма, и даже больше (наверное, уже больше 1 Гб). Проводки – это закостенелый тип данных. Никаких изменений в них, до самого конца света не будет. Нет, не план счетов, а именно структура, в которой они хранятся. Так что нет проблем – делаем этот тип на основе обычной таблицы и все.

    Но, не все так просто на данном этапе, а именно на 8 мая 2002 года. Самой большой проблемой при автоматизации, даже не очень крупного предприятия является распределенность баз и репликация. Я бы даже сказал, что у крупных предприятий с этим проблем, как правило, меньше, т.к. они могут позволить себе иметь высокоскоростную выделенную линию связи с филиалами. На данном этапе разработки УБД модули распределения баз и репликации находятся в состоянии вяло текущего проектирования, но все может очень быстро изменится. Так как УБД имеет жесткую раз и навсегда заданную структуру БД, поэтому и распределение данных по разным базам в online режиме работы и репликация не являются серьезной проблемой.

Факторы, влияющие на производительность.

    Как – то пришло ко мне письмо с вопросом: «Вот, мол, мы пишем свою базу с объектным подходом, но, если в базе более 1000 строк, то начинаются жуткие тормоза. А как у Вас?». Я быстро ответил, что и у меня при увеличении количества строк, тоже, начинаются тормоза. Отослал и пожалел, что так быстро. Может, люди создали нечто, что в базе начинаются проблемы после 1000 строк? В УБД размер базы не критичен, по крайней мере, не более, чем для обычных программ, написанных обычными методами. Собственно, Вы можете убедиться, что «сборка» данных запросом производится, исключительно используя оптимальные планы, и ведется, используя первичные ключи. Никаких натуральных переборов. Более того, по мере оптимизации кода сервера УБД и клиентской части тормоза при закачке строк с сервера на клиент постепенно уменьшаются. На самом деле, критичным является единовременная выборка определенного количества строк при открытии документа, т.к. УБД закачивает сразу все строки на клиент. На данном этапе дела обстоят примерно так: если сервером выступает AMD Atlon 1000, с обычным IDE диском, 256 Мб памяти, УБД сервер и InterBase на одном компьютере, сетка 100 Мб, а клиентом – PIII 500, 256 Мб памяти, то Fech чуть более 20 000 строк с 15 столбиками разного типа: целые числа, даты, строки и т.д. происходит менее чем за 10 секунд. Так же, не влияет на производительность глубина наследования, и количество типов данных в справочнике типов. Никак не влияет на производительность сложность дерева данных. Однако, есть некоторые проблемы у клиентской части комплекса с вычисляемыми полями. Вычисляются значения в вычисляемых полях только в том случае, если к ним есть обращение по чтению значения, но вычисляются сразу все поля одной записи. Т.е. если Вы видите в окне 80 строк данных, то только для них и будут вычисляться вычисляемые поля. Это хорошо. Но, если Вы, например, решили скопировать все строки в буфер обмена, чтобы вставить их, например в Excel, то будут вычисляться все строки. Мало – ли какой у Вас там алгоритм, да и буфер обмена... Так что при проектировании данных это нужно учитывать. Так же, нужно учитывать, что данные Blob полей и производных от них могут не закачиваться на клиента сразу. Но при редактировании строки, закачаются обязательно. Если Вы используете какой – нибудь тип данных для сложных расчетов, то не делайте в нем текстовых данных или других Blob полей.

    Это, пожалуй, самые наболевшие моменты. Подробнее о правилах построения быстрых и эффективных настроек можно будет прочитать в следующей статье, так что продолжение следует…

     Банников Н.А. www.stikriz.narod.ru почта 2002 г.

Сайт создан в системе uCoz