Здравствуйте.
Этот материал призван обобщить и показать основные техники и приемы для работы с базами данных InterBase/FireBird из программ, запускаемых на платформе 1С. Почти все, что изложено здесь, так или иначе встречается в других интернет-источниках. Однако в этой статье материал достаточно полный. Возможно, что-то из заявленной темы и упущено, но все равно, изложенного материала (в моем понимании) достаточно для решения большинства задач (кстати, кое-что не получилось – может кто-нибудь подскажет решение, и можно будет дополнить материал). Кроме того, демонстрационный пример, приложенный к статье, может быть использован желающими в качестве каркаса для разработки собственных подобных программ. Несмотря на то, что большинство задач по доступу из 1С к сторонним базам ограничиваются необходимостью загрузить какие-либо данные из смежной информационной системы, мы покажем здесь более разнообразные варианты доступа к базам, в том числе различные виды записи информации в БД.
В этой публикации будет рассказано о том:
— как подключиться к серверу (базе) и отключиться при завершении работы;
— как выполнять SQL-запросы (принимать и передавать данные, в том числе и те, которые не могут быть размещены непосредственно в тексте запроса (длинные строки, BLOB-поля));
— как выполнять преобразование данных, в т.ч. текстовых кодировок, при передаче данных в базу и из базы;
— как обращаться к серверной логике базы (вызывать хранимые процедуры);
— как защитить свою программу от автоматического разрыва TCP-соединения сервером БД;
— прочие интересные приемы, связанные с программированием вообще (как сделать все вышеперечисленные действия удобно, надежно и переносимо).

Итак, приступим…

Вступление.


Демонстрационный пример получился весьма большим. Тем не менее, структура программы достаточно четкая – так что разобраться вполне реально.
В программе (это внешняя обработка) активно используется особый прием структурного программирования, условно названный «ООП в 1С» (подробнее описано здесь). По сути, почти весь общий модуль обработки (часть программы, не связанная непосредственно с интерфейсом пользователя) состоит из таких структур.
Также использована такая интерфейсная «приспособа», как «список информационных сообщений» (подробнее описано здесь).

Программа проверялась на версиях FireBird 1.5 и FireBird 2.1. Скорее всего, будет работать и для любой другой версии СУБД. Данный механизм доступа к базе использует технологию ADO. Соответственно, на компьютере, где запускается клиент 1С, должен быть установлен драйвер «Firebird_ODBC», а также клиентская библиотека «gds32.dll».

Полные исходные тексты программы приводить не буду (даже исходные тексты отдельных классов будут приведены с сокращениями). Для просмотра исходных текстов лучше открыть тестовый пример непосредственно в 1С – качество просмотра будет гораздо выше, чем то, что доступно средствами HTML-публикации.

Постановка задачи (суть демонстрационного примера).


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

Вот как это приложение будет выглядеть (покажем на серии слайдов-скриншотов):
Главное окно программы:

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

Далее будем считать, что:
— «Текущая группа» — это группа, выбранная в дереве групп (в левой колонке);
— «Содержимое текущей группы» — это список групп и элементов, отображаемый в правой колонке;
— «Текущий объект текущей группы» — это группа или элемент, выбранный в правой колонке;

Все команды (добавить, редактировать, удалить, переместить) относятся к содержимому правой колонки (либо к текущей группе, либо к текущему элементу текущей группы) – как и в стандартном интерфейсе работы со справочниками 1С.

Главное меню и панель инструментов содержат следующие команды:
Изобр. Название команды Описание
«База»>«Подключение» Выполняет подключение к базе (закрывает предыдущее подключение, если оно было)
«База»>«Выход» Выход из программы

«Действия»>«Добавить» Создать новый элемент (карточку) в текущей группе справочника.

«Действия»>«Новая группа» Создать новую группу в текущей группе справочника.

«Действия»>«Скопировать» Создать новый объект в текущей группе справочника на основе данных текущего выделенного объекта. Что именно создается (группа или карточка) зависит от того, какой объект был выделен в текущей группе .
«Действия»>«Изменить» Открыть на редактирование текущий объект текущей группы (группу или карточку).

«Действия»>«Удалить» Удалить текущий объект текущей группы (группу или карточку).

«Действия»>«Переместить в группу» Переместить в другую группу текущий объект текущей группы (группу или карточку).

«Действия»>«Обновить» Заново перезагрузить все данные (и дерево групп и список содержимого текущей группы).
«Настройки»>«Смена пароля» Смена пароля текущего пользователя
«Настройки»>«Список пользователей» Редактирование списка пользователей базы.
«Настройки»>«Профили подключения» Редактирование профилей подключения к базам

Окно программы до подключения к базе:


Диалог выполнения подключения:
(Команда: «База»>«Подключение»)


Окно программы после подключения к базе:


Диалог настройки профилей подключения к базам:
(Команда: «Настройки»>«Профили подключения»)


Диалог смены пароля:
(Команда: «Настройки»>«Смена пароля»)


Настройки списка пользователей:
(Команда: «Настройки»>«Список пользователей»)


Редактирование группы справочника контактов:
(Команды: «Действия»>«Новая группа», «Действия»>«Скопировать», «Действия»>«Изменить»)


Редактирование элемента справочника контактов (карточки):
(Команды: «Действия»>«Добавить», «Действия»>«Скопировать», «Действия»>«Изменить»)


Диалог выбора группы в дереве групп:
Используется для указания родительской группы в диалогах редактирования группы и карточки, а также для указания группы на значения при перемещении текущего объекта текущей группы (объекта, выбранного в правом списке главного окна программы) в новую группу.


Структура базы данных.


Основные сущности системы:
1. Справочник пользователей системы (учетных записей). Состоит только из элементов (учетных записей). Соответственно в базе представляется одной таблицей.
2. Собственно сам целевой справочник контактной информации. Состоит из групп (дерево справочника) и элементов (карточек). В базе справочник будет представлен отдельной таблицей групп (в ней будет храниться дерево уровней справочника) и отдельной таблицей элементов (в ней будут храниться сами карточки с контактами).

В базе данных создадим следующие таблицы:
— таблица «GROUPS» — хранение дерева групп;
Название поля Тип данных Описание
GRID INTEGER Идентификатор записи в таблице – идентификатор узла дерева (все значения >0).
PGRID INTEGER Идентификатор родительского узла. Таким образом, устанавливается связь между узлами в единое дерево. Значение 0 означает, что данный узел входит в «корень» дерева. Сам корень в базе не хранится (он всегда есть в программе – его нельзя изменить).
GRNAME VARCHAR(255) Наименование узла дерева. Уникальность этих значений (в пределах таблицы) не отслеживается, как и в справочниках 1С.

— таблица «CARDS» — хранение элементов справочника – карточек с контактными данными;
Название поля Тип данных Описание
CDID INTEGER Идентификатор записи в таблице – идентификатор карточки (все значения >0).
GRID INTEGER Идентификатор родительского узла дерева (ссылка на GROUPS.GRID). Таким образом, устанавливается связь карточки с деревом. Значение 0 означает, что данная карточка входит в «корень» дерева (хотя в таблице GROUPS записи с GRID=0 нет).
GRNAME VARCHAR(255) Наименование карточки. Уникальность этих значений (в пределах таблицы) не отслеживается, как и в справочниках 1С.
CDDESCRIPT VARCHAR(255) Поле карточки «Описание».
CDCONTACTS BLOB (TEXT – 1) Контактная информация. По факту текстовое поле в виде типа MEMO – большой текст переменной длины, хранимый отдельно от основной таблицы. К нему применяются все правила автоматической работы с кодировками (как у CHAR и VARCHAR).
CDNOTE BLOB (BINARY – 0) Примечания. По факту текстовое поле, но описано как бинарный BLOB. Так делать не следует, но поскольку формат базы данных часто выбираем не мы, то такой вариант вполне возможен. На этом примере покажем, как работать с текстом (кодировками) в случае, если база данных не знает о том, что это текст.
CDIMAGE BLOB (BINARY – 0) Здесь хранится изображение, прикрепленное к карточке. По факту содержит тоже самое, что и соответствующий графический файл.

— таблица «USERS» — учетные данные информационной системы;
Название поля Тип данных Описание
UID INTEGER Идентификатор записи в таблице.
UNAME VARCHAR(255) Название учетной записи пользователя (логин). Программа контролирует уникальность (в пределах таблицы) этого поля (без учета регистра символов – т.е. при наличии пользователя «admin», учетную запись «AdMiN» создать нельзя).
UPWD VARCHAR(255) Пароль пользователя (хранится в открытом виде, т.к. функция md5 отсутствует в младших версиях FireBird, а реализация этого алгоритма в 1С тоже объемная – не хотелось «заморачиваться»).
URIGHTS VARCHAR(255) Полномочия пользователя – строка из символов «0» и «1», которые находясь в соответствующей позиции, показывают отсутствие («0») или наличие («1») соответствующих полномочий у пользователя.
В данной версии используются следующие права пользователя:
-позиция 1 – возможность просматривать данные;
-позиция 2 – возможность редактировать данные;
-позиция 3 – возможность редактировать список пользователей (администрирование)

Также в базе будут размещены следующие хранимые процедуры:
Название процедуры Название параметра Тип параметра Описание
SAVE_GROUP Процедура выполняет сохранение (добавление или обновление) группы в базу.
Входные параметры:
GRID INTEGER Идентификатор сохраняемой группы (0, если группа не сохранялась ранее)
PGRID INTEGER Идентификатор родительской группы
GRNAME VARCHAR(255) Название группы
Выходные параметры:
RESULT INTEGER Идентификатор сохраненной группы, если ее удалось сохранить;
0 — если группа не была сохранена
ERRMSG VARCHAR(255) Текст сообщения об ошибке, если ошибка произошла (RESULT=0)
DELETE_GROUP Процедура выполняет удаление группы из базы.
Входные параметры:
GRID INTEGER Идентификатор удаляемой группы
Выходные параметры:
RESULT INTEGER Идентификатор удаленной группы, если ее удалось удалить;
0 — если группа не была удалена
ERRMSG VARCHAR(255) Текст сообщения об ошибке, если ошибка произошла (RESULT=0)
MOVE_GROUP_TO_GROUP Процедура выполняет перенос указанной группы и всего ее содержимого (подгрупп и вложенных карточек) в другую указанную группу.
Входные параметры:
SRC_GRID INTEGER Идентификатор группы, которую нужно перенести
DST_GRID INTEGER Идентификатор группы, в которую нужно перенести исходную группу
Выходные параметры:
RESULT INTEGER Идентификатор группы (SRC_GRID), если она была перенесена;
0 — если перенос группы не состоялся (она не была перенесена)
ERRMSG VARCHAR(255) Текст сообщения об ошибке, если ошибка произошла (RESULT=0)
MOVE_GROUP_CONTENTS_TO_GROUP Процедура выполняет перенос содержимого указанной группы (подгрупп и вложенных карточек) в другую указанную группу. При этом сама исходная группа остается на прежнем месте.
Входные параметры:
SRC_GRID INTEGER Идентификатор группы, содержимое которой нужно перенести
DST_GRID INTEGER Идентификатор группы, в которую нужно перенести содержимое группы
Выходные параметры:
RESULT INTEGER Идентификатор группы (SRC_GRID), если ее содержимое было перенесено;
0 — если перенос содержимого группы не состоялся
ERRMSG VARCHAR(255) Текст сообщения об ошибке, если ошибка произошла (RESULT=0)
MOVE_CARD_TO_GROUP Процедура выполняет перенос карточки в указанную группу.
Входные параметры:
SRC_CDID INTEGER Идентификатор карточки, которую нужно перенести
DST_GRID INTEGER Идентификатор группы, в которую нужно перенести карточку
Выходные параметры:
RESULT INTEGER Идентификатор карточки (SRC_CDID), если она была перенесена;
0 — если перенос карточки не состоялся (она не была перенесена)
ERRMSG VARCHAR(255) Текст сообщения об ошибке, если ошибка произошла (RESULT=0)
TEST_GROUP_PARENT_HIERARCHY Процедура проверяет, является ли группа PGRID иерархическим (возможно через несколько промежуточных узлов) родителем группы GRID или нет. Данная процедура не вызывается из программы, а используется в других процедурах.
Входные параметры:
GRID INTEGER Идентификатор группы, вхождение которой в группу PGRID нужно проверить
PGRID INTEGER Идентификатор группы, вхождение в которую группы GRID нужно проверить
Выходные параметры:
RESULT SMALLINT Результат проверки:
1 — если группа PGRID является иерархическим родителем группы GRID;
0 — если группа PGRID не является родителем группы GRID

Процедур для работы с карточками (SAVE_CARD, DELETE_CARD) и пользователями (SAVE_USER, DELETE_USER) не будет. Реализуем их функционал непосредственно в 1С, чтобы показать вызов из 1С не только хранимых процедур, но и разнообразных запросов.

Представление сущностей информационной системы в программе.


В нашей системе есть следующие информационные сущности:
группа справочника контактов (набор групп, образующих дерево (поддерево) групп – полное или частичное представление данных таблицы GROUPS);
элемент (карточка) справочника контактов (набор карточек – полное или частичное представление данных таблицы CARDS);
учетная запись системы (набор учетных записей – полное или частичное представление данных таблицы USERS)

Модель представлена в коде 1С следующими классами:
BaseObj (БазоваяСущность) – класс – абстрактный родитель для всех остальных классов из данной группы. Содержит общие поля и методы.
ObjGroup (СущностьГруппа) – потомок от BaseObj, представляет собой реализацию сущности «группа справочника контактов».
ObjCard (СущностьКарточка) – потомок от BaseObj, представляет собой реализацию сущности «элемент справочника контактов».
ObjUser (СущностьПользователь) – потомок от BaseObj, представляет собой реализацию сущности «пользователь системы».
ObjSet (НаборОбъектов) – тоже потомок от BaseObj, но является не сущностью, а универсальной коллекцией сущностей, которой можно представить набор из объектов любого типа (ObjGroup, ObjCard, ObjUser). Естественно, что нельзя смешивать в одном наборе сущности разных типов.

Соответствующая реализация этих классов в 1С будет выглядеть так:
Код классов
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// К Л А С С Ы    П Р Е Д С Т А В Л Я Ю Щ И Е    С У Щ Н О С Т И    С И С Т Е М Ы
//
///////////////////////////////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////////////
// Реализация класса БазоваяСущность (BaseObj)

// Создание полей объекта в виде структуры.
//
// Параметры:
//  Нет.
// Возврат:
//  Возвращает созданную структуру
//
Функция BaseObj_СоздатьОбъект() Экспорт
	
	BaseObj = Новый Структура;
	// Общие параметры сущности
	BaseObj.Вставить("ClassID", 0);      // Идентификатор класса сущности (INT)
	BaseObj.Вставить("ChangeInfo", Ложь);// Признак наличия несохраненных изменений в наборе	
	BaseObj.Вставить("ErrMsg", "");      // Сообщение о последней ошибке (STR)
	BaseObj.ClassID = Проч.ClassIDs.CLN_UNKNOWN;   // Неизвестный тип данных
	
	Возврат BaseObj;
	
КонецФункции

// Имитирует конструктор объекта.
//
// Параметры:
//  ClassID - тип объекта, который создается. Если не указано, то создается
//    обычная заготовка - базовый класс
// Возврат:
//  (Структура) - Структура с полями объекта
//
Функция BaseObj_Конструктор(ClassID=0) Экспорт
	
	BaseObj = Неопределено;
	
	Если ClassID = Проч.ClassIDs.CLN_GROUP Тогда
		// Тип ObjGroup
		BaseObj = ObjGroup_Конструктор()
	ИначеЕсли ClassID = Проч.ClassIDs.CLN_CARD Тогда
		// Тип ObjCard
		BaseObj = ObjCard_Конструктор()
	ИначеЕсли ClassID = Проч.ClassIDs.CLN_USER Тогда
		// Тип ObjUser
		BaseObj = ObjUser_Конструктор()
	КонецЕсли;
	
	Если BaseObj = Неопределено Тогда
		// Создать просто базовый объект
		BaseObj = BaseObj_СоздатьОбъект();
	КонецЕсли;
	
	Возврат BaseObj;
	
КонецФункции

// Имитирует деструктор объекта - освобождает память.
//
// Параметры:
//  BaseObj - ссылка на объект, порожденный от базового класса
//
Процедура BaseObj_Деструктор(BaseObj) Экспорт
	
	Если BaseObj.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
		// Тип ObjGroup
		ObjGroup_Деструктор(BaseObj)
	ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_CARD Тогда
		// Тип ObjCard
		ObjCard_Деструктор(BaseObj)
	ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_USER Тогда
		// Тип ObjUser
		ObjUser_Деструктор(BaseObj)
	КонецЕсли

КонецПроцедуры

// Инициализирует атрибуты объекта значениями по умолчанию.
//
// Параметры:
//  BaseObj - ссылка на объект
//
Процедура BaseObj_SetDefAttr(BaseObj) Экспорт
	
	BaseObj.ErrMsg = "";
	Если BaseObj.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
		// Тип ObjGroup
		ObjGroup_SetDefAttr(BaseObj)
	ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_CARD Тогда
		// Тип ObjCard
		ObjCard_SetDefAttr(BaseObj)
	ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_USER Тогда
		// Тип ObjUser
		ObjUser_SetDefAttr(BaseObj)
	КонецЕсли
	
КонецПроцедуры

// Копирует значения полей объекта BaseObj2 в текущий BaseObj1.
// Все предыдущие значения из BaseObj1 будут утеряны.
//
// Параметры:
//  BaseObj1 - ссылка на объект
//  BaseObj2 - ссылка на другой копируемый объект
//
Процедура BaseObj_Assign(BaseObj1, BaseObj2) Экспорт
	
	Если (BaseObj1 <> Неопределено) И
		 (BaseObj2 <> Неопределено) И
		 (BaseObj1.ClassID = BaseObj2.ClassID) И
		 (BaseObj_IsEqual(BaseObj1, BaseObj2) = Ложь) Тогда
		Если BaseObj1.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
			// Тип ObjGroup
			ObjGroup_Assign(BaseObj1, BaseObj2)
		ИначеЕсли BaseObj1.ClassID = Проч.ClassIDs.CLN_CARD Тогда
			// Тип ObjCard
			ObjCard_Assign(BaseObj1, BaseObj2)
		ИначеЕсли BaseObj1.ClassID = Проч.ClassIDs.CLN_USER Тогда
			// Тип ObjUser
			ObjUser_Assign(BaseObj1, BaseObj2)
		КонецЕсли
	КонецЕсли
	 
КонецПроцедуры

// Сравнивает два объекта между собой.
//
// Параметры:
//  BaseObj1 - ссылка на объект
//  BaseObj2 - ссылка на другой сравниваемый объект
// Возврат
//  Истина, если объект BaseObj2 содержит теже данные, что и
//  текущий BaseObj1 и Ложь - в противном случае
//
Функция BaseObj_IsEqual(BaseObj1, BaseObj2) Экспорт
	
	Рез = Ложь;
	
	Если (BaseObj1 <> Неопределено) И
		 (BaseObj2 <> Неопределено) И
		 (BaseObj1.ClassID = BaseObj2.ClassID) Тогда
		Если BaseObj1.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
			// Тип ObjGroup
			Рез = ObjGroup_IsEqual(BaseObj1, BaseObj2)
		ИначеЕсли BaseObj1.ClassID = Проч.ClassIDs.CLN_CARD Тогда
			// Тип ObjCard
			Рез = ObjCard_IsEqual(BaseObj1, BaseObj2)
		ИначеЕсли BaseObj1.ClassID = Проч.ClassIDs.CLN_USER Тогда
			// Тип ObjUser
			Рез = ObjUser_IsEqual(BaseObj1, BaseObj2)
		КонецЕсли
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Сравнивает ключевые поля двух объектов между собой.
//
// Параметры:
//  BaseObj1 - ссылка на объект
//  BaseObj2 - ссылка на другой сравниваемый объект
// Возврат
//  Истина, если объект BaseObj2 содержит теже значения ключевых полей, что и
//  текущий BaseObj1 и Ложь - в противном случае
//
Функция BaseObj_IsEqualKeys(BaseObj1, BaseObj2) Экспорт

	Рез = Ложь;
	
	Если (BaseObj1 <> Неопределено) И
		 (BaseObj2 <> Неопределено) И
		 (BaseObj1.ClassID = BaseObj2.ClassID) Тогда
		Если BaseObj1.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
			// Тип ObjGroup
			Рез = ObjGroup_IsEqualKeys(BaseObj1, BaseObj2)
		ИначеЕсли BaseObj1.ClassID = Проч.ClassIDs.CLN_CARD Тогда
			// Тип ObjCard
			Рез = ObjCard_IsEqualKeys(BaseObj1, BaseObj2)
		ИначеЕсли BaseObj1.ClassID = Проч.ClassIDs.CLN_USER Тогда
			// Тип ObjUser
			Рез = ObjUser_IsEqualKeys(BaseObj1, BaseObj2)
		КонецЕсли
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Проверяет, являются ли значения ключеных полей объекта пустыми (незначащими),
// т.е. по сути это новый объект, ни разу не сохраненный в БД.
//
// Параметры:
//  BaseObj - ссылка на объект
// Возврат
//  Истина, если объект BaseObj содержит пустые ключевые поля и Ложь - в противном случае
//
Функция BaseObj_IsKeyEmpty(BaseObj) Экспорт

	Рез = Ложь;
	
	Если BaseObj <> Неопределено Тогда
		Если BaseObj.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
			// Тип ObjGroup
			Рез = ObjGroup_IsKeyEmpty(BaseObj)
		ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_CARD Тогда
			// Тип ObjCard
			Рез = ObjCard_IsKeyEmpty(BaseObj)
		ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_USER Тогда
			// Тип ObjUser
			Рез = ObjUser_IsKeyEmpty(BaseObj)
		КонецЕсли
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Возвращает сообщение о последней возникшей ошибке.
//
// Параметры:
//  BaseObj - ссылка на объект
//
Функция BaseObj_GetErrorMsg(BaseObj) Экспорт
	
	Возврат BaseObj.ErrMsg
	
КонецФункции

// Возвращает признак наличия несохраненных изменений.
//
// Параметры:
//  BaseObj - ссылка на объект
// Возврат
//  Значение признака несохраненных изменений
//
Функция BaseObj_GetChangeInfo(BaseObj) Экспорт
	
	Возврат BaseObj.ChangeInfo
	
КонецФункции

// Принудительно устанавливает значение признака наличия
// несохраненных изменений.
//
// Параметры:
//  BaseObj - ссылка на объект
//  ChangeInfo - значение признака несохраненных изменений
//
Процедура BaseObj_SetChangeInfo(BaseObj, ChangeInfo) Экспорт
	
	Если BaseObj <> Неопределено Тогда
		Если BaseObj.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
			// Тип ObjGroup
			ObjGroup_SetChangeInfo(BaseObj, ChangeInfo)
		ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_CARD Тогда
			// Тип ObjCard
			ObjCard_SetChangeInfo(BaseObj, ChangeInfo)
		ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_USER Тогда
			// Тип ObjUser
			ObjUser_SetChangeInfo(BaseObj, ChangeInfo)
		КонецЕсли
	КонецЕсли
	
КонецПроцедуры

// Загружает поля объекта из текущей строки набора данных DS.
//
// Параметры:
//  BaseObj - ссылка на объект
//  DS - набор данных типа ADORecordSet
// Возврат:
//  Истина, если загрузка выполнена успешно и Ложь, если возникли ошибки
//
Функция BaseObj_LoadFromDataSet(BaseObj, DS) Экспорт

	Рез = Ложь;  // Параметры еще не прочитаны
	BaseObj.ErrMsg = "";
	
	// Пробуем выполнить загрузку, не обращая внимание на состояние параметров
	Если BaseObj.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
		// Тип ObjGroup
		Рез = ObjGroup_LoadFromDataSet(BaseObj, DS)
	ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_CARD Тогда
		// Тип ObjCard
		Рез = ObjCard_LoadFromDataSet(BaseObj, DS)
	ИначеЕсли BaseObj.ClassID = Проч.ClassIDs.CLN_USER Тогда
		// Тип ObjUser
		Рез = ObjUser_LoadFromDataSet(BaseObj, DS)
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции


////////////////////////////////////////////////////////////////////////////////
// Реализация класса НаборОбъектов (ObjSet) - потомок от класса BaseObj
// (коллекция объектов - поле ClassID означает идентификатор класса тех объектов, 
// которые должны храниться в коллекции)

// Создание полей объекта в виде структуры.
//
// Параметры:
//  Нет.
// Возврат:
//  Возвращает созданную структуру
//
Функция ObjSet_СоздатьОбъект() Экспорт
	
	ObjSet = BaseObj_СоздатьОбъект();
	// Общие параметры набора
	ObjSet.Вставить("ItemsList", Новый Массив);   // Массив для хранения набора
	
	Возврат ObjSet;
	
КонецФункции

// Имитирует конструктор объекта.
//
// Параметры:
//  ClassID - тип объектов, которые будут храниться в коллекции.
// Возврат:
//  (Структура) - Структура с полями объекта
//
Функция ObjSet_Конструктор(ClassID=0) Экспорт
	
	ObjSet = ObjSet_СоздатьОбъект();
	ObjSet.ClassID = ClassID;
	// Инициализируем поля класса
  	ObjSet_SetDefAttr(ObjSet);
	
	Возврат ObjSet;
	
КонецФункции

// Имитирует деструктор объекта - освобождает память.
//
// Параметры:
//  ObjSet - ссылка на объект
//
Процедура ObjSet_Деструктор(ObjSet) Экспорт
	
	ObjSet_Clear(ObjSet)

КонецПроцедуры

// Инициализирует атрибуты объекта значениями по умолчанию.
//
// Параметры:
//  ObjSet - ссылка на объект
//
Процедура ObjSet_SetDefAttr(ObjSet) Экспорт
	
	ObjSet.ErrMsg = ""
	
КонецПроцедуры

// Проверяет элемент Obj на возможность добавить его в набор ObjSet.
//
// Параметры:
//  ObjSet - ссылка на набор объектов
//  Obj - ссылка на объект, который проверяем на возможность добавления
// Возврат:
//  Истина, если элемент Obj можно добавить в набор ObjSet и Ложь - в противном случае
//
Функция ObjSet_TestItemForAdd(ObjSet, Obj) Экспорт
	
	Рез = Ложь;		// Результат еще неизвестен	
	ObjSet.ErrMsg = "";
	
	Если Obj <> Неопределено Тогда
		Если ObjSet.ClassID = Obj.ClassID Тогда
			// Проверяем на отсутствие в наборе объектов с теми же ключевыми полями
			ФлагСовпаденияПоКлючу = Ложь;
			Для Каждого СущнНабора Из ObjSet.ItemsList Цикл
				Если BaseObj_IsEqualKeys(СущнНабора, Obj) Тогда
					ФлагСовпаденияПоКлючу = Истина;
					Прервать
				КонецЕсли
			КонецЦикла;
			Если НЕ ФлагСовпаденияПоКлючу Тогда
				Рез = Истина
			Иначе
				ObjSet.ErrMsg = "Такой элемент в наборе уже есть"
			КонецЕсли
		Иначе
			ObjSet.ErrMsg = "Тип добавляемого объекта не соответствует набору"
		КонецЕсли
	Иначе
		ObjSet.ErrMsg = "Добавляемый объект не определен"
	КонецЕсли;
	
	Возврат Рез
			
КонецФункции

// Возвращает кол-во элементов в наборе.
//
// Параметры:
//  ObjSet - ссылка на объект
// Возврат:
//  Количество элементов в наборе
//
Функция ObjSet_GetCount(ObjSet) Экспорт
	
	Возврат ObjSet.ItemsList.Количество()

КонецФункции

// Возвращает элемент из набора под номером Index (нумерация с 0).
//
// Параметры:
//  ObjSet - ссылка на объект
//  Index - номер возвращаемого элемента
// Возврат:
//  Ссылка на элемент набора (с номером Index) или Неопределено, если
//  элемент не найден
//
Функция ObjSet_GetItemByIndex(ObjSet, Index) Экспорт

	Рез = Неопределено;
	ObjSet.ErrMsg = "";
	
	Если (Index >= 0) И (Index < ObjSet.ItemsList.Количество()) Тогда
		Рез = ObjSet.ItemsList[Index]
	Иначе
		ObjSet.ErrMsg = "Элемент не найден";
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Устанавливает значения элемента набора под номером Index (нумерация с 0),
// копируя их из переданного объекта.
//
// Параметры:
//  ObjSet - ссылка на набор объектов
//  Index - номер редактируемого элемента
//  Obj - ссылка на объект из которого копируются данные
// Возврат:
//  Истина, если копирование данных выполнено успешно и Ложь, если
//  операцию выполнить не удалось
//
Функция ObjSet_SetItemToIndex(ObjSet, Index, Obj) Экспорт

	Рез = Ложь;
	ObjSet.ErrMsg = "";
	
	Если Obj <> Неопределено Тогда
		Если ObjSet.ClassID = Obj.ClassID Тогда
			Если (Index >= 0) И (Index < ObjSet.ItemsList.Количество()) Тогда
				// Проверить, возможно новый объект создаст дублирование по ключевым
				// полям
				Если BaseObj_IsKeyEmpty(Obj) Тогда
					// Это новый объект (без ключевых полей) - его можно записать в набор
					Рез = Истина
				Иначе
					Если BaseObj_IsEqualKeys(ObjSet.ItemsList[Index], Obj) Тогда
						// Теже ключевые поля, что и у исходного объекта - можно записывать
						Рез = Истина
					Иначе
						// Проверяем остальные объекты
						ФлагСовпаденияПоКлючу = Ложь;
						Для Каждого СущнНабора Из ObjSet.ItemsList Цикл
							Если BaseObj_IsEqualKeys(СущнНабора, Obj) Тогда
								ФлагСовпаденияПоКлючу = Истина;
								Прервать
							КонецЕсли
						КонецЦикла;
						Если НЕ ФлагСовпаденияПоКлючу Тогда
							Рез = Истина
						Иначе
							ObjSet.ErrMsg = "Набор уже содержит объект с данным идентификатором"
						КонецЕсли
					КонецЕсли
				КонецЕсли;		
						
				// Разобраться с результатами проверок
				Если Рез Тогда
					// Все проверки пройдены - выполнить установку значений
					BaseObj_Assign(ObjSet.ItemsList[Index], Obj)
				КонецЕсли
			Иначе
				ObjSet.ErrMsg = "Элемент с указанным индексом отсутствует в наборе"
			КонецЕсли
		Иначе
			ObjSet.ErrMsg = "Тип объекта копирования не соответствует набору"
		КонецЕсли
	Иначе
		ObjSet.ErrMsg = "Объект копирования не определен"
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Добавляет новый элемент Obj в набор.
//
// Параметры:
//  ObjSet - ссылка на объект набора
//  Obj - ссылка на добавляемый объект
// Возврат:
//  Истина, если добавление прошло успешно и Ложь - в противном случае
//
Функция ObjSet_Add(ObjSet, Obj) Экспорт
	
	Рез = Ложь;
	ObjSet.ErrMsg = "";
	
	Если Obj <> Неопределено Тогда
		// Проверяем корректность добавляемого элемента
		Если ObjSet_TestItemForAdd(ObjSet, Obj) Тогда
			// Элемент корректен
			ObjSet.ItemsList.Добавить(Obj);
			ObjSet.ChangeInfo = Истина;
			Рез = Истина  // Добавление прошло успешно
		КонецЕсли
	Иначе
		ObjSet.ErrMsg = "Объект добавления не определен"
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Удаляет элемент с указанным индексом из набора.
//
// Параметры:
//  ObjSet - ссылка на объект
//  Index - индекс удаляемого элемента (нумерация с 0)
// Возврат:
//  Истина, если элемент найден и удален и Ложь - в противном случае
//
Функция ObjSet_Delete(ObjSet, Index) Экспорт
	
	Рез = Ложь;
	ObjSet.ErrMsg = "";
	
	Если (Index >= 0) И (Index < ObjSet.ItemsList.Количество()) Тогда
		BaseObj_Деструктор(ObjSet.ItemsList[Index]);
		ObjSet.ItemsList.Удалить(Index);
		Рез = Истина
	Иначе
		ObjSet.ErrMsg = "Элемент не найден";
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Удаляет все элементы из набора.
//
// Параметры:
//  ObjSet - ссылка на объект
// Возврат:
//
Процедура ObjSet_Clear(ObjSet) Экспорт
	
	Если ObjSet.ItemsList.Количество() > 0 Тогда
		Для Каждого Item Из ObjSet.ItemsList Цикл
			BaseObj_Деструктор(Item);
		КонецЦикла;
		ObjSet.ItemsList.Очистить();
		ObjSet.ChangeInfo = Истина
	КонецЕсли
	
КонецПроцедуры

// Возвращает признак наличия несохраненных изменений
// в наборе или во вложенных элементах
//
// Параметры:
//  ObjSet - ссылка на объект
// Возврат:
//  Значение признака несохраненных изменений
//
Функция ObjSet_GetChangeInfo(ObjSet) Экспорт

	Если НЕ ObjSet.ChangeInfo Тогда
		// Возможно, несохраненные изменения есть
		Для Каждого Item Из ObjSet.ItemsList Цикл
			Если BaseObj_GetChangeInfo(Item) Тогда
				ObjSet.ChangeInfo = Истина;
				Прервать
			КонецЕсли
		КонецЦикла
	КонецЕсли;

	Возврат	ObjSet.ChangeInfo
	
КонецФункции

// Устанавливает признак наличия несохраненных изменений
// в наборе и вложенных элементах в значение ChangeInfo
//
// Параметры:
//  ObjSet - ссылка на объект
//  ChangeInfo - значение признака несохраненных изменений
//
Процедура ObjSet_SetChangeInfo(ObjSet, ChangeInfo) Экспорт

	ObjSet.ChangeInfo = ChangeInfo;
	Если Не ChangeInfo Тогда
		Для Каждого Item Из ObjSet.ItemsList Цикл
			BaseObj_SetChangeInfo(Item, ChangeInfo)
		КонецЦикла
	КонецЕсли
	
КонецПроцедуры

// Возвращает текущее сообщение о последней возникшей ошибке
// в наборе или во вложенных элементах
//
// Параметры:
//  ObjSet - ссылка на объект
// Возврат:
//  Сообщение о последней возникшей ошибке
//
Функция ObjSet_GetErrorMsg(ObjSet) Экспорт
	
	Если ObjSet.ErrMsg = "" Тогда
		// Возможно, сообщения об ошибках есть
		Для Каждого Item Из ObjSet.ItemsList Цикл
			Если Item.ErrMsg <> "" Тогда
				ObjSet.ErrMsg = Item.ErrMsg;
				Прервать
			КонецЕсли
		КонецЦикла
	КонецЕсли;

	Возврат	ObjSet.ErrMsg
	
КонецФункции

// Проверяет корректность самого набора и его элементов.
//
// Параметры:
//  ObjSet - ссылка на объект
// Возврат:
//  Истина, если набор не содержит ошибок и Ложь - в противном случае
//
Функция ObjSet_TestSet(ObjSet) Экспорт
	
	Рез = Ложь;		// Результат еще неизвестен	
	ObjSet.ErrMsg = "";
	
	// Проверить тип элементов набора
	Если (ObjSet.ClassID = Проч.ClassIDs.CLN_GROUP) ИЛИ
		 (ObjSet.ClassID = Проч.ClassIDs.CLN_CARD) ИЛИ
		 (ObjSet.ClassID = Проч.ClassIDs.CLN_USER) Тогда
		// Проверить элементы набора
		Для Инд1=0 По ObjSet.ItemsList.Количество()-1 Цикл
			Сущн1 = ObjSet.ItemsList[Инд1];
			Если ObjSet.ClassID = Сущн1.ClassID Тогда
				ФлагСовпаденияПоКлючу = Ложь;
				Для Инд2=Инд1+1 По ObjSet.ItemsList.Количество()-1 Цикл
					Сущн2 = ObjSet.ItemsList[Инд2];
					Если BaseObj_IsEqualKeys(Сущн1, Сущн2) Тогда
						ФлагСовпаденияПоКлючу = Истина;
						Прервать
					КонецЕсли
				КонецЦикла;
				Если ФлагСовпаденияПоКлючу Тогда
					ObjSet.ErrMsg = "У нескольких объектов совпадает идентификатор";
					Прервать
				КонецЕсли
			Иначе
				// Тип элемента не совпадает с типом набора
				ObjSet.ErrMsg = "Тип некоторых элементов не совпадает с типом элементов набора"
			КонецЕсли
		КонецЦикла
	Иначе
		// Некорректный тип элементов набора
		ObjSet.ErrMsg = "Некорректный тип элементов набора"
	КонецЕсли;
	// Определиться с результатом проверки
	Если ObjSet.ErrMsg = "" Тогда
		Рез = Истина
	КонецЕсли;
		
	Возврат Рез	
			
КонецФункции

// Загружает объекты из набора данных DS. Текущий набор объектов удаляется.
// Зугрузка ведется от текущего положения курсора в наборе данных
// (должен быть выставлен в нужную позицию предварительно) и в указанном
// количестве строк.
// Параметры:
//  ObjSet - ссылка на объект
//  DS - набор данных типа ADORecordSet
//  RowCount - количество строк, которое должно быть загружено из набора данных
//             (если RowCount=-1, то нужно загружать все строки до самого конца
//             набора)
// Возврат:
//  Количество успешно загруженных объектов из набора
//
Функция ObjSet_LoadFromDataSet(ObjSet, DS, RowCount=-1) Экспорт

	Рез = 0;
	ObjSet.ErrMsg = "";
	
	ObjSet_Clear(ObjSet);
	Если ObjSet.ErrMsg = "" Тогда
		Рез = ObjSet_AddFromDataSet(ObjSet, DS, RowCount)
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Загружает объекты из набора данных DS, добавляя их к уже существующему набору объектов.
// Зугрузка ведется от текущего положения курсора в наборе данных
// (должен быть выставлен в нужную позицию предварительно) и в указанном
// количестве строк.
// Параметры:
//  ObjSet - ссылка на объект
//  DS - набор данных типа ADORecordSet
//  RowCount - количество строк, которое должно быть загружено из набора данных
//             (если RowCount=-1, то нужно загружать все строки до самого конца
//             набора)
// Возврат:
//  Количество успешно загруженных объектов из набора
//
Функция ObjSet_AddFromDataSet(ObjSet, DS, RowCount=-1) Экспорт

	Рез = 0;
	ObjSet.ErrMsg = "";
	
	Если RowCount <> 0 Тогда
		Пока (DS.EOF = 0) И ((RowCount < 0) ИЛИ (Рез < RowCount)) Цикл
			// Создать объект
			Obj = BaseObj_Конструктор(ObjSet.ClassID);
			Если Obj <> Неопределено Тогда
				// Пробуем выполнить загрузку значений полей
				Если BaseObj_LoadFromDataSet(Obj, DS) Тогда
					// Выполнить чтение полей удалось - добавить объект в набор
					Если ObjSet_Add(ObjSet, Obj) Тогда
						// Удалось добавить объект в набор
						Рез = Рез + 1
					Иначе
						// Добавить объект не удалось
						Прервать
					КонецЕсли
				Иначе
					// Считать поля не удалось - удалить объект
					ObjSet.ErrMsg = Obj.ErrMsg;
					BaseObj_Деструктор(Obj);
					Прервать
				КонецЕсли
			Иначе
				// Не удалось создать объект
				ObjSet.ErrMsg = "Не удалось создать новый объект для сущности"
			КонецЕсли;
			// Перейти к следующей записи
			DS.MoveNext();
		КонецЦикла
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции


////////////////////////////////////////////////////////////////////////////////
// Реализация класса СущностьГруппа (ObjGroup)

// Создание полей объекта в виде структуры.
//
// Параметры:
//  Нет.
// Возврат:
//  Возвращает созданную структуру
//
Функция ObjGroup_СоздатьОбъект() Экспорт
	
	ObjGroup = BaseObj_СоздатьОбъект();
	ObjGroup.ClassID = Проч.ClassIDs.CLN_GROUP;  // Тип ObjGroup
	// Общие параметры сущности
	ObjGroup.Вставить("GRID", Неопределено);   // Идентификатор группы (INT)
	ObjGroup.Вставить("PGRID", Неопределено);  // Идентификатор родительской группы (INT)
	ObjGroup.Вставить("GRName", Неопределено); // Название группы (STR)
	
	Возврат ObjGroup;
	
КонецФункции

// Имитирует конструктор объекта.
//
// Параметры:
//  Нет.
// Возврат:
//  (Структура) - Структура с полями объекта
//
Функция ObjGroup_Конструктор() Экспорт
	
	ObjGroup = ObjGroup_СоздатьОбъект();
	// Инициализируем поля объекта
  	ObjGroup_SetDefAttr(ObjGroup);
	
	Возврат ObjGroup;
	
КонецФункции

// Имитирует деструктор объекта - освобождает память.
//
// Параметры:
//  ObjGroup - ссылка на объект
//
Процедура ObjGroup_Деструктор(ObjGroup) Экспорт
	
	
КонецПроцедуры

// Инициализирует атрибуты объекта значениями по умолчанию.
//
// Параметры:
//  ObjGroup - ссылка на объект
//
Процедура ObjGroup_SetDefAttr(ObjGroup) Экспорт
	
	ObjGroup.GRID = 0;
	ObjGroup.PGRID = 0;
	ObjGroup.GRName = "";

КонецПроцедуры

// Копирует значения полей объекта ObjGroup2 в текущий ObjGroup1.
// Все предыдущие значения из ObjGroup1 будут утеряны.
//
// Параметры:
//  ObjGroup1 - ссылка на объект
//  ObjGroup2 - ссылка на другой копируемый объект
//
Процедура ObjGroup_Assign(ObjGroup1, ObjGroup2) Экспорт
	
	Если (ObjGroup1 <> Неопределено) И 
		 (ObjGroup2 <> Неопределено) И 
		 (ObjGroup_IsEqual(ObjGroup1, ObjGroup2) = Ложь) Тогда
		ObjGroup1.GRID   = ObjGroup2.GRID;
		ObjGroup1.PGRID  = ObjGroup2.PGRID;
		ObjGroup1.GRName = ObjGroup2.GRName;

		ObjGroup1.ChangeInfo = Истина
	КонецЕсли

КонецПроцедуры

// Возвращает Истина, если объект ObjGroup2 содержит теже данные, что и
// текущий ObjGroup1 и Ложь - в противном случае
//
// Параметры:
//  ObjGroup1 - ссылка на объект
//  ObjGroup2 - ссылка на другой объект для сравнения
// Возврат:
//  Истина, если объекты идентичны и Ложь - в противном случае
//
Функция ObjGroup_IsEqual(ObjGroup1, ObjGroup2) Экспорт
	
	IsEqual = Ложь;
	Если (ObjGroup1 <> Неопределено) И 
		 (ObjGroup2 <> Неопределено) Тогда
		Если (ObjGroup2.GRID   = ObjGroup1.GRID) И
			 (ObjGroup2.PGRID  = ObjGroup1.PGRID) И
			 (ObjGroup2.GRName = ObjGroup1.GRName) Тогда
			IsEqual = Истина  // Все сравнения завершены успешно
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEqual
	
КонецФункции

// Сравнивает ключевые поля двух объектов между собой.
//
// Параметры:
//  ObjGroup1 - ссылка на объект
//  ObjGroup2 - ссылка на другой сравниваемый объект
// Возврат
//  Истина, если объект ObjGroup2 содержит теже значения ключевых полей, что и
//  текущий ObjGroup1 и Ложь - в противном случае
//
Функция ObjGroup_IsEqualKeys(ObjGroup1, ObjGroup2) Экспорт

	IsEqual = Ложь;
	Если (ObjGroup1 <> Неопределено) И 
		 (ObjGroup2 <> Неопределено) Тогда
		Если ((ObjGroup2.GRID = ObjGroup1.GRID) И (ObjGroup1.GRID > 0)) Тогда
			IsEqual = Истина  // Все сравнения завершены успешно
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEqual
	
КонецФункции

// Проверяет, являются ли значения ключеных полей объекта пустыми (незначащими),
// т.е. по сути это новый объект, ни разу не сохраненный в БД.
//
// Параметры:
//  ObjGroup - ссылка на объект
// Возврат
//  Истина, если объект ObjGroup содержит пустые ключевые поля и Ложь - в противном случае
//
Функция ObjGroup_IsKeyEmpty(ObjGroup) Экспорт

	Рез = Ложь;
	
	IsEmpty = Ложь;
	Если ObjGroup <> Неопределено Тогда
		Если ObjGroup.GRID <= 0 Тогда
			IsEmpty = Истина
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEmpty
	
КонецФункции

// Инициализирует атрибуты объекта значениями переданными в качестве параметров.
//
// Параметры:
//  ObjGroup - ссылка на объект
//	GRID - идентификатор группы
//	PGRID - идентификатор родительской группы
//	GRName - название группы
//
Процедура ObjGroup_SetAttr(ObjGroup, GRID, PGRID, GRName) Экспорт
	
	// Заносим данные во временный объект, чтобы отследить,
	// есть ли отличие от текущих данных 
	TmpObjGroup = ObjGroup_Конструктор();
	
	TmpObjGroup.GRID = GRID;
	TmpObjGroup.PGRID = PGRID;
	TmpObjGroup.GRName = GRName;

	ObjGroup_Assign(ObjGroup, TmpObjGroup);
	ObjGroup_Деструктор(TmpObjGroup);
	
КонецПроцедуры

// Принудительно устанавливает значение признака наличия 
// несохраненных изменений.
//
// Параметры:
//  ObjGroup - ссылка на объект
//  ChangeInfo - значение признака несохраненных изменений
//
Процедура ObjGroup_SetChangeInfo(ObjGroup, ChangeInfo) Экспорт
	
	ObjGroup.ChangeInfo = ChangeInfo
	
КонецПроцедуры

// Загружает поля объекта из текущей строки набора данных DS.
//
// Параметры:
//  ObjGroup - ссылка на объект
//  DS - набор данных типа ADORecordSet
// Возврат:
//  Истина, если загрузка выполнена успешно и Ложь, если возникли ошибки
//
Функция ObjGroup_LoadFromDataSet(ObjGroup, DS) Экспорт

	Рез = Ложь;  // Параметры еще не прочитаны
	ObjGroup.ErrMsg = "";
	
	Попытка
		ObjGroup.GRID   = DS.Fields("GRID").Value;
		ObjGroup.PGRID  = DS.Fields("PGRID").Value;
		ObjGroup.GRName = DS.Fields("GRNAME").Value;
		
		ObjGroup_SetChangeInfo(ObjGroup, Ложь); // Сбрасываем признак изменений, т.к.
		                                        // эти данные сохранены в базе
		Рез = Истина;  // Поля считаны успешно
	Исключение
		// Ошибка при чтении набора данных
		ObjGroup.ErrMsg = ИнформацияОбОшибке().Описание;
	КонецПопытки;
	
	Возврат Рез
	
КонецФункции


////////////////////////////////////////////////////////////////////////////////
// Реализация класса СущностьКарточка (ObjCard)

// Создание полей объекта в виде структуры.
//
// Параметры:
//  Нет.
// Возврат:
//  Возвращает созданную структуру
//
Функция ObjCard_СоздатьОбъект() Экспорт
	
	ObjCard = BaseObj_СоздатьОбъект();
	ObjCard.ClassID = Проч.ClassIDs.CLN_CARD;  // Тип ObjCard
	// Общие параметры сущности
	ObjCard.Вставить("CDID", Неопределено);   // Идентификатор элемента (INT)
	ObjCard.Вставить("GRID", Неопределено);   // Идентификатор группы (INT)
	ObjCard.Вставить("CDName", Неопределено); // Название элемента (STR)
	ObjCard.Вставить("CDDescript", Неопределено); // Описание (STR)
	ObjCard.Вставить("CDContacts", Неопределено); // Контактная информация (STR)
	ObjCard.Вставить("CDNote", Неопределено); // Примечание (STR)
	ObjCard.Вставить("CDImage", Неопределено);// Изображение (Картинка)
	// Специальные поля, отмечающие факт наличия несохраненных изменения в BLOB-полях
	ObjCard.Вставить("ChangeCDContacts", Неопределено);// Признак изменений в поле CDContacts
	ObjCard.Вставить("ChangeCDNote", Неопределено);    // Признак изменений в поле CDNote
	ObjCard.Вставить("ChangeCDImage", Неопределено);   // Признак изменений в поле CDImage
	
	Возврат ObjCard;
	
КонецФункции

// Имитирует конструктор объекта.
//
// Параметры:
//  Нет.
// Возврат:
//  (Структура) - Структура с полями объекта
//
Функция ObjCard_Конструктор() Экспорт
	
	ObjCard = ObjCard_СоздатьОбъект();
	// Инициализируем поля объекта
  	ObjCard_SetDefAttr(ObjCard);
	
	Возврат ObjCard;
	
КонецФункции

// Имитирует деструктор объекта - освобождает память.
//
// Параметры:
//  ObjCard - ссылка на объект
//
Процедура ObjCard_Деструктор(ObjCard) Экспорт
	
	
КонецПроцедуры

// Инициализирует атрибуты объекта значениями по умолчанию.
//
// Параметры:
//  ObjCard - ссылка на объект
//
Процедура ObjCard_SetDefAttr(ObjCard) Экспорт
	
	ObjCard.CDID = 0;
	ObjCard.GRID = 0;
	ObjCard.CDName = "";
	ObjCard.CDDescript = Неопределено;
	ObjCard.CDContacts = Неопределено;
	ObjCard.CDNote = Неопределено;
	ObjCard.CDImage = Неопределено;
	ObjCard.ChangeCDContacts = Ложь;
	ObjCard.ChangeCDNote = Ложь;
	ObjCard.ChangeCDImage = Ложь;

КонецПроцедуры

// Копирует значения полей объекта ObjCard2 в текущий ObjCard1.
// Все предыдущие значения из ObjCard1 будут утеряны.
//
// Параметры:
//  ObjCard1 - ссылка на объект
//  ObjCard2 - ссылка на другой копируемый объект
//
Процедура ObjCard_Assign(ObjCard1, ObjCard2) Экспорт
	
	Если (ObjCard1 <> Неопределено) И 
		 (ObjCard2 <> Неопределено) И 
		 (ObjCard_IsEqual(ObjCard1, ObjCard2) = Ложь) Тогда
		ObjCard1.CDID       = ObjCard2.CDID;
		ObjCard1.GRID       = ObjCard2.GRID;
		ObjCard1.CDName     = ObjCard2.CDName;
		ObjCard1.CDDescript = ObjCard2.CDDescript;
		Если ObjCard1.CDContacts <> ObjCard2.CDContacts Тогда
			ObjCard1.CDContacts = ObjCard2.CDContacts;
			ObjCard1.ChangeCDContacts = Истина
		КонецЕсли;
		Если ObjCard1.CDNote <> ObjCard2.CDNote Тогда
			ObjCard1.CDNote = ObjCard2.CDNote;
			ObjCard1.ChangeCDNote = Истина
		КонецЕсли;
		Если НЕ КартинкиРавны(ObjCard1.CDImage, ObjCard2.CDImage) Тогда	
			ObjCard1.CDImage= ObjCard2.CDImage;
			ObjCard1.ChangeCDImage = Истина
		КонецЕсли;

		ObjCard1.ChangeInfo = Истина
	КонецЕсли

КонецПроцедуры

// Возвращает Истина, если объект ObjCard2 содержит теже данные, что и
// текущий ObjCard1 и Ложь - в противном случае
//
// Параметры:
//  ObjCard1 - ссылка на объект
//  ObjCard2 - ссылка на другой объект для сравнения
// Возврат:
//  Истина, если объекты идентичны и Ложь - в противном случае
//
Функция ObjCard_IsEqual(ObjCard1, ObjCard2) Экспорт
	
	IsEqual = Ложь;
	Если (ObjCard1 <> Неопределено) И 
		 (ObjCard2 <> Неопределено) Тогда
		Если (ObjCard2.CDID       = ObjCard1.CDID) И
			 (ObjCard2.GRID       = ObjCard1.GRID) И
			 (ObjCard2.CDName     = ObjCard1.CDName) И
			 (ObjCard2.CDDescript = ObjCard1.CDDescript) И
			 (ObjCard2.CDContacts = ObjCard1.CDContacts) И
			 (ObjCard2.CDNote     = ObjCard1.CDNote) И
			 КартинкиРавны(ObjCard2.CDImage, ObjCard1.CDImage) Тогда
			IsEqual = Истина  // Все сравнения завершены успешно
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEqual
	
КонецФункции

// Сравнивает ключевые поля двух объектов между собой.
//
// Параметры:
//  ObjCard1 - ссылка на объект
//  ObjCard2 - ссылка на другой сравниваемый объект
// Возврат
//  Истина, если объект ObjCard2 содержит теже значения ключевых полей, что и
//  текущий ObjCard1 и Ложь - в противном случае
//
Функция ObjCard_IsEqualKeys(ObjCard1, ObjCard2) Экспорт

	IsEqual = Ложь;
	Если (ObjCard1 <> Неопределено) И 
		 (ObjCard2 <> Неопределено) Тогда
		Если ((ObjCard2.CDID = ObjCard1.CDID) И (ObjCard1.CDID > 0)) Тогда
			IsEqual = Истина  // Все сравнения завершены успешно
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEqual
	
КонецФункции

// Проверяет, являются ли значения ключеных полей объекта пустыми (незначащими),
// т.е. по сути это новый объект, ни разу не сохраненный в БД.
//
// Параметры:
//  ObjCard - ссылка на объект
// Возврат
//  Истина, если объект ObjCard содержит пустые ключевые поля и Ложь - в противном случае
//
Функция ObjCard_IsKeyEmpty(ObjCard) Экспорт

	Рез = Ложь;
	
	IsEmpty = Ложь;
	Если ObjCard <> Неопределено Тогда
		Если ObjCard.CDID <= 0 Тогда
			IsEmpty = Истина
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEmpty
	
КонецФункции

// Инициализирует атрибуты объекта значениями переданными в качестве параметров.
//
// Параметры:
//  ObjCard - ссылка на объект
//	CDID - идентификатор карточки
//	GRID - идентификатор группы
//	GRName - название карточки
//  CDDescript - описание
//  CDContacts - контактная информация
//  CDNote     - примечание
//  CDImage    - изображение
//
Процедура ObjCard_SetAttr(ObjCard, CDID, GRID, CDName, 
						  CDDescript = Неопределено, 
						  CDContacts = Неопределено, 
						  CDNote = Неопределено,
						  CDImage = Неопределено) Экспорт
	
  	// Заносим данные во временный объект, чтобы отследить,
	// есть ли отличие от текущих данных 
	TmpObjCard = ObjCard_Конструктор();
	
	TmpObjCard.CDID       = CDID;
	TmpObjCard.GRID       = GRID;
	TmpObjCard.CDName     = CDName;
	Если CDDescript <> Неопределено Тогда
		TmpObjCard.CDDescript = CDDescript;
	КонецЕсли;
	Если CDContacts <> Неопределено Тогда
		TmpObjCard.CDContacts = CDContacts;
	КонецЕсли;
	Если CDNote <> Неопределено Тогда
		TmpObjCard.CDNote = CDNote
	КонецЕсли;
	Если CDImage <> Неопределено Тогда
		TmpObjCard.CDImage = CDImage;
	КонецЕсли;

	ObjCard_Assign(ObjCard, TmpObjCard);
	ObjCard_Деструктор(TmpObjCard);

КонецПроцедуры

// Принудительно устанавливает значение признака наличия 
// несохраненных изменений.
//
// Параметры:
//  ObjCard - ссылка на объект
//  ChangeInfo - значение признака несохраненных изменений
//
Процедура ObjCard_SetChangeInfo(ObjCard, ChangeInfo) Экспорт
	
	ObjCard.ChangeInfo = ChangeInfo;
	Если ChangeInfo = Ложь Тогда
		ObjCard.ChangeCDContacts = Ложь;
		ObjCard.ChangeCDNote = Ложь;
		ObjCard.ChangeCDImage = Ложь
	КонецЕсли

КонецПроцедуры

// Загружает поля объекта из текущей строки набора данных DS.
//
// Параметры:
//  ObjCard - ссылка на объект
//  DS - набор данных типа ADORecordSet
// Возврат:
//  Истина, если загрузка выполнена успешно и Ложь, если возникли ошибки
//
Функция ObjCard_LoadFromDataSet(ObjCard, DS) Экспорт

	Рез = Ложь;  // Параметры еще не прочитаны
	ObjCard.ErrMsg = "";
	
	Попытка
		Попытка
			ObjCard.CDID       = DS.Fields("CDID").Value;
		Исключение
		КонецПопытки;
		Попытка
			ObjCard.GRID       = DS.Fields("GRID").Value;
		Исключение
		КонецПопытки;
		Попытка
			ObjCard.CDName     = DS.Fields("CDNAME").Value;
		Исключение
		КонецПопытки;
		Попытка
			ObjCard.CDDescript = DS.Fields("CDDESCRIPT").Value;
		Исключение
		КонецПопытки;
		Попытка
			ObjCard.CDContacts = DS.Fields("CDCONTACTS").Value;
		Исключение
		КонецПопытки;
		Попытка
			CDNoteArray		   = DS.Fields("CDNOTE").Value;
			ObjCard.CDNote     = COMSafeArrayWIN1251_ВСтрокуUTF8(CDNoteArray);
		Исключение
		КонецПопытки;
		Попытка
			CDImageArray	   = DS.Fields("CDIMAGE").Value;
			ObjCard.CDImage    = COMSafeArray_ВКартинку(CDImageArray)
		Исключение
		КонецПопытки;
			
		ObjCard_SetChangeInfo(ObjCard, Ложь); // Сбрасываем признак изменений, т.к.
		                                      // эти данные сохранены в базе
		Рез = Истина;  // Поля считаны успешно
	Исключение
		// Ошибка при чтении набора данных
		ObjCard.ErrMsg = ИнформацияОбОшибке().Описание;
	КонецПопытки;

	Возврат Рез
	
КонецФункции


////////////////////////////////////////////////////////////////////////////////
// Реализация класса СущностьПользователь (ObjUser)

// Создание полей объекта в виде структуры.
//
// Параметры:
//  Нет.
// Возврат:
//  Возвращает созданную структуру
//
Функция ObjUser_СоздатьОбъект() Экспорт
	
	ObjUser = BaseObj_СоздатьОбъект();
	ObjUser.ClassID = Проч.ClassIDs.CLN_USER;  // Тип ObjUser
	// Общие параметры сущности
	ObjUser.Вставить("UID", Неопределено);     // Идентификатор пользователя (INT)
	ObjUser.Вставить("UName", Неопределено);   // Логин (STR)
	ObjUser.Вставить("UPwd", Неопределено);    // Пароль (STR)
	ObjUser.Вставить("URights", Неопределено); // Права пользователя (STR)
	
	Возврат ObjUser;
	
КонецФункции

// Имитирует конструктор объекта.
//
// Параметры:
//  Нет.
// Возврат:
//  (Структура) - Структура с полями объекта
//
Функция ObjUser_Конструктор() Экспорт
	
	ObjUser = ObjUser_СоздатьОбъект();
	// Инициализируем поля объекта
  	ObjUser_SetDefAttr(ObjUser);
	
	Возврат ObjUser;
	
КонецФункции

// Имитирует деструктор объекта - освобождает память.
//
// Параметры:
//  ObjUser - ссылка на объект
//
Процедура ObjUser_Деструктор(ObjUser) Экспорт
	
	
КонецПроцедуры

// Инициализирует атрибуты объекта значениями по умолчанию.
//
// Параметры:
//  ObjUser - ссылка на объект
//
Процедура ObjUser_SetDefAttr(ObjUser) Экспорт
	
	ObjUser.UID = 0;
	ObjUser.UName = "";
	ObjUser.UPwd = Неопределено;
	ObjUser.URights = "";

КонецПроцедуры

// Копирует значения полей объекта ObjUser2 в текущий ObjUser1.
// Все предыдущие значения из ObjUser1 будут утеряны.
//
// Параметры:
//  ObjUser1 - ссылка на объект
//  ObjUser2 - ссылка на другой копируемый объект
//
Процедура ObjUser_Assign(ObjUser1, ObjUser2) Экспорт
	
	Если (ObjUser1 <> Неопределено) И 
		 (ObjUser2 <> Неопределено) И 
		 (ObjUser_IsEqual(ObjUser1, ObjUser2) = Ложь) Тогда
		ObjUser1.UID     = ObjUser2.UID;
		ObjUser1.UName   = ObjUser2.UName;
		ObjUser1.UPwd    = ObjUser2.UPwd;
		ObjUser1.URights = ObjUser2.URights;

		ObjUser1.ChangeInfo = Истина
	КонецЕсли

КонецПроцедуры

// Возвращает Истина, если объект ObjUser2 содержит теже данные, что и
// текущий ObjUser1 и Ложь - в противном случае
//
// Параметры:
//  ObjUser1 - ссылка на объект
//  ObjUser2 - ссылка на другой объект для сравнения
// Возврат:
//  Истина, если объекты идентичны и Ложь - в противном случае
//
Функция ObjUser_IsEqual(ObjUser1, ObjUser2) Экспорт
	
	IsEqual = Ложь;
	Если (ObjUser1 <> Неопределено) И 
		 (ObjUser2 <> Неопределено) Тогда
		Если (ObjUser2.UID     = ObjUser1.UID) И
			 (ObjUser2.UName   = ObjUser1.UName) И
			 (ObjUser2.UPwd    = ObjUser1.UPwd) И
			 IsEqualUserRights(ObjUser2.URights, ObjUser1.URights) Тогда
			IsEqual = Истина  // Все сравнения завершены успешно
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEqual
	
КонецФункции

// Сравнивает ключевые поля двух объектов между собой.
//
// Параметры:
//  ObjUser1 - ссылка на объект
//  ObjUser2 - ссылка на другой сравниваемый объект
// Возврат
//  Истина, если объект ObjUser2 содержит теже значения ключевых полей, что и
//  текущий ObjUser1 и Ложь - в противном случае
//
Функция ObjUser_IsEqualKeys(ObjUser1, ObjUser2) Экспорт

	IsEqual = Ложь;
	Если (ObjUser1 <> Неопределено) И 
		 (ObjUser2 <> Неопределено) Тогда
		Если ((ObjUser2.UID = ObjUser1.UID) И (ObjUser1.UID > 0)) ИЛИ
			 (ВРег(ObjUser2.UName) = ВРег(ObjUser1.UName)) Тогда
			IsEqual = Истина  // Все сравнения завершены успешно
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEqual
	
КонецФункции

// Проверяет, являются ли значения ключеных полей объекта пустыми (незначащими),
// т.е. по сути это новый объект, ни разу не сохраненный в БД.
//
// Параметры:
//  ObjUser - ссылка на объект
// Возврат
//  Истина, если объект ObjUser содержит пустые ключевые поля и Ложь - в противном случае
//
Функция ObjUser_IsKeyEmpty(ObjUser) Экспорт

	Рез = Ложь;
	
	IsEmpty = Ложь;
	Если ObjUser <> Неопределено Тогда
		Если (ObjUser.UID <= 0) ИЛИ
			 (СокрЛП(ObjUser) = "") Тогда
			IsEmpty = Истина
		КонецЕсли
	КонецЕсли;
	
	Возврат IsEmpty
	
КонецФункции

// Инициализирует атрибуты объекта значениями переданными в качестве параметров.
//
// Параметры:
//  ObjUser - ссылка на объект
//	UID - идентификатор пользователя
//	UName - логин пользователя
//	UPwd - пароль пользователя
//	URights - права пользователя
//
Процедура ObjUser_SetAttr(ObjUser, UID, UName, UPwd, URights) Экспорт
	
	// Заносим данные во временный объект, чтобы отследить,
	// есть ли отличие от текущих данных 
	TmpObjUser = ObjUser_Конструктор();
	
	TmpObjUser.UID = UID;
	TmpObjUser.UName = UName;
	TmpObjUser.UPwd = UPwd;
	TmpObjUser.URights = URights;

	ObjUser_Assign(ObjUser, TmpObjUser);
	ObjUser_Деструктор(TmpObjUser);

КонецПроцедуры

// Принудительно устанавливает значение признака наличия 
// несохраненных изменений.
//
// Параметры:
//  ObjUser - ссылка на объект
//  ChangeInfo - значение признака несохраненных изменений
//
Процедура ObjUser_SetChangeInfo(ObjUser, ChangeInfo) Экспорт
	
	ObjUser.ChangeInfo = ChangeInfo
	
КонецПроцедуры

// Загружает поля объекта из текущей строки набора данных DS.
//
// Параметры:
//  ObjUser - ссылка на объект
//  DS - набор данных типа ADORecordSet
// Возврат:
//  Истина, если загрузка выполнена успешно и Ложь, если возникли ошибки
//
Функция ObjUser_LoadFromDataSet(ObjUser, DS) Экспорт

	Рез = Ложь;  // Параметры еще не прочитаны
	ObjUser.ErrMsg = "";
	
	Попытка
		ObjUser.UID     = DS.Fields("UID").Value;
		ObjUser.UName   = DS.Fields("UNAME").Value;
		//ObjUser.UPwd  = DS.Fields("UPWD").Value;   // Никогда не копируем пароль
		ObjUser.UPwd    = Неопределено;
		ObjUser.URights = DS.Fields("URIGHTS").Value;
			
		ObjUser_SetChangeInfo(ObjUser, Ложь); // Сбрасываем признак изменений, т.к.
		                                      // эти данные сохранены в базе
		Рез = Истина;  // Поля считаны успешно
	Исключение
		// Ошибка при чтении набора данных
		ObjUser.ErrMsg = ИнформацияОбОшибке().Описание;
	КонецПопытки;
	
	Возврат Рез
	
КонецФункции



Работа с базой данных. Технология ADO.


Для доступа к базе мы используем технологию, которая наиболее хорошо описана в Интернете применительно к 1С – «Microsoft ADO». И это конечно разумно, т.к. данная технология самая стабильно доступная на компьютерах с Windows. Это не только 1С касается, а также других средств разработки, например «Visual Studio». Это программисты Delphi привыкли к разнообразию технологий работы с БД. У остальных разработчиков выбор средств более ограничен. Описывать ADO как таковую я не буду, так как в Интернете очень много отличных статей накопилось, за последние 10-15 лет. Мы рассмотрим те интерфейсы и типы данных, которые понадобятся нам для реализации, описанной в этой статье (подробности смотрите в документации MSDN).
Название интерфейса Описание Используемые свойства и методы
Connection Обеспечивает подключение к базе данных. State (свойство) — показывает текущее состояние подключения (установлено или нет);

Open (метод) — позволяет установить подключение к базе;
Close (метод) – выполняет отключение от базы
Command Обеспечивает выполнение команд (нас интересуют только SQL-запросы) в рамках указанного подключения (Connection). ActiveConnection (свойство) – указывает на подключение, с которым связан объект выполнения команд;
CommandType (свойство) – указывает тип команды (нас интересует тип – «SQL-запрос»);
CommandText (свойство) – текст команды (в нашем случае – текст запроса);
Parameters (свойство) – коллекция параметров команды (запроса) для передачи данных в базу;

Execute (метод) – выполняет команду (запрос). Возвращает результирующий набор данных;
CreateParameter (метод) – позволяет создать новый параметр команды (запроса)
Recordset Набор данных (таблица), получаемый в результате выполнения запроса. Обеспечивает передачу данных из базы в клиентское приложение. State (свойство) – показывает состояние набора данных (доступен или нет);
EOF (свойство) – признак конца набора данных (показывает, стоит внутренний указатель на последней строке набора или нет);
Fields (свойство) – коллекция полей – доступ к колонкам текущей строки набора;

MoveNext (метод) – сдвигает внутренний указатель набора на одну строку в направлении конца набора;
Close (метод) – закрывает набор данных
Fields Коллекция полей, обеспечивающая доступ к полям текущей строки набора данных. Fields(FieldInd), Fields(FieldName) – доступ к конкретному полю Field по его индексу (порядковому номеру) или по имени (строковое значение имени поля)
Field Поле, обеспечивающее доступ к колонке текущей строки набора данных. Value (свойство) – значение поля (дает доступ к значению соответствующей колонки текущей строки связанного набора Recordset)
Parameters Коллекция значений, представляющая собой параметры запроса – для передачи данных от клиентского приложения в базу. Count (свойство) – показывает количество параметров в коллекции;

Append (метод) – добавляет параметр в конец коллекции;
Delete (метод) – удаляет указанный параметр из коллекции

Полное описание типов данных ADO, используемых в этой программе, приведено ниже:
Реализация перечислимых типов
// Инициализирует набор констант, связанных с группой COM-объектов ADO
//
// Параметры:
//  НЕТ.
//
// Возврат:
//  Структура с полями - имитациями констант и перечислимых типов ADO
//
Функция ИнициализацияКонстантADO() Экспорт
	
	ADO = Новый Структура;
	
	// Задает режимы доступа к данным для объектов типа Connection, Record и Stream
	ConnectModeEnum = Новый Структура;
	ConnectModeEnum.Вставить("adModeRead", 1);            // Режим доступа "Только для чтения"
	ConnectModeEnum.Вставить("adModeReadWrite", 3);       // Режим доступа "Чтение и запись"
	ConnectModeEnum.Вставить("adModeRecursive", 4194304); // 
	ConnectModeEnum.Вставить("adModeShareDenyNone", 16);  // Режим доступа "Другие также могут открыть для чтения и записи"
    ConnectModeEnum.Вставить("adModeShareDenyRead", 4);   // Режим доступа "Другие не могут открыть для чтения"
	ConnectModeEnum.Вставить("adModeShareDenyWrite", 8);  // Режим доступа "Другие не могут открыть для записи"
	ConnectModeEnum.Вставить("adModeShareExclusive", 12); // Режим доступа "Монопольное использование - другие не подключатся"
    ConnectModeEnum.Вставить("adModeUnknown", 0);         // 
	ConnectModeEnum.Вставить("adModeWrite", 2 );          // Режим доступа "Только для записи"
	ADO.Вставить("ConnectModeEnum", ConnectModeEnum);
	
	// Тип команд для объекта Command (то, как будут интерпретированы аргументы команды)
	CommandTypeEnum = Новый Структура;
	CommandTypeEnum.Вставить("adCmdUnspecified", -1); // Тип команды не определен
	CommandTypeEnum.Вставить("adCmdText", 1);         // В поле CommandText содержится
	                                                  // произвольный текстовый SQL-запрос,
													  // возвращающий данные
	CommandTypeEnum.Вставить("adCmdTable", 2);        // В поле CommandText содержится
	                                                  // название таблицы, все записи
													  // которой нужно получить автосгенерированным SQL-запросом
	CommandTypeEnum.Вставить("adCmdStoredProc", 4);   // В поле CommandText содержится
	                                                  // название хранимой процедуры,
													  // возвращающий данные
	CommandTypeEnum.Вставить("adCmdUnknown", 8);      // Тип указан и будет определяться
	                                                  // автоматически (adCmdText, adCmdTable, adCmdStoredProc)
													  // в зависимости от содержимого CommandText
	CommandTypeEnum.Вставить("adCmdFile", 256);       // В поле CommandText содержится
	                                                  // имя файла, содержимое которого
													  // нужно получить в объекте Recordset
	CommandTypeEnum.Вставить("adCmdTableDirect", 512);// В поле CommandText содержится
	                                                  // название таблицы, все записи
													  // которой нужно получить напрямую, без SQL-запроса
	ADO.Вставить("CommandTypeEnum", CommandTypeEnum);
	
	// Задает тип данных в таких объектах, как: Field, Parameter и Property
	DataTypeEnum = Новый Структура;
	DataTypeEnum.Вставить("AdArray", 8192);           // Комбинируется с другими типами, указывая, что это массив (того "другого" типа)
	DataTypeEnum.Вставить("adBigInt", 20);            // 64-битное целое со знаком
	DataTypeEnum.Вставить("adBinary", 128);           // Двоичное значение
	DataTypeEnum.Вставить("adBoolean", 11);           // Булево
	DataTypeEnum.Вставить("adBSTR", 8);               // Строка символов,  с завершающим символом Null (в кодировке Unicode)
	DataTypeEnum.Вставить("adChapter", 136);          //
	DataTypeEnum.Вставить("adChar", 129);             // Строка символов
	DataTypeEnum.Вставить("adCurrency", 6);           // Денежная сумма. 8-байтовое целое со знаком, кратное 10000
	DataTypeEnum.Вставить("adDate", 7);               // Дата. Вещественное число, показывающее кол-во дней, прошедшее с 12/30/1899
	DataTypeEnum.Вставить("adDBDate", 133);           // Значение даты в формате ГГГГММДД
	DataTypeEnum.Вставить("adDBTime", 134);           // Значение времени в формате ччммсс
	DataTypeEnum.Вставить("adDBTimeStamp", 135);      // Точная дата и время в формате ГГГГММДДччммсс плюс тысячные доли секунды
	DataTypeEnum.Вставить("adDecimal", 14);           // Десятичное число с фиксированными целой и дробной частями
	DataTypeEnum.Вставить("adDouble", 5);             // Число с плавающей точкой двойной точности
	DataTypeEnum.Вставить("adEmpty", 0);              // Пустое значение (значение не определено)
	DataTypeEnum.Вставить("adError", 10);             // 32-х битный код ошибки
	DataTypeEnum.Вставить("adFileTime", 64);          // 64-х битное число, показывающее кол-во интервалов по 100нс, прошедшее с 01/01/1601
	DataTypeEnum.Вставить("adGUID", 72);              // Глобальный уникальный идентификатор GUID
	DataTypeEnum.Вставить("adIDispatch", 9);          // Указатель на интерфейс COM-объекта IDispatch. В настоящее время - не поддерживается.
	DataTypeEnum.Вставить("adInteger", 3);            // 32-битное целое со знаком
	DataTypeEnum.Вставить("adIUnknown", 13);          // Указатель на интерфейс COM-объекта IUnknown. В настоящее время - не поддерживается.
	DataTypeEnum.Вставить("adLongVarBinary", 205);    // Длинное двоичное значение (только для объекта Parameter) 
	DataTypeEnum.Вставить("adLongVarChar", 201);      // Длинное строковое значение (только для объекта Parameter) 
	DataTypeEnum.Вставить("adLongVarWChar", 203);     // Длинное строковое значение, заканчивающееся символом Null - строка Unicode (только для объекта Parameter)
	DataTypeEnum.Вставить("adNumeric", 131);          // Десятичное число фиксированной точности 
	DataTypeEnum.Вставить("adPropVariant", 138);      //
	DataTypeEnum.Вставить("adSingle", 4);             // Значение одинарной точности с плавающей точкой 
	DataTypeEnum.Вставить("adSmallInt", 2);           // 16-битное целое со знаком
	DataTypeEnum.Вставить("adTinyInt", 16);           // 8-битное целое со знаком
	DataTypeEnum.Вставить("adUnsignedBigInt", 21);    // 64-битное целое без знака
	DataTypeEnum.Вставить("adUnsignedInt", 19);       // 32-битное целое без знака
	DataTypeEnum.Вставить("adUnsignedSmallInt", 18);  // 16-битное целое без знака
	DataTypeEnum.Вставить("adUnsignedTinyInt", 17);   // 8-битное целое без знака
	DataTypeEnum.Вставить("adUserDefined", 132);      // Переменная определяемая пользователем
	DataTypeEnum.Вставить("adVarBinary", 204);        // Двоичное значение (только для объекта Parameter) 
	DataTypeEnum.Вставить("adVarChar", 200);          // Строковое значение (только для объекта Parameter) 
	DataTypeEnum.Вставить("adVariant", 12);           // Вариантное значение. В настоящее время - не поддерживается.
	DataTypeEnum.Вставить("adVarNumeric", 139);       // Десятичное число фиксированной точности (только для объекта Parameter) 
	DataTypeEnum.Вставить("adVarWChar", 202);         // Символьная строка Unicode, заканчивающаяся символом Null (только для объекта Parameter) 
	DataTypeEnum.Вставить("adWChar", 130);            // Символьная строка Unicode, заканчивающаяся символом Null
	ADO.Вставить("DataTypeEnum", DataTypeEnum);
	
	// Задает направление передачи параметра в запросе
	ParameterDirectionEnum = Новый Структура;
	ParameterDirectionEnum.Вставить("adParamInput", 1);      // Входной параметр
	ParameterDirectionEnum.Вставить("adParamInputOutput", 3);// Входной и выходной параметр
	ParameterDirectionEnum.Вставить("adParamOutput", 2);     // Выходной параметр
	ParameterDirectionEnum.Вставить("adParamReturnValue", 4);// Параметр-возвращаемое значение
	ParameterDirectionEnum.Вставить("adParamUnknown", 0);    // Неизвестное направление передачи
	ADO.Вставить("ParameterDirectionEnum", ParameterDirectionEnum);

	// Определяет состояние объекта (открыт он или закрыт) для объектов подключения к
	// источникам данных, исполнения команд и извлечения данных
	ObjectStateEnum = Новый Структура;
	ObjectStateEnum.Вставить("adStateClosed", 0);     // Указывает, что объект закрыт
	ObjectStateEnum.Вставить("adStateOpen", 1);       // Указывает, что объект открыт
	ObjectStateEnum.Вставить("adStateConnecting", 2); // Указывает, что объект подключен
    ObjectStateEnum.Вставить("adStateExecuting", 4);  // Указывает, что объект выполняет команду
    ObjectStateEnum.Вставить("adStateFetching", 8);   // Указывает, что строки объекта извлекаются
	ADO.Вставить("ObjectStateEnum", ObjectStateEnum);
	
	// Задает тип хранимых данных в объекте Stream
	StreamTypeEnum = Новый Структура;
	StreamTypeEnum.Вставить("adTypeBinary", 1);           // Двоичные данные
	StreamTypeEnum.Вставить("adTypeText",   2);           // Текстовые данные, зависящие 
	                                                      // от кодировки, указанной в Stream.Charset
	ADO.Вставить("StreamTypeEnum", StreamTypeEnum);
	
	// Задает тип создания файла при выполнении сохранения объекта Stream
	SaveOptionsEnum = Новый Структура;
	SaveOptionsEnum.Вставить("adSaveCreateNotExist", 1);  // Файл создается, только если он не существовал ранее
	SaveOptionsEnum.Вставить("adSaveCreateOverWrite",2);  // Файл создается независимо от того, существует он или нет
	                                                      // (если уже существует, то перезаписывается)
	ADO.Вставить("SaveOptionsEnum", SaveOptionsEnum);
	
	
	Возврат ADO
	
КонецФункции


Работа с базой данных. Подпрограммы на 1С.


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

Основной класс для работы с базой данных – это класс «ПодключениеКБазе». Он позволяет выполнять подключение-отключение к БД и выполнять запросы.

Также удобным вспомогательным средством является класс «НаборПараметровЗапроса». Этот набор позволяет удобным образом встраивать различные значения в текст SQL-запросов. Мы готовим шаблон запроса, добавляем в набор именованные значения (параметры), а затем обрабатываем шаблон текста запроса, содержащий указания на то, в каком месте вставлять то или иное значение запроса. Набор обрабатывает шаблон запроса, подставляет в него конкретные значения и таким образом получает окончательный текст запроса. В шаблоне запроса место подстановки значения параметра указывается как : имя_параметра (двоеточие, а затем название параметра – так это делается в Delphi).
Вот как это выглядит на примере:
НабПрмЗапр = НабПрмЗапр_Конструктор();
...
// Выбор группы
// Шаблон запроса
QSelectGroupTmpl = "SELECT grid, pgid, grname FROM groups WHERE grid = :grid;";
// Очищаем набор от предыдущего использования
НабПрмЗапр_Очистить(ПодкБазе.НабПрмЗапр);
// Добавляем в набор параметр с именем grid и значением 10
НабПрмЗапр_УстПарам(НабПрмЗапр, "grid", 10);
// Просто для примера – изменили значение уже существующего
// в наборе параметра с именем grid на 20. В наборе по-прежнему 1 параметр
НабПрмЗапр_УстПарам(НабПрмЗапр, "grid", 20);
// Обрабатываем шаблон запроса – подставляем значения параметров – получаем 
// окончательный текст запроса – 
// должно быть: "SELECT grid, pgid, grname FROM groups WHERE grid = 20;"
ТекстЗапроса = НабПрмЗапр_ЗначВТекстЗапр(НабПрмЗапр, QSelectGroupTmpl);

Более подробно использование этого механизма (шаблоны запросов) можно посмотреть в демонстрационном примере в подпрограммах класса «ПодкБазе», начиная с функции «ПодкБазе_SelectGroup».

Набор параметров анализирует тип значения (понимает «Неопределено», «Число», «Строка», «Дата», «Булево») и подставляет значения в запрос в синтаксисе, корректном для данной СУБД.

Исходный текст класса «НаборПараметровЗапроса»:
Исходный текст
////////////////////////////////////////////////////////////////////////////////
// Реализация класса НаборПараметровЗапроса (НабПрмЗапр)

// Создание полей объекта в виде структуры.
//
// Параметры:
//  Нет.
// Возврат:
//  Возвращает созданную структуру
//
Функция НабПрмЗапр_СоздатьОбъект() Экспорт
	
	НабПрмЗапр = Новый Структура;
	НабПрмЗапр.Вставить("СписПрм", Новый Массив); // Набор параметров
	
	Возврат НабПрмЗапр;
	
КонецФункции

// Имитирует конструктор объекта.
//
// Параметры:
//  Нет.
// Возврат:
//  (Структура) - Структура с полями объекта
//
Функция НабПрмЗапр_Конструктор() Экспорт
	
	НабПрмЗапр = НабПрмЗапр_СоздатьОбъект();
	
	Возврат НабПрмЗапр;
	
КонецФункции

// Имитирует деструктор объекта - освобождает память.
//
// Параметры:
//  НабПрмЗапр - ссылка на объект
//
Процедура НабПрмЗапр_Деструктор(НабПрмЗапр) Экспорт
	
	НабПрмЗапр_Очистить(НабПрмЗапр)
	
КонецПроцедуры

// Установка нового значения параметра. Если параметр не существует, он будет добавлен
//
// Параметры:
//  НабПрмЗапр - ссылка на объект
//  ИмяПрм - название параметра (регистр не учитывается)
//  ЗначПрм - новое значение параметра
//
Процедура НабПрмЗапр_УстПарам(НабПрмЗапр, Знач ИмяПрм, Знач ЗначПрм) Экспорт
	
	Если (ИмяПрм <> Неопределено) И  (ТипЗнч(ИмяПрм) = Тип("Строка")) И (СокрЛП(ИмяПрм) <> "") Тогда
		ИмяПрм = СокрЛП(ИмяПрм);
		// Поиск в имеющихся параметрах
		Для Каждого Прм Из НабПрмЗапр.СписПрм Цикл
			Если ВРег(Прм.ИмяПрм) = ВРег(ИмяПрм) Тогда
				// Нашли параметр - обновить значение
				Прм.ЗначПрм = ЗначПрм;
				Возврат
			КонецЕсли
		КонецЦикла;
		// Добавить новый параметр
		Прм = Новый Структура;
		Прм.Вставить("ИмяПрм", ИмяПрм);
		Прм.Вставить("ЗначПрм", ЗначПрм);
		НабПрмЗапр.СписПрм.Добавить(Прм)
	КонецЕсли
	
КонецПроцедуры

// Возвращает количество параметров в наборе
//
// Параметры:
//  НабПрмЗапр - ссылка на объект
// Возврат:
//  Количество параметров в наборе
//
Функция НабПрмЗапр_Количество(НабПрмЗапр) Экспорт
	
	Возврат НабПрмЗапр.СписПрм.Количество()
	
КонецФункции

// Очищает набор параметров - удаляет все параметры из запроса
//
// Параметры:
//  НабПрмЗапр - ссылка на объект
//
Процедура НабПрмЗапр_Очистить(НабПрмЗапр) Экспорт
	
	НабПрмЗапр.СписПрм.Очистить()
	
КонецПроцедуры

// Преобразует маску сравнения с подстановочными символами
// (? - любой один символ, * - любое кол-во любых символов)
// в аргумент SQL-оператора Like (соответствующие символы _ и %)
//
// Параметры:
//  ЗначМаски - исходное значение маски с символами шаблона: ? и *
// Возврат:
//  Преобразованное значение маски с символами шаблона: _ и %
//
Функция НабПрмЗапр_ПреобрМаскуLike(Знач ЗначМаски) Экспорт
	
	Рез = "";
	Если (ЗначМаски <> Неопределено) И 
		 (ТипЗнч(ЗначМаски) = Тип("Строка")) И 
		 (ЗначМаски <> "") Тогда
		Рез = ЗначМаски;
		Рез = СтрЗаменить(Рез, "_", "");
		Рез = СтрЗаменить(Рез, "%", "");
		Рез = СтрЗаменить(Рез, "?", "_");
		Рез = СтрЗаменить(Рез, "*", "%");
	КонецЕсли;
	
	Возврат Рез

КонецФункции

// Преобразует значение параметры в строку, пригодную для подстановки в 
// SQL-запросы FireBird
//  - числовое значение выводится без пробелов с точкой в качестве разделителя
//  - булевские значения выводятся как 0 и 1
//  - значения даты выводятся в формате dd.mm.yyyy hh:mm:ss, заключенные в апострофы
//  - строковые значения выводятся в апострофах с экранированием недопустимых символов
//  - значения Неопределено и Null выволятся как NULL
//
// Параметры:
//  ЗначПрм - значение
// Возврат:
//  Строковое представление значения, пригодное для окончательной подстановки в запрос
//
Функция НабПрмЗапр_ЗначВСтроку(Знач ЗначПрм) Экспорт
	
	Рез = "";
	Если (ЗначПрм = Неопределено) ИЛИ (ЗначПрм = Null) Тогда
		Рез = "NULL"
	Иначе
		Если ТипЗнч(ЗначПрм) = Тип("Число") Тогда
			// Числовое значение
			Рез = Формат(ЗначПрм, "ЧГ=0; ЧРД='.'; ЧН=''")
		Иначе
			Если ТипЗнч(ЗначПрм) = Тип("Дата") Тогда
				// Значение - дата
				Рез = "'" + Формат(ЗначПрм, "ДФ='dd.MM.yyyy HH:mm:ss'") + "'"
			Иначе
				Если ТипЗнч(ЗначПрм) = Тип("Булево") Тогда
					// Булево значение
					Рез = Формат(ЗначПрм, "БЛ=0; БИ=1")
				Иначе
					Если ТипЗнч(ЗначПрм) = Тип("Строка") Тогда
						// Строковое значение
						Рез = СтрЗаменить(ЗначПрм, "'", "''");  // Экранирование
						Рез = "'" + Рез + "'"
					КонецЕсли
				КонецЕсли
			КонецЕсли
		КонецЕсли
	КонецЕсли;
	
	Возврат Рез

КонецФункции

// Получает текст запроса из шаблона, подставляя в шаблон значения параметров.
//
// Параметры:
//  НабПрмЗапр - ссылка на объект
//  ШаблонЗапр - шаблон запроса, где места простановки параметров обозначены как ":имя_параметра"
// Возврат:
//  Переработанный шаблон запроса, в который подставлены значения
//
Функция НабПрмЗапр_ЗначВТекстЗапр(НабПрмЗапр, Знач ШаблонЗапр) Экспорт
	
	Рез = "";
	Если (ШаблонЗапр <> Неопределено) И 
		 (ТипЗнч(ШаблонЗапр) = Тип("Строка")) И 
		 (ШаблонЗапр <> "") Тогда
		Рез = ШаблонЗапр;
		Для Каждого Прм Из НабПрмЗапр.СписПрм Цикл
			Рез = СтрЗаменить(Рез, ":" + Прм.ИмяПрм, НабПрмЗапр_ЗначВСтроку(Прм.ЗначПрм))
		КонецЦикла
	КонецЕсли;
	
	Возврат Рез

КонецФункции


Итак, рассмотрим, наконец, основной класс работы с БД – класс «ПодключениеКБазе».
Класс имеет в своем составе следующие подпрограммы:
Название подпрограммы Описание Примечание
Группа подпрограмм общего назначения. Здесь конструктор объекта, подпрограммы подключения к БД и отключения от БД, а также подпрограммы, возвращающие текущие значения общих свойств объекта.
Функция ПодкБазе_СоздатьОбъект() Создание структуры объекта
Функция ПодкБазе_Конструктор() Конструктор объекта
Процедура ПодкБазе_Деструктор(ПодкБазе) Деструктор объекта
Функция ПодкБазе_GetDBAddr(ПодкБазе) Возвращает адрес базы данных
Функция ПодкБазе_GetDBUserName(ПодкБазе) Возвращает имя пользователя сервера БД, с помощью которого подключились
Функция ПодкБазе_GetConnect(ПодкБазе) Возвращает признак установки соединения с базой
Функция ПодкБазе_ConnectToDB(ПодкБазе, FBDBAddr, FBUserName, FBUserPass) Выполняет подключение к серверу БД
Процедура ПодкБазе_Disconnect(ПодкБазе) Производит отключение от базы
Функция ПодкБазе_GetUserRegister(ПодкБазе) Возвращает признак регистрации пользователя
Функция ПодкБазе_RegisterUser (ПодкБазе, UserName, UserPass) Выполняет регистрацию пользователя Проверяет учетные данные пользователя в системе
Процедура ПодкБазе_UnregisterUser(ПодкБазе) Закрывает сеанс текущего пользователя
Функция ПодкБазе_GetRegLogin(ПодкБазе) Возвращает логин пользователя в системе, указанный при авторизации
Функция ПодкБазе_GetUserID(ПодкБазе) Возвращает идентификатор учетной записи в базе для текущего зарегистрированного пользователя
Функция ПодкБазе_GetUserName(ПодкБазе) Возвращает название учетной записи в базе для текущего зарегистрированного пользователя
Функция ПодкБазе_GetUserRights(ПодкБазе) Возвращает маску прав текущего пользователя
Функция ПодкБазе_TestUserRight(ПодкБазе, RightIndex) Проверяет, обладает ли текущий пользователь указанным правом
Функция ПодкБазе_GetErrorMsg(ПодкБазе) Возвращает сообщение о последней возникшей ошибке.
Группа подпрограмм, предназначенных для непосредственного выполнения команд ADO – непосредственного выполнения запросов без каких-либо дополнительных логических проверок. Однако выполняется проверка существования соединения с БД (защита от разрыва соединения).
Процедура ПодкБазе_ClearCommandParameters(ПодкБазе) Выполняет очистку набора параметров ADODB.Command
Функция ПодкБазе_ExecSQL(ПодкБазе, ТекстЗапроса) Выполняет SQL-запрос, не
возвращающий набор данных
Функция ПодкБазе_GetRecordSet(ПодкБазе, ТекстЗапроса) Выполняет SQL-запрос, который
возвращает набор данных
Функция ПодкБазе_CheckRestConnect(ПодкБазе, ТестОбязат=Ложь) Выполняет проверку работоспособности соединения с сервером.
Группа подпрограмм для выполнения запросов к базе более высокого логического уровня, по сравнению с предыдущей группой. Здесь проверяется права пользователя на доступ к данным.
Функция ПодкБазе_CheckRights(ПодкБазе, RightsMask="") Выполняет проверку прав доступа к информации в базе.
Функция ПодкБазе_SelectDataSet(ПодкБазе, ТекстЗапроса, DstDataSet, RightsMask="") Выполняет операцию по выборке данных из базы в набор данных
Функция ПодкБазе_SelectSingleVal(ПодкБазе, ТекстЗапроса, OutVal, OutValName, RightsMask="") Выполняет операцию по выборке из базы одного единственного значения
Функция ПодкБазе_SelectPair(ПодкБазе, ТекстЗапроса, OutVal1, OutVal2, OutValName1, OutValName2, RightsMask="") Выполняет операцию по выборке из базы пары значений
Функция ПодкБазе_SelectList(ПодкБазе, ТекстЗапроса, List, RightsMask="") Выполняет операцию по выборке данных из базы в список значений
Функция ПодкБазе_SelectListPair(ПодкБазе, ТекстЗапроса, List, RightsMask="") Выполняет операцию по выборке данных из базы в список пар значений
Функция ПодкБазе_SelectObject(ПодкБазе, ТекстЗапроса, Obj, RightsMask="") Выполняет операцию по выборке данных из базы в объект
Функция ПодкБазе_SelectObjects(ПодкБазе, ТекстЗапроса, ClassID, ObjectsSet, RightsMask="") Выполняет операцию по выборке данных из базы в набор объектов
Функция ПодкБазе_WriteQuery(ПодкБазе, ТекстЗапроса, RightsMask="") Выполняет операцию по записи в базу данных без возврата результата
Группа непосредственно прикладных вызовов, с помощью которых функционал верхнего уровня выполняет необходимые действия с моделью данных.
Функция ПодкБазе_SelectGroup(ПодкБазе, GRID, ObjGroup) Загружает поля указанной группы
Функция ПодкБазе_SelectGroups(ПодкБазе, PGRID, ObjSetGroups) Загружает набор групп, входящих в указанную группу
Функция ПодкБазе_SelectCard(ПодкБазе, CDID, ObjCard) Загружает поля указанной карточки с контактной информацией
Функция ПодкБазе_SelectCardsNames(ПодкБазе, GRID, ObjSetCards) Загружает набор карточек, входящих в указанную группу Загружаются только заголовки карточек (BLOB-поля не загружаются)
Функция ПодкБазе_SelectUser(ПодкБазе, UID, ObjUser) Загружает поля указанного пользователя
Функция ПодкБазе_SelectUsers(ПодкБазе, ObjSetUsers) Загружает набор пользователей — все пользователи системы
Функция ПодкБазе_SaveGroup(ПодкБазе, ObjGroup) Сохраняет в базу группу — выполняет INSERT для нового объекта и UPDATE для уже существующего
Функция ПодкБазе_DeleteGroup(ПодкБазе, GRID) Удаляет из базы группу и все её содержимое (подгруппы и карточки)
Функция ПодкБазе_SaveCard(ПодкБазе, ObjCard) Сохраняет в базу карточку — выполняет INSERT для нового объекта и UPDATE для уже существующего.
Функция ПодкБазе_DeleteCard(ПодкБазе, CDID) Удаляет из базы указанную карточку
Функция ПодкБазе_MoveGroupToGroup(ПодкБазе, SrcGRID, DstGRID) Перемещает указанную группу и все ее содержимое в другую группу. Сама группа при этом остается на прежнем месте.
Функция ПодкБазе_MoveGroupContentsToGroup(ПодкБазе, SrcGRID, DstGRID) Перемещает содержимое указанной группы в другую группу. Сама группа при этом остается на прежнем месте.
Функция ПодкБазе_MoveCardToGroup(ПодкБазе, SrcCDID, DstGRID) Перемещает указанную карточку в другую группу.
Функция ПодкБазе_UpdatePassword(ПодкБазе, OldPassword, NewPassword) Выполняет обновление (перезапись в базу) пароля текущего пользователя сеанса.
Функция ПодкБазе_SaveUsers(ПодкБазе, ObjSetUsers, DelUsersIDs) Сохраняет в базу набор пользователей — выполняет INSERT для новых объектов и UPDATE
для уже существующих. Также удаляет указанных пользователей — выполняет DELETE.

Рассмотрим основные подпрограммы, непосредственно связанные с работой через ADO:
установление соединения и регистрация пользователя (имеется в виду не пользователь БД, а внутренний пользователь системы) – функции «ПодкБазе_ConnectToDB», «ПодкБазе_RegisterUser».
Установление соединения проходит следующие этапы (переход к последующему этапу происходит при успешном окончании предыдущего, а иначе весь процесс завершается с ошибкой):
— подключение к СУБД через «ADODB.Connection»;
— создание объекта выполнения команд «ADODB.Command» через установленное подключение. Все наши команды – это всегда произвольные SQL-запросы (CommandType = CommandTypeEnum.adCmdText). Другие типы команд (таблицы, хранимые процедуры и т.д.) никогда не используем;
— проверка корректности базы, того что подключенная база именно данной информационной системы (в нашем случае просто проверяем наличие нужных таблиц в базе).
Регистрация пользователя состоит из проверки наличия указанной при регистрации учетной записи и ее пароля в базе. Если такой пользователь в базе существует, то в объекте подключения выставляются соответствующие поля в значение успеха авторизации пользователя.
Исходный текст описанных функций:
Исходный текст
// Выполняет подключение к серверу БД
//
// Параметры:
//  ПодкБазе - ссылка на объект
//  FBDBAddr - адрес базы данных (возможно с указанием сервера)
//  FBUserName - имя пользователя БД
//  FBUserPass - пароль пользователя БД
// Возврат:
//  Истина, если подключение удалось и Ложь - в противном случае
//
Функция ПодкБазе_ConnectToDB(ПодкБазе, FBDBAddr, FBUserName, FBUserPass) Экспорт

	// Запрос на получение списка таблиц базы
	QGetTab = "SELECT RDB$RELATION_NAME AS tabname FROM RDB$RELATIONS " + 
			  "WHERE ((RDB$SYSTEM_FLAG = 0)AND(RDB$VIEW_SOURCE IS NULL)) " +
			  "ORDER BY RDB$RELATION_NAME;";

	Рез = Ложь;
	ПодкБазе.FBDBAddr = FBDBAddr;        // Параметры
	ПодкБазе.FBUserName = FBUserName;    // соединения
	ПодкБазе.FBUserPass = FBUserPass;    // с сервером БД
	ПодкБазе.FBSrvConn = Неопределено;   // Подключения к серверу нет
	ПодкБазе.FBCommand = Неопределено;   // Объект для выполнения команд не создан
	ПодкБазе.IsDBConnect = Ложь;         // База не открыта
	ПодкБазе.LastTestTime = 0;           // Сбросить время последнего обновления
	ПодкБазе_UnregisterUser(ПодкБазе);   // Никто не зарегистрирован
	ПодкБазе.ErrMsg = "";                // Ошибок не найдено

	// Выполняем подключение к базе данных FireBird
	ПарамПодкл_Driver = "driver={" + "Firebird/InterBase(r) driver" + "}";
	ПарамПодкл_UID = "uid=" + FBUserName;
	ПарамПодкл_PWD = "pwd=" + FBUserPass;
	ПарамПодкл_DataBase = "database=" + FBDBAddr;
	СтрокаСоединения = ПарамПодкл_Driver + ";" +
					   ПарамПодкл_UID + ";" + 
					   ПарамПодкл_PWD + ";" + 
					   ПарамПодкл_DataBase;
	Попытка
		// Создаем соединение
		ПодкБазе.FBSrvConn = Новый COMОбъект("ADODB.Connection");
		// Открываем соединение
		ПодкБазе.FBSrvConn.open(СтрокаСоединения);
		ПодкБазе.IsDBConnect = Истина;
		// Создать объект для выполнения запросов
		ПодкБазе.FBCommand = Новый COMObject("ADODB.Command");
		ПодкБазе.FBCommand.ActiveConnection = ПодкБазе.FBSrvConn;
		ПодкБазе.FBCommand.NamedParameters = True;
		ПодкБазе.FBCommand.CommandType = ADO.CommandTypeEnum.adCmdText;
		// Проверить возможность работы с базой
		// Проверить, что нужные таблицы присутствуют
		ТекстЗапроса = QGetTab;
		Попытка
			ПодкБазе.FBCommand.CommandText = ТекстЗапроса;
			НаборДанных = ПодкБазе.FBCommand.Execute();
		Исключение
			НаборДанных = Неопределено
		КонецПопытки;
		Если НаборДанных <> Неопределено Тогда
			Если НаборДанных.State = ADO.ObjectStateEnum.adStateOpen Тогда
				Попытка
					СписокТаблиц = Новый Массив;
					// Получить список таблиц
					Пока НаборДанных.EOF = 0 Цикл
						СписокТаблиц.Добавить(СокрЛП(НаборДанных.Fields("TABNAME").Value));
						// Перейти к следующей записи
						НаборДанных.MoveNext();
					КонецЦикла;
					// Проверить наличие нужных таблиц
					Для Каждого TabName Из Проч.QTabsLst Цикл
						Если СписокТаблиц.Найти(TabName) = Неопределено Тогда
							// Не найдена нужная таблица в базе
							ПодкБазе.ErrMsg = "Не найдена таблица " + TabName + " в базе";
							Прервать
						КонецЕсли
					КонецЦикла;
				Исключение
					// Возникла ошибка при чтении списка таблиц
					ПодкБазе.ErrMsg = "Не удалось получить список таблиц базы"
				КонецПопытки
			Иначе
				// Возникла ошибка при чтении списка таблиц
				ПодкБазе.ErrMsg = "Не удалось получить список таблиц базы";
			КонецЕсли;
			// Уничтожить результаты запроса
			НаборДанных.Close();
			НаборДанных = Неопределено
		Иначе
			// Возникла ошибка при чтении списка таблиц
			ПодкБазе.ErrMsg = "Не удалось получить список таблиц базы"
		КонецЕсли;
		
	Исключение
		// Возникла ошибка
		ПодкБазе.ErrMsg = "Не удалось подключиться к базе FireBird: " + ОписаниеОшибки()
	КонецПопытки;
	
	Если ПодкБазе.ErrMsg = "" Тогда
		// Все проверки завершены - подключение выполнено
		Рез = Истина
	Иначе
		// Эта база не подходит - отключиться
		ПодкБазе_Disconnect(ПодкБазе)
	КонецЕсли;
	Возврат Рез;
	
КонецФункции

// Выполняет регистрацию пользователя - проверяет регистрационную
// информацию об имени UserName и пароле UserPass по предварительно открытой
// базе данных. 
//
// Параметры:
//  ПодкБазе - ссылка на объект
//  UserName - логин пользователя
//  UserPass - пароль пользователя
// Возврат:
//  Истина, если пользователь прошел регистрацию
//  (имеется в базе и пароль верен) и Ложь - в противном случае
//
Функция ПодкБазе_RegisterUser(ПодкБазе, UserName, UserPass) Экспорт
	
	// Запрос на получение идентификатора и группы пользователя
	QGetUsr = "SELECT * FROM USERS WHERE " +
			  "((UPPER(UNAME COLLATE PXW_CYRL) = :upname)OR(UNAME = :name)) AND (UPWD = :pass);";
			  
	Рез = Ложь;
	ПодкБазе.ErrMsg = "";  // Ошибок не найдено
	Если ПодкБазе.IsDBConnect Тогда
		// Есть подключение - можно проверять регистрационные данные
		ПодкБазе_UnregisterUser(ПодкБазе);  // Удалить старые регистрационные данные
		// Выполнить проверку новых данных
		// Получаем информацию о пользователе
		НабПрмЗапр_Очистить(ПодкБазе.НабПрмЗапр);
		НабПрмЗапр_УстПарам(ПодкБазе.НабПрмЗапр, "upname", ВРег(UserName));
		НабПрмЗапр_УстПарам(ПодкБазе.НабПрмЗапр, "name", UserName);
		НабПрмЗапр_УстПарам(ПодкБазе.НабПрмЗапр, "pass", UserPass);
		ТекстЗапроса = НабПрмЗапр_ЗначВТекстЗапр(ПодкБазе.НабПрмЗапр, QGetUsr);
		Попытка
			ПодкБазе.FBCommand.CommandText = ТекстЗапроса;
			НаборДанных = ПодкБазе.FBCommand.Execute();
		Исключение
			НаборДанных = Неопределено
		КонецПопытки;
		Если НаборДанных <> Неопределено Тогда
			Если (НаборДанных.State = ADO.ObjectStateEnum.adStateOpen) И 
				 (НаборДанных.EOF = 0) Тогда
				Попытка
					ПодкБазе.UID = НаборДанных.Fields("UID").Value;
					ПодкБазе.UName = НаборДанных.Fields("UNAME").Value;
					ПодкБазе.Rights = НаборДанных.Fields("URIGHTS").Value;
				Исключение
				КонецПопытки;
				
				ПодкБазе.IsRegister = Истина
			КонецЕсли;
			// Уничтожить результаты запроса
			НаборДанных.Close();
			НаборДанных = Неопределено
		КонецЕсли;
		Если ПодкБазе.IsRegister Тогда
			// Регистрация прошла успешно
			ПодкБазе.RegLogin = UserName;
			ПодкБазе.RegPass = UserPass;
			
			Рез = Истина
		Иначе
			// Регистрация не удалась
			ПодкБазе.UID = 0;
			ПодкБазе.UName = "";
			ПодкБазе.Rights = "";
			Если ПодкБазе.ErrMsg = "" Тогда
				ПодкБазе.ErrMsg = "Пользователь не зарегистрирован"
			КонецЕсли
		КонецЕсли
	Иначе
		// Подключение не установлено
		ПодкБазе.ErrMsg = "Подключение не установлено"
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции


разрыв соединения и завершение работы пользователя (имеется в виду не пользователь БД, а внутренний пользователь системы) – функции «ПодкБазе_Disconnect», «ПодкБазе_UnregisterUser».
Завершение работы пользователя выполняется просто как перевод соответствующих полей в объекте подключения в состояние «пользователь не авторизован»
Отключение от базы выполняется неявным уничтожением объекта выполнения команд «ADODB.Command», а также закрытием самого подключения «ADODB.Connection» (чтобы корректно завершилось само подключение к серверу БД по TCP) с последующим неявным уничтожением;
Исходный текст описанных функций:
Исходный текст
// Производит отключение от базы
//
// Параметры:
//  ПодкБазе - ссылка на объект
//
Процедура ПодкБазе_Disconnect(ПодкБазе) Экспорт
	
	// Закрываем подключение пользователя
	ПодкБазе_UnregisterUser(ПодкБазе);
	// Отключаемся от базы данных
	ПодкБазе.IsDBConnect = Ложь;
	ПодкБазе.LastTestTime = 0;
	ПодкБазе.FBCommand = Неопределено;
	Если ПодкБазе.FBSrvConn <> Неопределено Тогда
		Если ПодкБазе.FBSrvConn.State = ADO.ObjectStateEnum.adStateOpen Тогда
			ПодкБазе.FBSrvConn.Close()
		КонецЕсли;
		ПодкБазе.FBSrvConn = Неопределено;
	КонецЕсли
	
КонецПроцедуры

// Закрывает сеанс текущего пользователя
//
// Параметры:
//  ПодкБазе - ссылка на объект
//
Процедура ПодкБазе_UnregisterUser(ПодкБазе) Экспорт
	
	ПодкБазе.IsRegister = Ложь;
	ПодкБазе.RegLogin = "";
	ПодкБазе.RegPass = "";
	ПодкБазе.UID = 0;
	ПодкБазе.UName = "";
	ПодкБазе.Rights = "";
	
КонецПроцедуры


выполние запросов и защита от разрыва соединения – функции «ПодкБазе_ExecSQL», «ПодкБазе_GetRecordSet», «ПодкБазе_CheckRestConnect».
Для выполнения запросов используем тот самый объект «ADODB.Command», который был создан при подключении – его метод «Execute». При необходимости возвращаем полученный объект набора данных «ADODB.Recordset» в качестве результата.
Теперь по поводу разрыва соединения. Проблема выглядит следующим образом: при отсутствии обращений клиентской программы к серверу СУБД в течение некоторого непродолжительного времени (15-20 мин.), подключение разрывается (закрывается TCP-соединение) и последующие обращения естественно вызывают ошибку. Если это никак не обрабатывать, то данная вещь неприятна для пользователей – человек отвлекся на другую работу (перестал наживать кнопки в приложении и выполнять запросы к базе), затем вернулся к программе, а она выдает ошибки. И так может быть много раз за день. Потеря несохраненных данных, соответственно обостряет ситуацию.
По поводу природы этого явления особой ясности нет. Некоторые возлагают вину за разрыв соединения на «файрволы», но это объяснение выглядит нелогичным – зачем брандмауэр будет разрывать уже существующее соединение. В любом случае, у меня подобных программ (файрволов) установлено не было, а проблема была. Имейте в виду, проблема возникает при реальном сетевом подключении (если подключение через localhost, т.е. клиент и СУБД на одной машине, что часто бывает у разработчиков, то разрыва не будет).
В протоколе связи между сервером БД и его клиентом не предусмотрено никаких вспомогательных запросов-подтверждений (иногда их называют NOP-запросами) исправности физического TCP-соединения, которые бы периодически проходили в фоновом режиме (например, от сервера к клиенту запрос на наличие соединения, и обратный ответ). Вообще, в FireBird предусмотрены т.н. DUMMY-пакеты (это в принципе должно быть как раз то, что я описал выше), но в конфигурации сервера они по-умолчанию отключены. Более того, есть приписка в комментариях «конфига» о том, что включать нельзя, иначе Windows может «рухнуть». В подтверждение дается ссылка на статью с сайта Microsoft, в которой говорится о подобных проблемах при слишком интенсивном использовании сокетов (почему сокеты так сильно используются DUMMY-пакетами???), причем, только для Windows-2000, да и то лишь до установки соответствующего KB-обновления. Тем не менее, комментарий о недопустимости включения DUMMY-режима проходит через все версии FireBird (может быть его нет только в самых новых).
Вместо этого, в тех же комментариях FireBird заявляется, что СУБД используют с подобной целью (контроль наличия соединения) непосредственно сам TCP-протокол (автоматический фоновый обмен ASK-запросами на уровне TCP), для чего создаются сокеты с опцией SO_KEEPALIVE. Но, во-первых, начало проб «KEEP ALIVE» (при настройках Windows по умолчанию) начинается только по прошествии 2-х часов от начала бездействия канала (а не через 15-20мин., как происходит разрыв в реальности). А во-вторых, еще раз напоминаю, что на момент принудительного разрыва связи, и сервер и клиент находятся в исправном состоянии, т.е. обмен ASK-пакетами не должен приводить к разрыву. В общем, непонятно откуда берется проблема.
Зато понятно, как эту проблему решить. Будем при выполнении каждого запроса, оценивать существование соединения, и если оно разорвано, то будем пытаться восстановить его (открыть заново) незаметно (в фоновом режиме) от самой подпрограммы выполнения запроса. Назовем подпрограмму, осуществляющую контроль наличия соединения (и попытку его восстановления) «Функция ПодкБазе_CheckRestConnect». Она будет возвращать «Истина» в том случае, если соединение существует (или удалось открыть его заново в процессе работы функции) и «Ложь», если восстановить соединение никак не удается (в этом случае запрос выполнен не будет и пользовательский интерфейс программы должен отработать ситуацию пропадания соединения). Как же «CheckRestConnect» проверяет наличие или отсутствие соединения с базой, имея в своем распоряжении только такой высокоуровневый инструмент, как экземпляр «ADODB.Command»? Очень просто – будем выполнять запрос, который должен заведомо корректно отработать на любой базе InterBase/FireBird, например запрос «SELECT 1 FROM RDB$DATABASE». Если запрос не удалось выполнить, значит соединение разорвано и нужно попробовать восстановить его. Вызов «CheckRestConnect» происходит при выполнении каждого запроса. Если «CheckRestConnect» будет выполнять служебный запрос каждый раз, когда его вызывают, то количество запросов возрастет в 2 раза (для каждого полезного + 1 служебный) и даст неоправданную нагрузку на сервер. Поэтому будем выполнять служебный запрос не при каждом вызове «CheckRestConnect», а через определенный интервал времени после последнего выполнения служебного запроса – в моем примере 5мин. Считаем, что за это время, разрыв соединения не произойдет. Заметьте, что в функциях выполнения запросов «ПодкБазе_ExecSQL» и «ПодкБазе_GetRecordSet», вызов «CheckRestConnect» может выполняться до 2-х раз (если после 1-го раза возникла ошибка, то делаем 2-ю попытку). Это сделано на тот случай, если за отведенный интервал (в моем случае 5мин.) все-таки произойдет разрыв, а функция «CheckRestConnect» не сделает проверку служебным запросом, и вернет ложный признак существования соединения. В таком случае, повторный вызов «CheckRestConnect» должен исправить ситуацию.
Исходный текст описанных функций:
Исходный текст
// Выполняет SQL-запрос переданный в параметре ТекстЗапроса и не
// возвращает набор данных (для запросов, отличных от выборок).
// Возвращает Истина, если выборка выполнена и Ложь - в противном случае
//
// Параметры:
//  ПодкБазе - ссылка на объект
//  ТекстЗапроса - текст запроса для выполнения
// Возврат:
//  Истина, если запрос выполнен и Ложь - в противном случае
//
Функция ПодкБазе_ExecSQL(ПодкБазе, ТекстЗапроса) Экспорт
	
	ПодкБазе.ErrMsg = "";          // Ошибок пока не найдено
	
	Рез = Ложь;
    Если ПодкБазе_CheckRestConnect(ПодкБазе) Тогда
		Попытка
			ПодкБазе.FBCommand.CommandText = ТекстЗапроса;
			ПодкБазе.FBCommand.Execute();
			// Запрос выполнен успешно
			Рез = Истина;
		Исключение
			// Ошибка выполнения запроса
			ПодкБазе.ErrMsg = "Ошибка при запросе: " + ОписаниеОшибки();
			// Возможно, потеря коннекта - попробовать еще раз
			Если ПодкБазе_CheckRestConnect(ПодкБазе, Истина) Тогда
				Попытка
					ПодкБазе.FBCommand.CommandText = ТекстЗапроса;
					ПодкБазе.FBCommand.Execute();
					// Запрос выполнен успешно
					Рез = Истина;
				Исключение
					// Ошибка выполнения запроса
					ПодкБазе.ErrMsg = "Ошибка при запросе: " + ОписаниеОшибки();
				КонецПопытки
			КонецЕсли
		КонецПопытки
	КонецЕсли;

	Возврат Рез
	
КонецФункции

// Выполняет SQL-запрос переданный в параметре ТекстЗапроса и
// возвращает полученный набор данных (или Неопределено, если набор данных
// не получен).
//
// Параметры:
//  ПодкБазе - ссылка на объект
//  ТекстЗапроса - текст запроса для выполнения
// Возврат:
//  Набор данных или Неопределено, если набор данных не получен
//
Функция ПодкБазе_GetRecordSet(ПодкБазе, ТекстЗапроса) Экспорт
	
	ПодкБазе.ErrMsg = "";          // Ошибок пока не найдено

	Рез = Неопределено;
    Если ПодкБазе_CheckRestConnect(ПодкБазе) Тогда
		Попытка
			ПодкБазе.FBCommand.CommandText = ТекстЗапроса;
			Рез = ПодкБазе.FBCommand.Execute();
		Исключение
			// Ошибка выполнения запроса
			ПодкБазе.ErrMsg = "Ошибка при запросе: " + ОписаниеОшибки();
			// Возможно, потеря коннекта - попробовать еще раз
			Если ПодкБазе_CheckRestConnect(ПодкБазе, Истина) Тогда
				Попытка
					ПодкБазе.FBCommand.CommandText = ТекстЗапроса;
					Рез = ПодкБазе.FBCommand.Execute();
				Исключение
					// Ошибка выполнения запроса
					ПодкБазе.ErrMsg = "Ошибка при запросе: " + ОписаниеОшибки();
				КонецПопытки
			КонецЕсли
		КонецПопытки
	КонецЕсли;

	Возврат Рез
	
КонецФункции

// Выполняет проверку работоспособности соединения с сервером FireBird 
// (не разорвалось ли оно). В случае разрыва соединения, пытается открыть его снова.
// Возвращает Истина, если выборка выполнена и Ложь - в противном случае
//
// Параметры:
//  ПодкБазе - ссылка на объект
//  ТестОбязат - признак (Булево), что необходимо обязательно выполнить проверку,
//               иначе проверка выполняется не чаще чем через установленный интервал
//               (5 мин.)
// Возврат:
//  Истина, если подключение готово к работе и Ложь - в противном случае
//
Функция ПодкБазе_CheckRestConnect(ПодкБазе, ТестОбязат=Ложь)
	
	// Запрос на проверку соединения
	QTestConnect = "SELECT 1 FROM RDB$DATABASE;";
	
	ПодкБазе.ErrMsg = "";          // Ошибок пока не найдено

	Рез = Ложь;
	Если (ПодкБазе.FBSrvConn <> Неопределено) И ПодкБазе.IsDBConnect Тогда
		// Получить текущий отсчет времени
		Интервал = 300000;  // Интервал проверок (не чаще), мс
		ВремяТекущ = ПолучитьТекущееВремяВМиллисекундах();
		Если ТестОбязат ИЛИ
			 (ПодкБазе.LastTestTime = Неопределено) ИЛИ
			 (ПодкБазе.LastTestTime = 0) ИЛИ
			 (ВремяТекущ - ПодкБазе.LastTestTime >= Интервал) Тогда
			// Выполняем проверку соединения тестовым запросом
			Попытка
				ПодкБазе.FBCommand.CommandText = QTestConnect;
				ПодкБазе.FBCommand.Execute();
				// Проверка выполнена удачно
				ПодкБазе.LastTestTime = ВремяТекущ;
				Рез = Истина
			Исключение
				// Ошибка выполнения запроса - пробуем переподключиться
					// Сохранить параметры подключения
				FBDBAddr   = ПодкБазе.FBDBAddr;      // Параметры
				FBUserName = ПодкБазе.FBUserName;    // соединения
				FBUserPass = ПодкБазе.FBUserPass;    // с сервером БД
				IsRegister = ПодкБазе.IsRegister;    // Параметры
				UserName   = ПодкБазе.RegLogin;      // регистрации пользователя
				UserPass   = ПодкБазе.RegPass;
					// Отключиться от базы
				ПодкБазе_Disconnect(ПодкБазе);
					// Пробуем выполнить подключение
				Если ПодкБазе_ConnectToDB(ПодкБазе, FBDBAddr, FBUserName, FBUserPass) Тогда
					// Подключение удалось - возможно нужно перерегистрировать пользователя
					Если IsRegister Тогда
						Если ПодкБазе_RegisterUser(ПодкБазе, UserName, UserPass) Тогда
							// Перерегистрация пользователя выполнена - соединение восстановлено
							ПодкБазе.LastTestTime = ВремяТекущ;
							Рез = Истина
						Иначе
							// Регистрация не удалась - разорвать подключение совсем
							ПодкБазе_Disconnect(ПодкБазе);
						КонецЕсли
					Иначе
						// Перерегистрация пользователя не нужна - соединение восстановлено
						ПодкБазе.LastTestTime = ВремяТекущ;
						Рез = Истина
					КонецЕсли
				КонецЕсли
			КонецПопытки
		Иначе
			// Возвращает удачный результат без фактической проверки
			Рез = Истина
		КонецЕсли
	Иначе
		// Подключение к базе не настроено
		ПодкБазе.ErrMsg = "Отсутствует подключение к базе";
	КонецЕсли;

	Возврат Рез

КонецФункции


Вспомогательные подпрограммы для работы с BLOB-параметрами.


В InterBase/FireBird существует 2 типа BLOB-полей: TEXT и BINARY.
TEXT – это по сути тоже, что и MEMO в других СУБД. Это текст неопределенной размерности, который хранится отдельно от основной таблицы. С ним через ADO можно работать также, как и с CHAR/ VARCHAR – как с текстовой строкой (все перекодировки выполняются автоматически). На уровне SQL – можно делать сравнение с другими текстовыми строками (операторы сравнения и LIKE). В нашей базе есть одно такое поле – это «CARDS.CDCONTACTS».
BINARY – это классический BLOB, как в других СУБД. Набор произвольных байт неопределенной размерности, который хранится отдельно от основной таблицы. Значение такого поля между 1С и СУБД («ADO.Recordset», «ADO.Parameters») в обоих направлениях передаются через массив COMSafeArray (массив одинарной размерности из однобайтовых чисел).
Текст в таких полях лучше не хранить, но в реальности это встречается сплошь и рядом. Для примера и в нашей базе есть такое поле – это «CARDS.CDNOTE». База ничего не знает о том, что в этом поле текст – все перекодировки нужно выполнять вручную. В базе будем хранить текст в кодировке WIN-1251, а у 1С тип данных «Строка» – в кодировке UTF-8. Для преобразования текста будем использовать следующие подпрограммы:
МассивWIN1251_ВСтрокуUTF8
СтрокаUTF8_ВМассивWIN1251
COMSafeArrayWIN1251_ВСтрокуUTF8
СтрокаUTF8_ВCOMSafeArrayWIN1251

Исходный текст
// Преобразует массив кодов символов (в кодировке Windows-1251)
// в строку символов 1C (кодирока UTF-8)
//
// Параметры:
//  МассивWIN1251 - массив кодов символов в кодировке Windows-1251
// Возврат:
//  Полученная строка или "", если операцию выполнить не удалось
//
Функция МассивWIN1251_ВСтрокуUTF8(МассивWIN1251) Экспорт
	
	Рез = "";
	
	Если ТипЗнч(МассивWIN1251) = Тип("Массив") Тогда
		Для Каждого КодСимв Из МассивWIN1251 Цикл
			Если ТипЗнч(КодСимв) = Тип("Число") Тогда
				// Перекодировка символа из Windows-1251 в UTF-8
				Если (КодСимв >= 192) И (КодСимв <= 223) Тогда
					// А - Я
					КодСимв = КодСимв + 848
				ИначеЕсли (КодСимв >= 224) И (КодСимв <= 239) Тогда
					// а - п
					КодСимв = КодСимв + 848
				ИначеЕсли (КодСимв >= 240) И (КодСимв <= 255) Тогда
					// р - я
					КодСимв = КодСимв + 848
				ИначеЕсли (КодСимв = 184) Тогда
					// ё
					КодСимв = 1105
				ИначеЕсли (КодСимв = 168) Тогда
					// Ё
					КодСимв = 1025
				ИначеЕсли (КодСимв = 185) Тогда
					// №
					КодСимв = 8470
				КонецЕсли;
				// Добавить символ к строке
				Рез = Рез + Символ(КодСимв)
			КонецЕсли
		КонецЦикла
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Преобразует строку символов 1С (кодирока UTF-8) в 
// массив кодов символов (в кодировке Windows-1251)
//
// Параметры:
//  СтрокаUTF8 - исходная строка в кодировке UTF-8
// Возврат:
//  Полученный массив символов в кодировке Windows-1251
//
Функция СтрокаUTF8_ВМассивWIN1251(СтрокаUTF8) Экспорт
	
	Рез = Новый Массив;
	
	Если ТипЗнч(СтрокаUTF8) = Тип("Строка") Тогда
		Для Инд=1 По СтрДлина(СтрокаUTF8) Цикл
			КодСимв = КодСимвола(Сред(СтрокаUTF8, Инд, 1));
			// Перекодировка символа из UTF-8 в Windows-1251
			Если КодСимв < 192 Тогда
				КодСимв = КодСимв
			ИначеЕсли (КодСимв >= 192+848) И (КодСимв <= 223+848) Тогда
				// А - Я
				КодСимв = КодСимв - 848
			ИначеЕсли (КодСимв >= 224+848) И (КодСимв <= 239+848) Тогда
				// а - п
				КодСимв = КодСимв - 848
			ИначеЕсли (КодСимв >= 240+848) И (КодСимв <= 255+848) Тогда
				// р - я
				КодСимв = КодСимв - 848
			ИначеЕсли (КодСимв = 1105) Тогда
				// ё
				КодСимв = 184
			ИначеЕсли (КодСимв = 1025) Тогда
				// Ё
				КодСимв = 168
			ИначеЕсли (КодСимв = 8470) Тогда
				// №
				КодСимв = 185
			Иначе
				// Остальные символы пропускаем
				КодСимв = -1
			КонецЕсли;
			// Добавление полученного символа в массив
			Если КодСимв >= 0 Тогда
				Рез.Добавить(КодСимв)
			КонецЕсли	
		КонецЦикла
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Преобразует массив чисел из объекта COMSafeArray (строка в кодировке Windows-1251)
// в строку символов 1C (кодирока UTF-8)
//
// Параметры:
//  ОбъектCOMSafeArray - ссылка на массив чисел типа COMSafeArray
//                       (размерность массива должна быть 1)
//                       Элементы массива - это коды символов исходной строки
//                       в кодировке Windows-1251
// Возврат:
//  Полученная строка или "", если операцию выполнить не удалось
//
Функция COMSafeArrayWIN1251_ВСтрокуUTF8(ОбъектCOMSafeArray) Экспорт
	
	Рез = "";
	
	Если ТипЗнч(ОбъектCOMSafeArray) = Тип("COMSafeArray") И
		 (ОбъектCOMSafeArray.GetDimensions() = 1) И
		 (ОбъектCOMSafeArray.GetLength(0) > 0) Тогда
		Массив = ОбъектCOMSafeArray.Выгрузить();
		Рез = МассивWIN1251_ВСтрокуUTF8(Массив)
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Преобразует строку символов 1C (кодирока UTF-8) в 
// массив кодов символов (кодировка Windows-1251) в виде объекта COMSafeArray
//
// Параметры:
//  СтрокаUTF8 - исходная строка в кодировке UTF-8
// Возврат:
//  Ссылка на массив чисел типа COMSafeArray с кодами символов строки в кодировке Windows-1251
//  или Неопределено, если преобразование не выполнено
//
Функция СтрокаUTF8_ВCOMSafeArrayWIN1251(СтрокаUTF8) Экспорт
	
	Рез = Неопределено;
	
	Если ТипЗнч(СтрокаUTF8) = Тип("Строка") Тогда
		Массив = СтрокаUTF8_ВМассивWIN1251(СтрокаUTF8);
		Рез = Новый COMSafeArray(Массив, "VT_UI1", Массив.Количество())
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции


Также в поле типа BLOB(BINARY) мы храним графическое изображение (картинку) — «CARDS.CDIMAGE». Здесь уже перекодировки не нужны, но возникает проблема обмена данными между объектами классов «Картинка» и «COMSafeArray». «COMSafeArray» может обмениваться данными с объектом «Массив». «Картинка» может обмениваться данными либо с файлом на диске, либо с объектом «ДвоичныеДанные», который в свою очередь может обмениваться данными со строками в формате «BASE64» с помощью встроенных в платформу функций «Base64Значение» и «Base64Строка». Поначалу хотел обойтись оперативной памятью (не задействовав файлы) и решил работать по схеме: «COMSafeArray»«Массив»«ДвоичныеДанные»«Картинка». Для этого написал функции «МассивБайтВСтрокуBase64» и «СтрокаBase64ВМассивБайт».
Таким образом, получился следующий код:
МассивБайтВСтрокуBase64
СтрокаBase64ВМассивБайт
COMSafeArray_ВКартинку
Картинка_ВCOMSafeArray

Исходный текст
// Преобразует массив байт в строку формата BASE64.
//
// Параметры:
//  МассивБайт - одномерный массив числовых значений (каждое число 0..255), который
//               нужно преобразовать;
//  РазСтрЧерез - количество символов BASE64, после которого нужно вставлять символы
//                разрыва в результирующей строке (для удобства чтения результата).
//                По стандарту - разрыв через каждые 72 символа. Если указать 0,
//                то разрывов не будет вообще (весь результат одной строкой);
//  СтрокаBase64 - сюда помещается результат преобразования - строка BASE64;
//  ИндексБайтаОшибки - если возникли ошибки преобразования, то здесь возвращается
//                      индекс в массиве МассивБайт (индексация с 0), на котором 
//                      прервалось преобразование (если -1, то ошибка не в значениях 
//                      исходного массива)
//  СообщОшиб - если возникли ошибки преобразования, то здесь возвращается текстовое 
//              сообщение о возникшей ошибке.
// Возврат:
//  Истина, если преобразование выполнено и Ложь, если возникли ошибки (в этом
//  случае информация об ошибке возвращается в соответствующих параметрах)
//
Функция МассивБайтВСтрокуBase64(МассивБайт, РазСтрЧерез=72, СтрокаBase64, ИндексБайтаОшибки, СообщОшиб) Экспорт
	
	АлфавитBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	
	Рез = Ложь;
	ИндексБайтаОшибки = -1;
	СообщОшиб = "";
	
	Если ТипЗнч(МассивБайт) = Тип("Массив") Тогда
		Если (ТипЗнч(РазСтрЧерез) = Тип("Число")) И (РазСтрЧерез >= 0) Тогда
			ВГраницаМассива = МассивБайт.ВГраница();
			СтрокаBase64 = "";
			ДлТекущПодстроки = 0; // Если СтрокаBase64 делится на подстроки по
			                      // РазСтрЧерез символа, то это текущ. длина такой подстроки
			ИндБайтаВТриаде = 0; // Индекс (0..2) текущего элемента массива в текущей триаде 
			ЗначТриады = 0;// Число, выражающее значение всех 3-х байтов триады
			Для Инд=0 По ВГраницаМассива Цикл
				// Получить значение очередного байта
				ЗначБайт = МассивБайт[Инд];
				Если (ЗначБайт >= 0) И (ЗначБайт <= 255) Тогда
					// Корректное значение байта
					ЗначТриады = 256*ЗначТриады + ЗначБайт;
					ИндБайтаВТриаде = ИндБайтаВТриаде + 1;
					Если (ИндБайтаВТриаде > 2) ИЛИ (Инд = ВГраницаМассива) Тогда
						// Закончилась текущая триада или закончился весь 
						// массив (последняя триада неполная) - выполнить 
						// преобразование триады в четверку BASE64
						
						// Если триада получилась неполной, то выполнить недостающее
						// умножение
						Для ИндДополн=ИндБайтаВТриаде По 2 Цикл
							ЗначТриады = 256*ЗначТриады
						КонецЦикла;
						// Выполняем формирование четверок BASE64
						СтрЧетв = "";  // Значение триады перекодированной в четверку BASE64
						// Если четверка неполная (из последней неполной триады),
						// то дополнить ее знаками "="
						Для ИндЧетв=ИндБайтаВТриаде+1 По 3 Цикл
							Остат = ЗначТриады % 64;
							СтрЧетв = "=" + СтрЧетв;
							ЗначТриады = (ЗначТриады - Остат) / 64
						КонецЦикла;
						// Выписываем символы BASE64 в четверку
						Для ИндЧетв=0 По ИндБайтаВТриаде Цикл
							Остат = ЗначТриады % 64;
							СтрЧетв = Сред(АлфавитBase64, Остат+1, 1) + СтрЧетв;
							ЗначТриады = (ЗначТриады - Остат) / 64
						КонецЦикла;
						// Записать четверку в строку результата
						Если РазСтрЧерез = 0 Тогда
							// Не разделяем результат на строки
							СтрокаBase64 = СтрокаBase64 + СтрЧетв
						Иначе
							// Разделяем на подстроки по РазСтрЧерез символов
							
							// Завершить предыдущую подстроку
							Если ДлТекущПодстроки >= РазСтрЧерез Тогда
								СтрокаBase64 = СтрокаBase64 + Символ(13) + Символ(10);
								ДлТекущПодстроки = 0
							КонецЕсли;
							// Добавить четверку в подстроку
							ДлСтрЧетв = СтрДлина(СтрЧетв);  // Должно быть всегда 4 символа
							Если ДлТекущПодстроки + ДлСтрЧетв <= РазСтрЧерез Тогда
								СтрокаBase64 = СтрокаBase64 + СтрЧетв;
								ДлТекущПодстроки = ДлТекущПодстроки + ДлСтрЧетв;
							Иначе
								Для ИндЧетв=1 По ДлСтрЧетв Цикл
									Если ДлТекущПодстроки >= РазСтрЧерез Тогда
										СтрокаBase64 = СтрокаBase64 + Символ(13) + Символ(10);
										ДлТекущПодстроки = 0
									КонецЕсли;
									СтрокаBase64 = СтрокаBase64 + Сред(СтрЧетв, ИндЧетв, 1);
									ДлТекущПодстроки = ДлТекущПодстроки + 1
								КонецЦикла
							КонецЕсли
						КонецЕсли;
						
						ЗначТриады = 0;
						ИндБайтаВТриаде = 0
					КонецЕсли
				Иначе
					// Ошибка: Значение элемента массива вне диапазона 0.255
					ИндексБайтаОшибки = Инд;
					СообщОшиб = "Значение элемент массива выходит за границы байта (0..255)";
				    Прервать
				КонецЕсли
			КонецЦикла
		Иначе
			// Ошибка: Неверное значение параметра РазСтрЧерез
			СообщОшиб = "Неверно указан интервал вставки разрыва строки"
		КонецЕсли
	Иначе
		// Ошибка: Неверный тип исходного массива
		СообщОшиб = "Неверный тип исходного массива"
	КонецЕсли;
	// Окончательно определить, удалось ли выполнить преобразование
	Если СообщОшиб = "" Тогда
		Рез = Истина
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Преобразует строку формата BASE64 в массив байт.
//
// Параметры:
//  СтрокаBase64 - текстовая строка в формате BASE64, которую нужно преобразовать;
//  МассивБайт - одномерный массив числовых значений (каждое число 0..255), в который
//               помещается результат преобразований (содержимое строки СтрокаBase64)
//  НомерСимволаОшибки - если возникли ошибки преобразования, то здесь возвращается
//                       номер символа в строке СтрокаBase64 (нумерация с 1), на котором 
//                       прервалось преобразование (если 0, то ошибка не в символах
//                       исходной строки)
//  СообщОшиб - если возникли ошибки преобразования, то здесь возвращается текстовое 
//              сообщение о возникшей ошибке.
// Возврат:
//  Истина, если преобразование выполнено и Ложь, если возникли ошибки (в этом
//  случае информация об ошибке возвращается в соответствующих параметрах)
//
Функция СтрокаBase64ВМассивБайт(СтрокаBase64, МассивБайт, НомерСимволаОшибки, СообщОшиб) Экспорт

	АлфавитBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	
	Рез = Ложь;
	НомерСимволаОшибки = 0;
	СообщОшиб = "";
	
	Если ТипЗнч(СтрокаBase64) = Тип("Строка") Тогда
		СтрокаBase64Длина = СтрДлина(СтрокаBase64);
		МассивБайт = Новый Массив;
		ЗначТриады = 0;   // Число, выражающее значение всех 3-х байтов триады и 4-х символов четверки
		ИндСимвВЧетв = 0; // Индекс (1..4) текущего считанного символа в текущей четверке
		                  // (с учетом символов "=")
		КолЗнакСимвВЧетв = 0; // Кол-во значащих символов в текущей четверке (без учета символов "=")
		Для Инд=1 По СтрокаBase64Длина Цикл
			ИсхСимв = Сред(СтрокаBase64, Инд, 1);
			Если (ИсхСимв = " ") ИЛИ (ИсхСимв = Символы.Таб) ИЛИ 
				 (ИсхСимв = Символы.ВК) ИЛИ (ИсхСимв = Символы.ПС) Тогда
				// Пропускаем символы-разделители
			Иначе
				Если ИсхСимв = "=" Тогда
					// Завершающие символы - их может быть не более 2-х в самом конце строки
					Если (Инд=СтрокаBase64Длина-1) ИЛИ (Инд=СтрокаBase64Длина) Тогда
						// Пропускаем символы "="
						
						// Добавить очередное значение к текущей четверке
						ЗначТриады = 64*ЗначТриады + 0;
						ИндСимвВЧетв = ИндСимвВЧетв + 1;
					Иначе
						// Ошибка: Некорректный символ в исходной строке
						НомерСимволаОшибки = Инд;
						СообщОшиб = "Некорректный символ в исходной строке";
					    Прервать
					КонецЕсли
				Иначе
					// Определить, какое число закодировано текущим символом BASE64
					ЗначСимвBase64 = Найти(АлфавитBase64, ИсхСимв)-1;
					Если (ЗначСимвBase64 >= 0) И (ЗначСимвBase64 <= 63) Тогда
						// Значение в исходной строке верное
						
						// Добавить очередное значение к текущей четверке
						ЗначТриады = 64*ЗначТриады + ЗначСимвBase64;
						ИндСимвВЧетв = ИндСимвВЧетв + 1;
						КолЗнакСимвВЧетв = КолЗнакСимвВЧетв + 1
					Иначе
						// Ошибка: Неверное значение в исходной строке
						НомерСимволаОшибки = Инд;
						СообщОшиб = "Некорректный символ в исходной строке";
					    Прервать
					КонецЕсли
				КонецЕсли;
				// Если считали очередную черверку из исходной строки - выполнить преобразование
				Если ИндСимвВЧетв >= 4  Тогда
					// Определить байты массива назначения, составляющие триаду, в которую 
					// перекодировалась четверка
					Остат = ЗначТриады % 256;
					Байт2 = Остат;
					ЗначТриады = (ЗначТриады - Остат) / 256;
					Остат = ЗначТриады % 256;
					Байт1 = Остат;
					ЗначТриады = (ЗначТриады - Остат) / 256;
					Остат = ЗначТриады % 256;
					Байт0 = Остат;
					Если КолЗнакСимвВЧетв >= 2 Тогда
 						МассивБайт.Добавить(Байт0);
						Если КолЗнакСимвВЧетв >= 3 Тогда
							МассивБайт.Добавить(Байт1);
							Если КолЗнакСимвВЧетв >= 4 Тогда
								МассивБайт.Добавить(Байт2)
							КонецЕсли
						КонецЕсли
					КонецЕсли;
					// Перейти к считыванию следующей четверки
					ЗначТриады = 0;
					ИндСимвВЧетв = 0;
					КолЗнакСимвВЧетв = 0;
				КонецЕсли
			КонецЕсли
		КонецЦикла;
		// Проверить, нет ли незавершенной четверки
		Если ИндСимвВЧетв > 0 Тогда
			Если СообщОшиб = "" Тогда
				// Ошибка: Последняя четверка символов не завершена
				СообщОшиб = "Последняя четверка символов не завершена"
			КонецЕсли
		КонецЕсли
	Иначе
		// Ошибка: Неверный тип исходной строки
		СообщОшиб = "Неверный тип исходной строки"
	КонецЕсли;
	// Окончательно определить, удалось ли выполнить преобразование
	Если СообщОшиб = "" Тогда
		Рез = Истина
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Преобразует массив чисел из объекта COMSafeArray (BLOB-поле с байтами 
// графического формата) в объект класса Картинка
//
// Параметры:
//  ОбъектCOMSafeArray - ссылка на массив чисел типа COMSafeArray
//                       (размерность массива должна быть 1)
//                       Элементы массива - это набор байт одного из поддерживаемых
//                       классом Картинка графических форматов
// Возврат:
//  Ссылка на объект типа Картинка или Неопределено, если операцию выполнить не удалось
//
Функция COMSafeArray_ВКартинку(ОбъектCOMSafeArray) Экспорт
	
	Рез = Неопределено;
	
	Если ТипЗнч(ОбъектCOMSafeArray) = Тип("COMSafeArray") И
		 (ОбъектCOMSafeArray.GetDimensions() = 1) И
		 (ОбъектCOMSafeArray.GetLength(0) > 0) Тогда
		Попытка
			Массив = ОбъектCOMSafeArray.Выгрузить();
			СтрокаBase64 = "";
			ИндексБайтаОшибки = 0;
			СообщОшиб = "";
			Если МассивБайтВСтрокуBase64(Массив, 0, СтрокаBase64, ИндексБайтаОшибки, СообщОшиб) Тогда
				ДвоичДанн = Base64Значение(СтрокаBase64);
				Картинка = Новый Картинка(ДвоичДанн);
				Рез = Картинка
			КонецЕсли
		Исключение
		
		КонецПопытки
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Преобразует объект типа Картинка в 
// массив байт графического формата в виде объекта COMSafeArray
//
// Параметры:
//  Картинка - ссылка на объект типа Картинка
// Возврат:
//  Ссылка на массив чисел типа COMSafeArray с байтами графического формата картинки
//  или Неопределено, если преобразование не выполнено
//
Функция Картинка_ВCOMSafeArray(Картинка) Экспорт
	
	Рез = Неопределено;
	
	Если (ТипЗнч(Картинка) = Тип("Картинка")) И
		 (Картинка.Вид <> ВидКартинки.Пустая) Тогда
		Попытка
			ДвоичДанн = Картинка.ПолучитьДвоичныеДанные();
			СтрокаBase64 = Base64Строка(ДвоичДанн);
			Массив = Новый Массив;
			НомерСимволаОшибки = 0;
			СообщОшиб = "";
			Если СтрокаBase64ВМассивБайт(СтрокаBase64, Массив, НомерСимволаОшибки, СообщОшиб) Тогда
				Рез = Новый COMSafeArray(Массив, "VT_UI1", Массив.Количество())
			КонецЕсли		
		Исключение
			
		КонецПопытки
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции


Из этой затеи ничего не вышло, так как функции «МассивБайтВСтрокуBase64» и «СтрокаBase64ВМассивБайт» оказались слишком медленными, и преобразование картинок размером даже в несколько десятков килобайт выполнялось десятки секунд. Я так и не докопался до причины – можно ли было улучшить мой алгоритм или все упиралось в ограничения платформы (исполнение через интерпретатор + необходимость работы со строками через функцию «Сред» (т.к. нет индексного доступа к символам строки, т.к. типа данных «Символ» тоже нет), а также функцию «Найти»). В общем, в конечном итоге решил делать обмен данными «как все» «COMSafeArray»«Файл» «Картинка». Для сохранения в файл и загрузки из файла использовал также по рекомендациям из Интернета – COM-объект «ADO.Stream». В результате, преобразование выполняется практически мгновенно. Вот этот код:
COMSafeArray_ВКартинку
Картинка_ВCOMSafeArray

Исходный текст
// Преобразует массив чисел из объекта COMSafeArray (BLOB-поле с байтами 
// графического формата) в объект класса Картинка
//
// Параметры:
//  ОбъектCOMSafeArray - ссылка на массив чисел типа COMSafeArray
//                       (размерность массива должна быть 1)
//                       Элементы массива - это набор байт одного из поддерживаемых
//                       классом Картинка графических форматов
// Возврат:
//  Ссылка на объект типа Картинка или Неопределено, если операцию выполнить не удалось
//
Функция COMSafeArray_ВКартинку(ОбъектCOMSafeArray) Экспорт
	
	Рез = Неопределено;
	
	Если ТипЗнч(ОбъектCOMSafeArray) = Тип("COMSafeArray") И
		 (ОбъектCOMSafeArray.GetDimensions() = 1) И
		 (ОбъектCOMSafeArray.GetLength(0) > 0) Тогда
		Попытка
			ИмяВремФайла = ПолучитьИмяВременногоФайла();
			// Сохранить BLOB во временный файл
			StreamOut = Новый COMОбъект("ADODB.Stream");
    		StreamOut.Type = ADO.StreamTypeEnum.adTypeBinary; // В потоке двоичные данные
    		StreamOut.Mode = ADO.ConnectModeEnum.adModeReadWrite; // Режим работы потока - на чтение и запись
		    StreamOut.Open();
		    StreamOut.Write(ОбъектCOMSafeArray); 
		    StreamOut.SaveToFile(ИмяВремФайла, ADO.SaveOptionsEnum.adSaveCreateOverWrite); 
    		StreamOut.Close();			
			// Считать картинку из временного файла
			Картинка = Новый Картинка(ИмяВремФайла);
			Рез = Картинка;
			// Удалить временный файл
			УдалитьФайлы(ИмяВремФайла)
		Исключение
		
		КонецПопытки
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Преобразует объект типа Картинка в 
// массив байт графического формата в виде объекта COMSafeArray
//
// Параметры:
//  Картинка - ссылка на объект типа Картинка
// Возврат:
//  Ссылка на массив чисел типа COMSafeArray с байтами графического формата картинки
//  или Неопределено, если преобразование не выполнено
//
Функция Картинка_ВCOMSafeArray(Картинка) Экспорт
	
	Рез = Неопределено;
	
	Если (ТипЗнч(Картинка) = Тип("Картинка")) И
		 (Картинка.Вид <> ВидКартинки.Пустая) Тогда
		Попытка
			ИмяВремФайла = ПолучитьИмяВременногоФайла();
			// Сохранить картинку во временный файл
			Картинка.Записать(ИмяВремФайла);
			// Считать BLOB из временного файла
			StreamIn = Новый COMОбъект("ADODB.Stream");
    		StreamIn.Type = ADO.StreamTypeEnum.adTypeBinary; // В потоке двоичные данные
    		StreamIn.Mode = ADO.ConnectModeEnum.adModeReadWrite;  // Режим работы потока - на чтение и запись
		    StreamIn.Open();
			StreamIn.LoadFromFile(ИмяВремФайла); 
		    Рез = StreamIn.Read(); 
    		StreamIn.Close();			
			// Удалить временный файл
			УдалитьФайлы(ИмяВремФайла)
		Исключение
			
		КонецПопытки
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции


Замечание по интерфейсу программы.


Важно обеспечить согласованность состояния модели данных и интерфейса ее отображения. Т.е. мы выполняем какую-то команду, после которой модель данных переходит в новое состояние, и нужно это новое состояние показать в интерфейсе. Программный код, реализующий подобные действия логически достаточно простой, но порой объемный и соответственно занудный. Платформа 1С ценна в том числе и тем, что берет всю подобную работу на себя и делает ее незаметной для разработчика конфигурации. Однако в нашем случае (работа со сторонней базой), многое придется делать самостоятельно. Так, например, поддержание согласованности модели и интерфейса для такой сущности как дерево групп является довольно «замороченной» задачей. В программе, дерево групп используется в главном окне программы (левая колонка), а также в диалоге выбора группы из дерева групп. Также нужно обеспечить согласованность для списка, отображающего содержимое группы (правая колонка главного окна программы).
В данном тестовом примере, подпрограммы, позволяющие осуществить увязку модели данных и интерфейса отображения для «дерева групп» и «списка содержимого группы» выделены в отдельный блок, т.е. обособлены и переносимы. В принципе код достаточно типичен и универсален – для применения в своих программах требуются лишь небольшие доработки, выполняемые «на автомате», не задумываясь.
Эти подпрограммы выглядят так:
Исходный текст
///////////////////////////////////////////////////////////////////////////////////////////////////
//
// П О Д П Р О Г Р А М М Ы    Д Л Я    О Т О Б Р А Ж Е Н И Я    Д А Н Н Ы Х    В    И Н Т Е Р Ф Е Й С Е
//
///////////////////////////////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////////////
// Подпрограммы для отображения дерева групп справочника (ДеревоГрупп)

// Инициализирует дерево групп справочника (подготовка к работе). 
// Создает необходимые колонки.
//
// Параметры:
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//
Процедура ДеревоГрупп_Инициализация(ДеревоГрупп) Экспорт
	
	Если ДеревоГрупп <> Неопределено Тогда
		// Сформировать описания типов данных, требующихся для колонок
		КЧ = Новый КвалификаторыЧисла(10, 0);
		КС = Новый КвалификаторыСтроки();
		МассивТипов = Новый Массив;
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Структура"));
		ОписаниеТиповСтруктуры = Новый ОписаниеТипов(МассивТипов);    // Тип "Структура"		
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Число"));
		ОписаниеТиповЧисла = Новый ОписаниеТипов(МассивТипов, , ,КЧ); // Тип "Число"
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Булево"));
		ОписаниеТиповБулевы = Новый ОписаниеТипов(МассивТипов);       // Тип "Булево"
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Строка"));
		ОписаниеТиповСтроки = Новый ОписаниеТипов(МассивТипов, , КС); // Тип "Строка"
	
		// Сформировать колонки
		ДеревоГрупп.Колонки.Очистить();
		ДеревоГрупп.Колонки.Добавить("Объект", ОписаниеТиповСтруктуры);
		ДеревоГрупп.Колонки.Добавить("Картинка", ОписаниеТиповЧисла);
		ДеревоГрупп.Колонки.Добавить("ПодгруппыЗагружены", ОписаниеТиповБулевы);
		ДеревоГрупп.Колонки.Добавить("Наименование", ОписаниеТиповСтроки)
	КонецЕсли
	
КонецПроцедуры

// Выполняет поиск элемента поддерева дерева групп по идентификатору группы.
//
// Параметры:
//  КорневаяСтрока - строка дерева групп, от которой начинается поиск
//  Ид - идентификатор группы, которую нужно найти в дереве;
// Возврат:
//  Строка дерева групп, соответствующая искомой группе и Неопределено, если группа не
//  найдена
//
Функция ДеревоГрупп_НайтиСтрокуУзлаДереваГруппПоИд(КорневаяСтрока, Ид) Экспорт
	
	Рез = Неопределено;
	
	Если КорневаяСтрока <> Неопределено Тогда
		Если (КорневаяСтрока.Объект <> Неопределено) И (КорневаяСтрока.Объект.GRID = Ид) Тогда
			Рез = КорневаяСтрока
		Иначе
			Для Каждого СтрокаДЗ Из	КорневаяСтрока.Строки Цикл
				Если (СтрокаДЗ.Объект <> Неопределено) И (СтрокаДЗ.Объект.GRID = Ид) Тогда
					Рез = СтрокаДЗ;
					Прервать
				Иначе
					Рез = ДеревоГрупп_НайтиСтрокуУзлаДереваГруппПоИд(СтрокаДЗ, Ид);
					Если Рез <> Неопределено Тогда
						Прервать
					КонецЕсли
				КонецЕсли
			КонецЦикла
		КонецЕсли
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Выполняет поиск элемента дерева групп по идентификатору группы.
//
// Параметры:
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//  Ид - идентификатор группы, которую нужно найти в дереве;
//  СтрокаНачалаПоиска - строка дерева групп, с в которой предпочтительно начинать
//  поиск объекта для того, чтобы найти его быстрее (если там не найдено, то будет 
//  запущен поиск по всему дереву);
// Возврат:
//  Строка дерева групп, соответствующая искомой группе и Неопределено, если группа не
//  найдена
//
Функция ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, Ид, СтрокаНачалаПоиска=Неопределено) Экспорт
	
	Рез = Неопределено;
	
	Если СтрокаНачалаПоиска <> Неопределено Тогда
		// Пробуем искать от специально указанной строки
		Рез = ДеревоГрупп_НайтиСтрокуУзлаДереваГруппПоИд(СтрокаНачалаПоиска, Ид)
	КонецЕсли;
	
	Если Рез = Неопределено Тогда
		// Общий поиск по всему дереву
		Для Каждого СтрокаДЗ Из ДеревоГрупп.Строки Цикл
			Если (СтрокаДЗ.Объект <> Неопределено) И (СтрокаДЗ.Объект.GRID = Ид) Тогда
				Рез = СтрокаДЗ
			Иначе
				Рез = ДеревоГрупп_НайтиСтрокуУзлаДереваГруппПоИд(СтрокаДЗ, Ид)
			КонецЕсли;
			Если Рез <> Неопределено Тогда
				Прервать
			КонецЕсли
		КонецЦикла
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Выполняет поиск элемента дерева групп по объекту, описывающему группу.
//
// Параметры:
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//  Объект - объект класса ObjGroup;
//  СтрокаНачалаПоиска - строка дерева групп, с в которой предпочтительно начинать
//  поиск объекта для того, чтобы найти его быстрее (если там не найдено, то будет 
//  запущен поиск по всему дереву);
// Возврат:
//  Строка дерева групп, соответствующая искомой группе и Неопределено, если группа не
//  найдена
//
Функция ДеревоГрупп_НайтиСтрокуДереваГруппПоОбъекту(ДеревоГрупп, Объект, СтрокаНачалаПоиска=Неопределено) Экспорт
	
	Рез = Неопределено;
	
	Если Объект <> Неопределено Тогда
		ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, Объект.GRID, СтрокаНачалаПоиска)
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции

// Выполняет проверку того, является ли группа ИдРодит иерархическим
// родителем (необязательно непосредственным, а возможно через несколько
// промежуточных групп) группы Ид. Проверки выполняются в дереве ДеревоГрупп.
//
// Параметры:
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//  Ид - идентификатор группы проверяемой как потомок;
//  ИдРодит - идентификатор группы проверяемой как родитель;
// Возврат:
//  Истина, если граппа Ид вложена в группу ИдРодит иерархически и Ложь,
//  если группа Ид не вложена в группу ИдРодит
//
Функция ДеревоГрупп_ИерархичПотомокВДеревеГрупп(ДеревоГрупп, Ид, ИдРодит) Экспорт
	
	Рез = Ложь;

	Если (Ид > 0) И (ИдРодит >= 0) И (Ид <> ИдРодит) Тогда
		Если ИдРодит = 0 Тогда
			// Элемент Ид точно вложен в корневой
			Рез = Истина
		Иначе
			// Выполнить полноценный поиск
			СтрокаДЗ = ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, Ид);
			Если СтрокаДЗ <> Неопределено Тогда
				// Поднимаемся к вершине дерева, пока не найдем группу ИдРодит
				// или не дойдем до самого верха
				СтрокаДЗ = СтрокаДЗ.Родитель;
				Пока СтрокаДЗ <> Неопределено Цикл
					Если СтрокаДЗ.Объект.GRID = ИдРодит Тогда
						// Да, установили что группа Ид вложена в группу ИдРодит 
						Рез = Истина;
						Прервать
					КонецЕсли;
					СтрокаДЗ = СтрокаДЗ.Родитель
				КонецЦикла
			КонецЕсли
		КонецЕсли
	КонецЕсли;
	
    Возврат Рез
	
КонецФункции

// Выполняет обновление информации в дереве групп для объекта Объект.
// Если изменилось название узла, то оно будет изменено в дереве. Если объект
// новый, то он будет добавлен. Если объект перемещен в другую группу (изменился
// родитель), то в дереве также будет сделано перемещение.
//
// Параметры:
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//  Объект - объект класса ObjGroup, изменения в котором нужно отобразаить;
//  СтрокаНачалаПоиска - строка дерева групп, с которой предпочтительно начинать
//  поиск объекта для того, чтобы найти его быстрее (если там не найдено, то будет 
//  запущен поиск по всему дереву);
// Возврат:
//  Строка дерева групп, в которой в конечном итоге находится объект Объект или
//  Неопределено, если такую строку установить не удалось
//
Функция ДеревоГрупп_ОбновитьСтрокуДереваГрупп(ДеревоГрупп, Объект, СтрокаНачалаПоиска=Неопределено) Экспорт
	
	СтрокаОбъекта = Неопределено;  // Строка дерева, в которой будет находиться объект в конечном итоге
	
	Если (ДеревоГрупп <> Неопределено) И (Объект <> Неопределено) Тогда
		СтрокаДЗ = ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, Объект.GRID, СтрокаНачалаПоиска);
		Если СтрокаДЗ = Неопределено Тогда
			// Строки объекта в дереве нет - возможно ее нужно добавить
			СтрокаРодителя = ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, Объект.PGRID, СтрокаНачалаПоиска);
			Если СтрокаРодителя <> Неопределено Тогда
				СтрокаДЗ = СтрокаРодителя.Строки.Добавить();
				СтрокаДЗ.Объект = Объект;
				СтрокаДЗ.Картинка = 0;
				СтрокаДЗ.ПодгруппыЗагружены = Ложь;
				СтрокаДЗ.Наименование = Объект.GRName;
				
				СтрокаОбъекта = СтрокаДЗ
			КонецЕсли
		Иначе
			// Нашли строку объекта в дереве
			СтрокаРодителя = СтрокаДЗ.Родитель;
			Если ((СтрокаРодителя = Неопределено) И (Объект.PGRID < 0)) ИЛИ
				 ((СтрокаРодителя <> Неопределено) И (СтрокаРодителя.Объект.GRID = Объект.PGRID)) Тогда
				// Перемещать в дереве объект не нужно
				СтрокаДЗ.Объект = Объект;
				СтрокаДЗ.Наименование = Объект.GRName;
				
				СтрокаОбъекта = СтрокаДЗ
			Иначе
				// Нужно выполнить перемещение объекта в дереве
				Если СтрокаРодителя <> Неопределено Тогда
					СтрокаРодителя.Строки.Удалить(СтрокаДЗ)
				КонецЕсли;
				СтрокаРодителя = ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, Объект.PGRID);
				Если СтрокаРодителя <> Неопределено Тогда
					СтрокаДЗ = СтрокаРодителя.Строки.Добавить();
					СтрокаДЗ.Объект = Объект;
					СтрокаДЗ.Картинка = 0;
					СтрокаДЗ.ПодгруппыЗагружены = Ложь;
					СтрокаДЗ.Наименование = Объект.GRName;
				
					СтрокаОбъекта = СтрокаДЗ
				КонецЕсли
			КонецЕсли
		КонецЕсли
	КонецЕсли;
	
	Возврат СтрокаОбъекта
	
КонецФункции

// Удаляет из дерева элемент, соответствующий объекту Объект 
// (и все его дочерние узлы).
//
// Параметры:
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//  Объект - объект класса ObjGroup, узел дерева для которого нужно удалить;
//  СтрокаНачалаПоиска - строка дерева групп, с в которой предпочтительно начинать
//  поиск объекта для того, чтобы найти его быстрее (если там не найдено, то будет 
//  запущен поиск по всему дереву);
// Возврат:
//  Строка дерева групп, на которую предпочительно перевести фокус ввода после
//  удаления строки объекта Объект и Неопределено, если такая строка не определена
//
Функция ДеревоГрупп_УдалитьСтрокуДереваГрупп(ДеревоГрупп, Объект, СтрокаНачалаПоиска=Неопределено) Экспорт
	
	СтрокаСледующОбъекта = Неопределено;
	
	Если (ДеревоГрупп <> Неопределено) И (Объект <> Неопределено) Тогда
		СтрокаОбъекта = ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, Объект.GRID, СтрокаНачалаПоиска);
		Если СтрокаОбъекта <> Неопределено Тогда
			СтрокаРодителя = СтрокаОбъекта.Родитель;
			Если СтрокаРодителя <> Неопределено Тогда
				ИндексСтрокиОбъекта = СтрокаРодителя.Строки.Индекс(СтрокаОбъекта);
				СтрокаРодителя.Строки.Удалить(СтрокаОбъекта);
				РодительКолич = СтрокаРодителя.Строки.Количество();
				Если РодительКолич > 0 Тогда
					Если ИндексСтрокиОбъекта < РодительКолич Тогда
						СтрокаСледующОбъекта = СтрокаРодителя.Строки[ИндексСтрокиОбъекта]
					Иначе
						СтрокаСледующОбъекта = СтрокаРодителя.Строки[РодительКолич-1]
					КонецЕсли
				Иначе
					СтрокаСледующОбъекта = СтрокаРодителя
				КонецЕсли 
			КонецЕсли
		КонецЕсли
	КонецЕсли;
	
	Возврат СтрокаСледующОбъекта
	
КонецФункции

// Процедура выполняет загрузку поддерева в указанный узел дерева групп.
//
// Параметры:
//  ПодкБазе - текущее подключение к базе;
//  ОбъектСообщ - объект для вывода сообщений в случае возникновения ошибок при загрузке;
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//  ИдРодит - идентификатор родительской группы, для которой нужно загрузить содержимое;
//  СтрокаРодит - строка дерева значений ДеревоГрупп, которая предположительно содержит
//                родительскую группу и в которую соответственно нужно загружать поддерево - 
//                может быть указана для ускорения операции вставки (если не указана, то
//                строка родительской группы будет искаться по всему дереву);
//  ГлубинаЗагр - максимальное кол-во уровней вложенности, которое можно загрузить,
//                начиная от узла ИдРодит (если =0, то загружать все поддерево целиком -
//                все уровни вложенности).
//
Процедура ДеревоГрупп_ЗагрузитьУзелДереваГрупп(ПодкБазе, ОбъектСообщ, ДеревоГрупп, ИдРодит, СтрокаРодит=Неопределено, ГлубинаЗагр=0) Экспорт
	
	Если (ДеревоГрупп <> Неопределено) И (ИдРодит >= 0) И (ГлубинаЗагр >= 0) Тогда
		// Определить строку дерева, в которую нужно будет делать вставку
		КорневаяСтрока = Неопределено;
		Если (СтрокаРодит <> Неопределено) И (СтрокаРодит.Объект.GRID = ИдРодит) Тогда
			КорневаяСтрока = СтрокаРодит
		КонецЕсли;
		Если КорневаяСтрока = Неопределено Тогда
			КорневаяСтрока = ДеревоГрупп_НайтиСтрокуДереваГруппПоИд(ДеревоГрупп, ИдРодит, СтрокаРодит)	
		КонецЕсли;
	
		Если КорневаяСтрока	<> Неопределено Тогда
			// Выполнить загрузку подгрупп в корневую строку
			КорневаяСтрока.ПодгруппыЗагружены = Ложь;
			КорневаяСтрока.Строки.Очистить();
		
			// Загрузить подгруппы текущей группы из базы
			НаборГруппУзла = ObjSet_Конструктор(Проч.ClassIDs.CLN_GROUP);
			Если ПодкБазе_SelectGroups(ПодкБазе, ИдРодит, НаборГруппУзла) Тогда
				// Добавлять выбранные элементы в дерево и одновременно
				// для каждого такого элемента загружать его поддерево рекурсивно,
				// если не исчерпана глубина обхода ГлубинаЗагр
				Для Инд=0 По ObjSet_GetCount(НаборГруппУзла)-1 Цикл
					Группа = ObjSet_GetItemByIndex(НаборГруппУзла, Инд);
					
					СтрокаДЗ = КорневаяСтрока.Строки.Добавить();
					СтрокаДЗ.Объект = Группа;
					СтрокаДЗ.Картинка = 0;
					СтрокаДЗ.ПодгруппыЗагружены = Ложь;
					СтрокаДЗ.Наименование = Группа.GRName;
				КонецЦикла;				
				КорневаяСтрока.ПодгруппыЗагружены = Истина;
				// Загрузить поддеревья
				Если ГлубинаЗагр <> 1 Тогда
					Для Каждого СтрокаДЗ Из КорневаяСтрока.Строки Цикл
						// Загрузить поддерево узла
						Если ГлубинаЗагр = 0 Тогда
							ДеревоГрупп_ЗагрузитьУзелДереваГрупп(ПодкБазе, ОбъектСообщ, ДеревоГрупп, 
																 СтрокаДЗ.Объект.GRID, СтрокаДЗ, ГлубинаЗагр)
						ИначеЕсли ГлубинаЗагр > 1 Тогда
							ДеревоГрупп_ЗагрузитьУзелДереваГрупп(ПодкБазе, ОбъектСообщ, ДеревоГрупп, 
																 СтрокаДЗ.Объект.GRID, СтрокаДЗ, ГлубинаЗагр-1)
						КонецЕсли
					КонецЦикла
				КонецЕсли
			Иначе
				// Вывести сообщение об ошибке в список сообщений
				Сообщения_Добавить(ОбъектСообщ, Проч.ТИПСООБЩ_УРОВЕНЬ1, "Ошибка загрузки дерева групп", Проч.СТАТСООБЩ_ОШИБКА, ПодкБазе_GetErrorMsg(ПодкБазе))
			КонецЕсли;
			ObjSet_Деструктор(НаборГруппУзла)
		КонецЕсли
	КонецЕсли
	
КонецПроцедуры

// Процедура выполняет загрузку дерева групп из базы, начиная с корневого элемента.
// Существующее на момент загрузки дерево удаляется.
//
// Параметры:
//  ПодкБазе - текущее подключение к базе;
//  ОбъектСообщ - объект для вывода сообщений в случае возникновения ошибок при загрузке;
//  ДеревоГрупп - ДеревоЗначений, содержащее дерево групп;
//  ГлубинаЗагр - максимальное кол-во уровней вложенности, которое можно загрузить,
//                начиная от корневого ИдРодит (если =0, то загружать все дерево целиком -
//                все уровни вложенности).
// Возврат:
//  Корневая строка дерева групп или Неопределено, если дерево групп не удалось создать
//
//
Функция ДеревоГрупп_ЗагрузитьДеревоГрупп(ПодкБазе, ОбъектСообщ, ДеревоГрупп, ГлубинаЗагр=0) Экспорт
	
	КорневаяСтрока = Неопределено;
	Если (ДеревоГрупп <> Неопределено) И (ГлубинаЗагр >= 0) Тогда	
		// Очистить дерево значений ДеревоГрупп
		ДеревоГрупп.Строки.Очистить();	
		// Добавить корневой элемент
		КорневаяГруппа = ObjGroup_Конструктор();
		ObjGroup_SetAttr(КорневаяГруппа, 0, -1, "Контакты");
		СтрокаДЗ = ДеревоГрупп.Строки.Добавить();
		СтрокаДЗ.Объект = КорневаяГруппа;
		СтрокаДЗ.Картинка = 0;
		СтрокаДЗ.ПодгруппыЗагружены = Ложь;
		СтрокаДЗ.Наименование = КорневаяГруппа.GRName;
		// Загрузить значения, вложенные в корневой элемент (не более 2-х уровней вложенности)
		ДеревоГрупп_ЗагрузитьУзелДереваГрупп(ПодкБазе, ОбъектСообщ, ДеревоГрупп, 
											 КорневаяГруппа.GRID, СтрокаДЗ, ГлубинаЗагр);
		КорневаяСтрока = СтрокаДЗ
	КонецЕсли;
	
	Возврат КорневаяСтрока
	
КонецФункции


////////////////////////////////////////////////////////////////////////////////
// Подпрограммы для отображения таблицы содержимого группы справочника (ТаблицаЭлементов)

// Инициализирует таблицу содержимого группы справочника - таблица подгрупп и карточек
// (готовит таблицу к работе). Создает необходимые колонки.
//
// Параметры:
//  ТаблицаЭлементов - ТаблицаЗначений, содержащая таблицу содержимого группы;
//
Процедура ТаблицаЭлементов_Инициализация(ТаблицаЭлементов) Экспорт
	
	Если ТаблицаЭлементов <> Неопределено Тогда
		// Сформировать описания типов данных, требующихся для колонок
		КЧ = Новый КвалификаторыЧисла(10, 0);
		КС = Новый КвалификаторыСтроки();
		МассивТипов = Новый Массив;
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Структура"));
		ОписаниеТиповСтруктуры = Новый ОписаниеТипов(МассивТипов);    // Тип "Структура"
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Число"));
		ОписаниеТиповЧисла = Новый ОписаниеТипов(МассивТипов, , ,КЧ); // Тип "Число"
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Булево"));
		ОписаниеТиповБулевы = Новый ОписаниеТипов(МассивТипов);       // Тип "Булево"
		МассивТипов.Очистить();
		МассивТипов.Добавить(Тип("Строка"));
		ОписаниеТиповСтроки = Новый ОписаниеТипов(МассивТипов, , КС); // Тип "Строка"
	
		// Сформировать колонки
		ТаблицаЭлементов.Колонки.Очистить();
		ТаблицаЭлементов.Колонки.Добавить("Объект", ОписаниеТиповСтруктуры);
		ТаблицаЭлементов.Колонки.Добавить("Картинка", ОписаниеТиповЧисла);
		ТаблицаЭлементов.Колонки.Добавить("ЭтоГруппа", ОписаниеТиповБулевы);
		ТаблицаЭлементов.Колонки.Добавить("Наименование", ОписаниеТиповСтроки)		
	КонецЕсли
	
КонецПроцедуры

// Проверяет входит ли объект Объект в группу ИдРодитГр или нет.
//
// Параметры:
//  Объект - объект класса ObjGroup или ObjCard, который нужно проверить;
//  ИдРодитГр - идентификатор группы, на принадлежность которой проверяется объект;
// Возврат:
//  Истина, если объект Объект входит в группу ИдРодитГр и Ложь - в противном
//  случае
//
Функция ТаблицаЭлементов_ОбъектИзГруппы(Объект, ИдРодитГр) Экспорт
	
	Рез = Ложь;
	Если (Объект <> Неопределено) И (ИдРодитГр >= 0) Тогда
		Рез = ((Объект.ClassID = Проч.ClassIDs.CLN_GROUP) И (Объект.PGRID = ИдРодитГр)) ИЛИ
		      ((Объект.ClassID = Проч.ClassIDs.CLN_CARD) И (Объект.GRID = ИдРодитГр))
	КонецЕсли;
		  
	Возврат Рез

КонецФункции

// Проверяет, есть ли связь объекта Объект со строкой СтрокаЭлемента или нет.
//
// Параметры:
//  Объект - объект класса ObjGroup или ObjCard, который нужно проверить;
//  СтрокаЭлемента - строка таблицы, которую нужно проверить;
// Возврат:
//  Истина, если объект Объект описывается строкой СтрокаЭлемента и Ложь - в противном
//  случае
//
Функция ТаблицаЭлементов_ОбъектИзСтроки(Объект, СтрокаЭлемента) Экспорт
	
	Рез = Ложь;
	Если (Объект <> Неопределено) И (СтрокаЭлемента <> Неопределено) Тогда
		Рез = ((Объект.ClassID = Проч.ClassIDs.CLN_GROUP) И (СтрокаЭлемента.ЭтоГруппа = Истина) И (Объект.GRID = СтрокаЭлемента.Объект.GRID)) ИЛИ
		      ((Объект.ClassID = Проч.ClassIDs.CLN_CARD) И (СтрокаЭлемента.ЭтоГруппа = Ложь) И (Объект.CDID = СтрокаЭлемента.Объект.CDID))
	КонецЕсли;
		  
	Возврат Рез
	
КонецФункции

// Ищет в таблице строку, связаную с объектом Объект. 
//
// Параметры:
//  ТаблицаЭлементов - ТаблицаЗначений, содержащее таблицу содержимого группы;
//  Объект - объект класса ObjGroup или ObjCard, строку которого нужно найти;
//  СтрокаЭлемента - строка таблицы, которая предположительно является результатом
//  функции и это просто нужно проверить (если не указана, то поиск строки
//  ведется по всей таблице);
// Возврат:
//  Строка таблицы, с которой связан указанный объект и Неопределено, если такая 
//  строка не определена
//
Функция ТаблицаЭлементов_НайтиСтрокуТаблицыЭлементовПоОбъекту(ТаблицаЭлементов, Объект, СтрокаЭлемента) Экспорт
	
	Рез = Неопределено;
	Если (ТаблицаЭлементов <> Неопределено) И (Объект <> Неопределено) Тогда
		Если (СтрокаЭлемента <> Неопределено) И
			 ТаблицаЭлементов_ОбъектИзСтроки(Объект, СтрокаЭлемента) Тогда
			// Да, СтрокаЭлемента - это строка объекта  
			Рез = СтрокаЭлемента
		Иначе
			// Выполнить поиск по каждой строке
			Для Каждого СтрокаТЗ Из ТаблицаЭлементов Цикл
				Если ТаблицаЭлементов_ОбъектИзСтроки(Объект, СтрокаТЗ) Тогда
					Рез = СтрокаТЗ;
					Прервать
				КонецЕсли
			КонецЦикла
		КонецЕсли
	КонецЕсли;
	
	Возврат Рез
	
КонецФункции
		
// Процедура выполняет загрузку содержимого указанной группы (подгрупп и карточек)
// в таблицу содержимого группы.
//
// Параметры:
//  ПодкБазе - текущее подключение к базе;
//  ОбъектСообщ - объект для вывода сообщений в случае возникновения ошибок при загрузке;
//  ТаблицаЭлементов - ТаблицаЗначений, содержащая таблицу содержимого группы;
//  ИдРодит - идентификатор родительской группы, для которой нужно загрузить содержимое;
//
Процедура ТаблицаЭлементов_ЗагрузитьТаблицуЭлементов(ПодкБазе, ОбъектСообщ, ТаблицаЭлементов, ИдРодит) Экспорт
	
	Если ТаблицаЭлементов <> Неопределено Тогда	
		// Очистить таблицу значений ТаблицаЭлементов
		ТаблицаЭлементов.Очистить();
		Если ИдРодит >= 0 Тогда
			// Пробуем загрузить вложенные группы
			НаборГруппУзла = ObjSet_Конструктор(Проч.ClassIDs.CLN_GROUP);
			Если ПодкБазе_SelectGroups(ПодкБазе, ИдРодит, НаборГруппУзла) Тогда
				// Добавлять выбранные элементы в таблицу
				Для Инд=0 По ObjSet_GetCount(НаборГруппУзла)-1 Цикл
					Группа = ObjSet_GetItemByIndex(НаборГруппУзла, Инд);
					
					СтрокаТаб = ТаблицаЭлементов.Добавить();
					СтрокаТаб.Объект = Группа;
					СтрокаТаб.Картинка = 0;
					СтрокаТаб.ЭтоГруппа = Истина;
					СтрокаТаб.Наименование = Группа.GRName;					
				КонецЦикла	
			Иначе
				// Вывести сообщение об ошибке в список сообщений
				Сообщения_Добавить(ОбъектСообщ, Проч.ТИПСООБЩ_УРОВЕНЬ1, "Ошибка загрузки групп в таблицу", Проч.СТАТСООБЩ_ОШИБКА, ПодкБазе_GetErrorMsg(ПодкБазе))
			КонецЕсли;
			ObjSet_Деструктор(НаборГруппУзла);
			
			// Пробуем загрузить вложенные карточки
			НаборКарточекУзла = ObjSet_Конструктор(Проч.ClassIDs.CLN_CARD);
			Если ПодкБазе_SelectCardsNames(ПодкБазе, ИдРодит, НаборКарточекУзла) Тогда
				// Добавлять выбранные элементы в таблицу
				Для Инд=0 По ObjSet_GetCount(НаборКарточекУзла)-1 Цикл
					Карточка = ObjSet_GetItemByIndex(НаборКарточекУзла, Инд);
				
					СтрокаТаб = ТаблицаЭлементов.Добавить();
					СтрокаТаб.Объект = Карточка;
					СтрокаТаб.Картинка = 1;
					СтрокаТаб.ЭтоГруппа = Ложь;
					СтрокаТаб.Наименование = Карточка.CDName;					
				КонецЦикла	
			Иначе
				// Вывести сообщение об ошибке в список сообщений
				Сообщения_Добавить(ОбъектСообщ, Проч.ТИПСООБЩ_УРОВЕНЬ1, "Ошибка загрузки карточек в таблицу", Проч.СТАТСООБЩ_ОШИБКА, ПодкБазе_GetErrorMsg(ПодкБазе))
			КонецЕсли;
			ObjSet_Деструктор(НаборГруппУзла)
		КонецЕсли
	КонецЕсли
	
КонецПроцедуры

// Выполняет обновление информации в таблице содержимого группы Объект.
// Если изменилось название объекта, то оно будет изменено в таблице. Если объект
// новый, то он будет добавлен. Если объект перемещен в другую группу (изменился
// родитель), то из текущей таблицы он также будет удален.
//
// Параметры:
//  ТаблицаЭлементов - ТаблицаЗначений, содержащее таблицу содержимого группы;
//  ИдРодитГр - идентификатор группы, содержимое которой загружено в таблицу;
//  Объект - объект класса ObjGroup, изменения в котором нужно отобразить;
//  СтрокаЭлемента - строка таблицы, которая предположительно описывает объект Объект
//  указывается для того, чтобы не выполнять поиск (если не указана, то поиск объекта
//  ведется по всей таблице);
// Возврат:
//  Строка таблицы, на которую нужно будет установить фокус (предположительно строка объекта)
//  Неопределено, если такую строку установить не удалось
//
Функция ТаблицаЭлементов_ОбновитьСтрокуТаблицыЭлементов(ТаблицаЭлементов, ИдРодитГр, Объект, СтрокаЭлемента=Неопределено) Экспорт
	
	СтрокаСледующОбъекта = Неопределено;
	
	Если (ТаблицаЭлементов <> Неопределено) И (ИдРодитГр >= 0) И (Объект <> Неопределено) Тогда
		СтрокаСледующОбъекта = ТаблицаЭлементов_НайтиСтрокуТаблицыЭлементовПоОбъекту(ТаблицаЭлементов, Объект, СтрокаЭлемента);
		Если СтрокаСледующОбъекта = Неопределено Тогда
			// Строки объекта в таблице нет - возможно ее нужно добавить
			Если ТаблицаЭлементов_ОбъектИзГруппы(Объект, ИдРодитГр) Тогда
				// Объект из этой группы - добавляем его в таблицу
				Если Объект.ClassID = Проч.ClassIDs.CLN_GROUP Тогда
					// Вставить новую группу перед 1-й карточкой
					// Найти место вставки
					Инд = 0;
					Пока Инд < ТаблицаЭлементов.Количество() Цикл
						СтрокаТЗ = ТаблицаЭлементов[Инд];
						Если НЕ СтрокаТЗ.ЭтоГруппа Тогда
							Прервать
						Иначе
							Инд = Инд + 1
						КонецЕсли
					КонецЦикла;
					// Выполнить вставку
					СтрокаТЗ = ТаблицаЭлементов.Вставить(Инд);
					СтрокаТЗ.Объект = Объект;
					СтрокаТЗ.Картинка = 0;
					СтрокаТЗ.ЭтоГруппа = Истина;
					СтрокаТЗ.Наименование = Объект.GRName;					
					
					СтрокаСледующОбъекта = СтрокаТЗ
				ИначеЕсли Объект.ClassID = Проч.ClassIDs.CLN_CARD Тогда	
					// Добавить новую карточку в конец таблицы
					СтрокаТЗ = ТаблицаЭлементов.Добавить();
					СтрокаТЗ.Объект = Объект;
					СтрокаТЗ.Картинка = 1;
					СтрокаТЗ.ЭтоГруппа = Ложь;
					СтрокаТЗ.Наименование = Объект.CDName;					
					
					СтрокаСледующОбъекта = СтрокаТЗ
				КонецЕсли
			КонецЕсли
		Иначе
			// Нашли строку объекта в таблице
			Если ТаблицаЭлементов_ОбъектИзГруппы(Объект, ИдРодитГр) Тогда
				// Объект остается в этой же группе - просто нужно обновить данные
				СтрокаСледующОбъекта.Объект = Объект;
				Если СтрокаСледующОбъекта.ЭтоГруппа Тогда
					СтрокаСледующОбъекта.Наименование = Объект.GRName
				Иначе
					СтрокаСледующОбъекта.Наименование = Объект.CDName
				КонецЕсли
			Иначе
				// Нужно удалить объект из таблицы (он перешел в другую группу)
				СтрокаСледующОбъекта = ТаблицаЭлементов_УдалитьСтрокуТаблицыЭлементов(ТаблицаЭлементов, Объект, СтрокаСледующОбъекта)
			КонецЕсли
		КонецЕсли
	КонецЕсли;
	
	Возврат СтрокаСледующОбъекта
	
КонецФункции

// Удаляет из таблицы элемент, соответствующий объекту Объект. 
//
// Параметры:
//  ТаблицаЭлементов - ТаблицаЗначений, содержащее таблицу содержимого группы;
//  Объект - объект класса ObjGroup или ObjCard, строку которого нужно удалить;
//  СтрокаЭлемента - строка таблицы, которая предположительно описывает удаляемый объект
//  указывается для того, чтобы не выполнять поиск (если не указана, то поиск объекта
//  ведется по всей таблице);
// Возврат:
//  Строка таблицы, на которую предпочительно перевести фокус ввода после
//  удаления строки объекта Объект и Неопределено, если такая строка не определена
//
Функция ТаблицаЭлементов_УдалитьСтрокуТаблицыЭлементов(ТаблицаЭлементов, Объект, СтрокаЭлемента=Неопределено) Экспорт
	
	СтрокаСледующОбъекта = Неопределено;

	Если (ТаблицаЭлементов <> Неопределено) И (Объект <> Неопределено) Тогда
		СтрокаОбъекта = ТаблицаЭлементов_НайтиСтрокуТаблицыЭлементовПоОбъекту(ТаблицаЭлементов, Объект, СтрокаЭлемента);
		Если СтрокаОбъекта <> Неопределено Тогда
			ИндексСтрокиОбъекта = ТаблицаЭлементов.Индекс(СтрокаОбъекта);
			ТаблицаЭлементов.Удалить(СтрокаОбъекта);
			ЭлемКолич = ТаблицаЭлементов.Количество();
			Если ЭлемКолич > 0 Тогда
				Если ИндексСтрокиОбъекта < ЭлемКолич Тогда
					СтрокаСледующОбъекта =  ТаблицаЭлементов[ИндексСтрокиОбъекта]
				Иначе
					СтрокаСледующОбъекта = ТаблицаЭлементов[ЭлемКолич-1]
				КонецЕсли
			КонецЕсли
		КонецЕсли
	КонецЕсли;
	
	Возврат СтрокаСледующОбъекта
	
КонецФункции


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

Еще одно замечание. Для сокращения времени построения дерева групп (имеет значение при деревьях большого размера), дерево не считывается из базы целиком сразу, а одновременно загружаются только 2 уровня вложенности в текущую раскрытую группу (1-й уровень вложенности соответственно виден пользователю как содержимое раскрытого узла, а 2-й уровень нужен для того, чтобы показать в интерфейсе, есть ли у узлов 1-го уровня содержимое – можно ли их раскрывать). Кроме того, уровни, которые уже были загружены из базы, не считываются из нее повторно. Полностью перестроить дерево можно только общей командой «Обновить». А так, в процессе навигации, сигналом для «догрузки» очередной порции дерева из базы в модель данных и интерфейс, является событие «ПередРазворачиванием» для узла группы. Все это реализуется несложно, как можно увидеть из демонстрационного примера.

Резюме по работе с FireBird.


Итак, соберем еще раз в кучу все приемы работы с FireBird из данной статьи:
1. Установка соединения с базой, работа с командами, отключение от базы:
— вначале при подключении к базе создаем один объект «ADO.Connection» и один объект «ADO.Command» и используем их на протяжении всего сеанса;
— всю работу с базой ведем только через SQL-запросы (другие виды команд не используем)
// Выполняем подключение к базе данных FireBird
ПарамПодкл_Driver = "driver={" + "Firebird/InterBase(r) driver" + "}";
ПарамПодкл_UID = "uid=" + FBUserName;
ПарамПодкл_PWD = "pwd=" + FBUserPass;
ПарамПодкл_DataBase = "database=" + FBDBAddr;
СтрокаСоединения = ПарамПодкл_Driver + ";" +
			 ПарамПодкл_UID + ";" + 
			 ПарамПодкл_PWD + ";" + 
			 ПарамПодкл_DataBase;
// Создаем соединение
ПодкБазе.FBSrvConn = Новый COMОбъект("ADODB.Connection");
// Открываем соединение
ПодкБазе.FBSrvConn.open(СтрокаСоединения);
ПодкБазе.IsDBConnect = Истина;
// Создать объект для выполнения запросов
ПодкБазе.FBCommand = Новый COMObject("ADODB.Command");
ПодкБазе.FBCommand.ActiveConnection = ПодкБазе.FBSrvConn;
ПодкБазе.FBCommand.CommandType = ADO.CommandTypeEnum.adCmdText;

— при отключении от базы – закрываем соединение, проверяя, что оно не было закрыто ранее
// Отключаемся от базы данных
ПодкБазе.IsDBConnect = Ложь;
ПодкБазе.LastTestTime = 0;
ПодкБазе.FBCommand = Неопределено;
Если ПодкБазе.FBSrvConn <> Неопределено Тогда
	Если ПодкБазе.FBSrvConn.State = ADO.ObjectStateEnum.adStateOpen Тогда
		ПодкБазе.FBSrvConn.Close()
	КонецЕсли;
	ПодкБазе.FBSrvConn = Неопределено;
КонецЕсли

2. Защита от разрыва соединения.
При выполнении запросов к базе проверяется актуальность соединения с СУБД. Суть этой проверки в попытке выполнения специального системного SQL-запроса, который всегда должен выполняться на любой базе. Если выполнить не удалось, то считаем что сервер разорвал подключение, и пробуем подключиться еще раз. Естественно, следует делать такие вызовы не при каждом полезном обращении к базе (нельзя просто удваивать количество запросов к системе – это снизит производительность), а только в том случае, если соединение длительное время простаивало (запросов к базе не было) и реально мог произойти разрыв соединения со стороны сервера. В данном примере, таким интервалом простоя выбрано время – 5мин. Описанный алгоритм обрабатывает подпрограмма «ПодкБазе_CheckRestConnect», вызываемая при выполнении каждого полезного запроса к базе.
3. Выполнение запросов:
— при запросах на выборку, параметры всех типов размещаем в тексте запроса, а все результаты получаем через «ADO.Recordset», и затем уже считываем в соответствующие структуры данных программы;
— при запросах на запись – большую часть типов данных (числа, дата/время, не длинные строки) передаем в тексте запроса (помним, что общий текст запроса должен быть не более 65535 символов), а длинные строки и BLOB-поля передаем в параметрах «ADO.Parameter»;
— хранимые процедуры вызываем обычным запросом на выборку «SELECT» или оператором «EXECUTE PROCEDURE» (вообще всю работу с базой осуществляем обычными SQL-запросами);
4. Работа с текстовыми кодировками:
— при работе с данными, объявленными в базе как текстовые (CHAR, VARCHAR, BLOB(TEXT)) — не беспокоимся о преобразовании кодировок;
— при размещении текста в данных, которые для этого не предназначены (BLOB(BINARY)) – выполняем перекодировку самостоятельно (строки 1С хранятся в UTF-8). В принципе перекодировку можно вообще не выполнять – для СУБД это не текст, а просто набор байт. Но конечно правильней хранить в базе эти данные в той же кодировке, что и у остальных текстовых полей.
5. Хранение изображений в базе (в BLOB):
— передача изображений (как и прочих BLOB(BINARY)-полей) между базой и приложением (в обе стороны) выполняется через одномерный массив COMSafeArray (кстати, поля BLOB(TEXT) передаются как обычный текст). Для преобразования объекта «Картинка» в «COMSafeArray» и обратно (с приемлемой скоростью), придется в качестве промежуточного элемента использовать временный файл на диске. Сохранять в файл и загружать из файла картинку можно встроенными в класс «Картинка» методами. А для сохранения в файл и загрузки из файла объекта «COMSafeArray», используем COM-объект потока «ADO.Stream».
6. Передача длинных строк и BLOB-полей в запросах на запись:
— для передачи параметров, значения которых не могут быть помещены в текст запроса, нужно использовать коллекцию «Parameters» объекта «ADO.Command». В случае, если параметр именованный, то его размещение в тексте запроса прописывается таким образом «:prm_name» (имя параметра и двоеточие вначале). Но для драйвера ADO FireBird мне не удалось добиться возможности указывать именованные параметры (не получается и все…). Если кто знает – подскажите, как это сделать. На данный момент мой вывод такой – драйвер ADO FireBird не поддерживает именованные параметры. Поэтому приходится использовать неименованные параметры, расположение которых в тексте запроса указывается знаком «?» (знак вопроса). Поскольку все вопросительные знаки одинаковы, то порядок преобразования нескольких таких параметров в конкретные значения для запроса, определяется исключительно последовательностью расположения соответствующих значений в коллекции параметров «Parameters». Собственно требование к порядку расположения параметров (так как они не имеют идентификаторов) и создает основную трудность — ухудшает структурированность подпрограмм в классе «ПодкБазе».

На этом все. Всем удачи. До встречи.

P.S.

Скачать демонстрационный пример (обработка для 1С 8.2 + базы данных) можно здесь.
В архив входят файлы баз данных для версий FireBird 1.5 и 2.1. При желании можно сделать базу самостоятельно: создается пустая база FireBird, а затем в ней выполняется содержимое файла «script.sql» (команды создания структур данных и хранимых процедур).

Комментарии (1)


  1. EvilBeaver
    09.03.2016 20:20

    А чем не устроили штатные внешние источники данных? Там вообще глубокая интеграция из коробки за пару тычков мышки