MySQL в качестве системы управления базой данных доминирует в интернет-проектах. Приложение 1С:Предприятие может устанавливать соединения с серверами MySQL для обмена данными в обоих направлениях. Это может использоваться например:
- для публикации сведений в интернет;
- для получения сведений из интернет;
- для обмена данными с другими приложениями 1С через общее интернет-хранилище.
Серверный доступ
Для установки соединения с сервером MySQL и подключения к одной из DB такая возможность должна быть предусмотрена целым комплексом серверных настроек, которые осуществляет администратор сервера, и здесь они не будут рассматриваться. Тем не менее, в ряде случаев доступ к каждой отдельной DB должен быть настроен администратором хостинга соответствующими средствами.
DirectAdmin Web Control Panel
При использовании для управления хостингом DirectAdmin Web Control Panel следует выполнить следующую последовательность действий:
- Открыть и авторизоваться в DirectAdmin Web Control Panel
- В списке Domain выбрать необходимый домен
- В разделе Your Account выбрать MySQL Management
- В колонке Database выбрать нужную DB
- В разделе Access Hosts просмотреть все разрешенные источники доступа
- localhost - открывает доступ к DB на самом сервере
- % - открывает доступ к DB для любых внешних подключений
- некоторый IP открывает доступ к DB из определенного ip-адреса
- Если доступ % (или иное) не разрешен, то его следует добавить с помощью ввода и кнопки Add Host
- В разделе Access Hosts просмотреть все разрешенные источники доступа
- В колонке Database выбрать нужную DB
- В разделе Your Account выбрать MySQL Management
- В списке Domain выбрать необходимый домен
Драйвер клиентского подключения
Для возможности установки соединения с сервером MySQL из приложения 1С:Предприятие в серверной системе 1С (в клиентской по отношению к серверу MySQL) должен быть установлен соответствующий ODBC драйвер. Актуальный драйвер ODBC Driver for MySQL (Connector/ODBC) можно загрузить в разделе коннекторов официального сайта //www.mysql.com/products/connector/.
Проверить наличие драйвера в системе можно в Панель управления -> Источники данных ODBC -> Администратор источника данных ODBC, драйвер должен присутствовать в закладке Драйверы в списке установленных драйверов, в списке будет отражено имя и версия. Окно администратора источника данных ODBC можно вызвать командами:
- x32 версия: %windir%\syswow64\odbcad32.exe
- x64 версия: %windir%\system32\odbcad32.exe
Другой способ проверить наличие драйвера, который можно реализовать программно, это просмотреть список параметров реестра в ветке установленных драйверов:
1 2 3 4 |
[HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers] "SQL Server"="Installed" "MySQL ODBC 8.0 Unicode Driver"="Installed" "MySQL ODBC 8.0 ANSI Driver"="Installed" |
Точное имя установленного драйвера будет необходимо для установки соединения в программном модуле 1С.
Соединение с сервером MySQL
Все операции с сервером MySQL в программе необходимо выполнять через установленное соединение, которое реализуется универсальным COM-объектом типа Connection в его методе .Open(<строка подключения>, [<логин>], [<пароль>], [<опции>]) использующем специально подготовленную строку подключения.
- строка подключения - образуется из нескольких обязательных и необязательных частей определяющих установление и параметры работы соединения с сервером.
- driver={<имя драйвера>} - имя установленного в системе драйвера (см.выше)
- server=<домен/ip сервера>; - адрес хоста сервера, по которому будет установлено сетевое подключение
- port=<порт>; - (необязательный) порт хоста сервера для подключения
- uid=<логин>; - логин подключения, необязателен, если логин будет указан в специальном параметре метода .Open
- pwd=<пароль>; - пароль логина, необязателен, если будет указан в специальном параметре метода .Open
- database=<БД по умолчанию>; - (необязательный) устанавливает БД по умолчанию, эквивалентен лидирующему выражению "use <db-name>;"
- MULTI_STATEMENTS=1; - опция, позволяющая выполнять запросы из нескольких выражений
- MULTI_RESULTS=1; - опция, позволяющая получить несколько результирующих наборов данных из запроса с несколькими выражениями
- STMT=SET CHARACTER SET utf8 - назначает кодировку символов UTF8
- логин - логин подключения, необязателен, если логин был указан в строке подключения
- пароль - пароль логина, необязателен, если пароль был указан в строке подключения
- опции - (необязательный)
Пример установки соединения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
... Соединение = Новый COMОбъект("ADODB.Connection"); СтрокаПодключения = "driver={MySQL ODBC 8.0 Unicode Driver}; |server=MySERVER; |uid=MyUser; |pwd=MyPass; |database=MyDB; |MULTI_STATEMENTS=1; |MULTI_RESULTS=1; |STMT=SET CHARACTER SET utf8"; Попытка Соединение.open(СтрокаПодключения); Исключение ... КонецПопытки; ... //вариант формирования строки подключения шаблоном СтрокаПодключения = СтрШаблон("DRIVER={MySQL ODBC 8.0 Unicode Driver}; SERVER=%1; DATABASE=%2; PORT=%3; UID=%4; PWD=%5; MULTI_STATEMENTS=1; MULTI_RESULTS=1; STMT=SET CHARACTER SET utf8", Хост, БазаДанных, Порт, Логин, Пароль); ... |
(https://dev.mysql.com/doc/dev/mysql-server/latest/namespacemysql__protocol_1_1Capabilities.html)
При успешной установке соединения свойство .State устанавливается в 1, а в случае ошибки остается в состоянии 0. Коллекция ошибок типа Errors доступна в объекте соединения, однако в нее попадают ошибки только уровня сервера, которые возникают после установки соединения с сервером. Если ошибка возникла до установки соединения по сетевой или иной причине, ошибка в коллекцию .Errors не попадет.
Для завершения работы с установленным соединением его желательно закрыть:
1 2 3 |
... Соединение.Close(); ... |
Выполнение выражений запросов
После успешного установления соединения сервером MySQL можно управлять посредством sql-выражений в пределах прав установленных для пользователя, логин которого был использован при подключении. Существует два способа выполнения sql-выражений: непосредственно через объект соединения; через дополнительный объект команды.
Запросы Соединения
Успешно открытый объект соединения типа Connection может передать на сервер MySQL sql-выражение для выполнения используя метод .Execute(<выражение>[, <обработано>[, <опции>]]), где:
- выражение - строковое sql-выражение запроса или команды;
- обработано - числовое, возвращаемое, количество записей обработанных выражением;
- опции
- ExecuteOptionEnum
- adAsyncExecute 0x10/16 Указывает, что будет выполняться асинхронно.
- Это значение не может объединяться с CommandTypeEnum значение adCmdTableDirect. - adAsyncFetch 0x20/32 Указывает, что оставшиеся строки после начальной количество указанных в CacheSize асинхронно нужно извлечь свойство.
- adAsyncFetchNonBlocking 0x40/64 Указывает, что основной поток не блокируется при извлечении. Если Запрошенная строка не были получены, текущая строка автоматически перемещается в конец файла.
- При открытии записей из Stream содержащий постоянно хранимых записей, adAsyncFetchNonBlocking не будет эффекта. Данная операция будет синхронной и блокировки.
- adAsynchFetchNonBlocking не действует, если adCmdTableDirect параметр используется для открытия записей. - adExecuteNoRecords 0x80/128 Указывает, что текст команды команду или хранимую процедуру, которая не возвращает строки (например, команды, только вставляет данные). - Если извлекаются все строки, они удаляются и не возвращается.
- adExecuteNoRecords могут передаваться только как необязательный параметр для команда или выполнить соединение метод. - adExecuteStream 0x400/ Указывает, что результаты выполнения команды должно возвращаться в виде потока.
- adExecuteStream могут передаваться только как необязательный параметр для Command Execute метод.
- adExecuteRecord Указывает, что CommandText команду или хранимую процедуру, которая возвращает одну строку, в которой будут возвращены в качестве записи объекта.
- adAsyncExecute 0x10/16 Указывает, что будет выполняться асинхронно.
- CommandTypeEnum
- adCmdText (1) Результатом является CommandText как текстовое определение команда или хранимая процедура вызывать.
- adCmdTable (2) Результатом является CommandText как на имя таблицы, столбцы которых возвращаются, созданного внутреннего запроса SQL.
- adCmdStoredProc (4) Результатом является CommandText как имя хранимой процедуры.
- adCmdUnknown (8) По умолчанию. Указывает, что тип команды в CommandText свойство неизвестно.
Если тип команды неизвестен, ADO сделает несколько попыток, чтобы интерпретировать CommandText.
- CommandText интерпретируется как текстовое определение для вызова команды или хранимых процедур. Это аналогично adCmdText.
- CommandText имя хранимой процедуры. Это аналогично adCmdStoredProc.
- CommandText интерпретируется как имя таблицы. Созданная внутри структура SQL-запрос возвращает все столбцы. Это аналогично adCmdTable. - adCmdFile (256) Результатом является CommandText как имя файла постоянно хранимых записей. Используется с набор записей. Откройте или Requery только.
- adCmdTableDirect (512) Результатом является CommandText как на имя таблицы, столбцы которого возвращаются все. Используется с Recordset.Open или Requery только. Чтобы использовать Seek метод, записей должен открываться с adCmdTableDirect.
Это значение не может объединяться с ExecuteOptionEnum значение adAsyncExecute.
- ExecuteOptionEnum
Результатом успешного выполнения sql-выражения будет новый COM объект Набор записей типа Recordset. В случае ошибки метод .Open(...) вернет пустое значение Неопределено, а перечисление ошибок будет размещено в свойстве .Errors объекта Соединение.
1 2 3 4 5 6 |
... НаборЗаписей = Соединение.Execute("SELECT Name FROM MyTable;"); Если НаборЗаписей <> Неопределено Тогда ... ИначеЕсли Соединение.Errors.Count > 0 Тогда ... |
///
Команды и Запросы
В программном коде можно создать специальный COM объект Команды типа Command и связать его с открытым соединением, иногда это может оказаться удобным. Объект Команды использует свойство .CommandText для sql-выражения, которое передается серверу MySQL для выполнения собственным метод .Execute([<обработано>[, <опции>]]), которое отличается от метода .Execute(...) Соединения только отсутствием параметра выражения.
1 2 3 4 5 6 7 8 9 10 |
... Команда = Новый COMОбъект("ADODB.Command"); Команда.ActiveConnection = Соединение; Команда.CommandType = 1; Команда.CommandText = "SELECT Name FROM MyTable;"; Попытка Обработано = 0; НаборЗаписей = Команда.Execute(Обработано, 1); Исключение ... |
Обработка набора записей
Любой успешный запрос к серверу методами .Execute(...) предполагает, что сервер вернет контейнер объекта Набор записей типа Recordset, однако следует учитывать следующие обстоятельства:
- Контейнер набора Recordset возвращается методом Execute() в качестве результата только в случае успешного выполнения выражения и если не была указана опция adExecuteNoRecords.
- В случае ошибки в выражении или его выполнении вызывается исключение и значение не возвращается (не изменяется или остается Неопределено)
- В случае применения опции adExecuteNoRecords значение не возвращается (не изменяется или остается Неопределено);
- Оценить наличие набора записей в контейнере можно через свойство контейнера .State, которое устанавливается в 1 при наличии набора (пусть даже пустого) или в 0 при его отсутствии;
- При отсутствии набора в контейнере любой метод относящийся к набору вызовет исключение;
- Методы .MoveFirst() или .MoveLast() применимы только к непустому набору, в пустом наборе они вызывают исключение;
- В пустом наборе методы .Bof() и .Eof() возвращают Истина одновременно;
- Структуру полей полученного набора можно получить из коллекции .Fields(<индекс поля>).Name даже в пустом наборе;
- Значение поля в записи можно получить через коллекцию .Fields(<индекс поля>).Value только если курсор установлен на одной из записей набора, в противном случае вызывается исключение.
Простейший способ просмотра набора записей:
1 2 3 4 5 6 7 8 9 |
... НаборЗаписей = Соединение.Execute("SELECT Name FROM MyTable;"); НаборЗаписей.MoveFirst(); Пока Не НаборЗаписей.Eof() Цикл Имя = НаборЗаписей.Fields("Name").Value; НаборЗаписей.MoveNext(); КонецЦикла; НаборЗаписей.Close(); ... |
Полный алгоритм выполнения и перебора результата одиночного запроса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... Попытка НаборЗаписей = Соединение.Execute("SELECT * FROM MyTable;"); Если НаборЗаписей.State = 1 Тогда Если Не НаборЗаписей.Eof() Тогда НаборЗаписей.MoveFirst(); Пока Не НаборЗаписей.Eof() Цикл Для ИндексПоля = 0 По НаборЗаписей.Fields.Count - 1 Цикл ИмяПоля = НаборЗаписей.Fields(ИндексПоля).Name; ЗнчПоля = НаборЗаписей.Fields(ИндексПоля).Value; Сообщить(ИмяПоля + "=" + ЗнчПоля); КонецЦикла; НаборЗаписей.MoveNext(); КонецЦикла; КонецЕсли; КонецЕсли; НаборЗаписей.Close(); Исключение ... КонецПопытки; |
Набор записей выражений не возвращающих данные
Большое число sql-выражений не предполагает получение ответных данных с сервера в форме набора записей, в частности выражения модификации данных INSERT, UPDATE и DELETE. Их выполнение передается серверу тем же методом .Execute(), отличие состоит только в том, что у результирующего контейнера типа Recordset свойство .State установлено в 0.
Последовательность Набора записей
Успешное выполнение методом .Execute(...) запроса, содержащего несколько sql-выражение возвратит последовательность контейнеров типа Recordset в том порядке, в каком они следуют в запросе. Для получения следующего контейнера Набора записей типа Recordset используется метод объекта Набора записей .NextRecordset().
- последовательность содержит контейнеры Набора записей всех выражений запроса, и тех которые вернули данные .State=1, и тех которые данные не возвращают .State=0
- последовательность контейнеров Набора записей следует в том порядке, в каком соответствующие sql-выражения следуют в запросе
- метод .Execute(...) возвращает первый Набор записей первого выражения запроса
- метод .NextRecordset() последнего Набора записей возвратит пустое значение Неопределено.
- возможность вернуться к предыдущему контейнеру Набора записей не предусмотрена
- контейнер Набора записей уничтожается немедленно, после того как в программном коде соответствующая переменная освобождается или получает значение другого контейнера.
1 2 3 4 5 6 7 8 9 |
... НаборЗаписей = Соединение.Execute("SELECT Name FROM MyTable; SELECT Age FROM MyTable;"); Пока НаборЗаписей <> Неопределено Цикл Если НаборЗаписей.State = 1 Тогда ... КонецЕсли; НаборЗаписей = НаборЗаписей.NextRecordset(); КонецЦикла; ... |
Ошибки при использовании Соединения
Ошибки возникающие при обработке и выполнении выражений запросов доступны в коллекции .Errors объекта соединения, которая содержит элементы типа Error, обладающие свойствами:
- Description - описание ошибки. Обычно наиболее важная информация содержится именно в описании.
- Number - номер ошибки. По номеру удобно производить поиск в базе знаний и в Интернет.
- Source - источник ошибки. Эта информация полезна только в том случае, если в коллекции Errors могут оказаться ошибки из разных источников.
- SQLState и NativeError - информация о возникшей ошибке, которая пришла из SQL-совместимого источника данных.
1 2 3 4 5 6 |
... ВсегоОшибок = Соединение.Errors.Count; Для ИндексОшибки = 0 По ВсегоОшибок - 1 Цикл Сообщить(Соединение.Errors.Item(ИндексОшибки).Description); КонецЦикла; ... |
Хранимые процедуры
При создании хранимой процедуры надо учесть возможность того, что процедура с таким именем уже существует, и в этом случае возникнет ошибка. Замещение кода процедуры командой ALTER в MySQL к сожалению не предусмотрено, поэтому создание процедуры надо начать с ее удаления:
1 |
DROP PROCEDURE IF EXISTS myProc |
Создание процедуры осуществляется командой CREATE PROCEDURE:
1 2 3 4 5 6 7 |
CREATE PROCEDURE myProc () BEGIN SELECT Name FROM MyTable1; SELECT Name FROM MyTable2; ... SELECT Name FROM MyTableN; END |
Вызов созданной процедуры выполняется командой CALL:
1 |
CALL myProc; |
Если процедура необходима для однократного выполнения, было бы разумным создавать временную процедуру, но временные процедуры в MySQL к еще одному сожалению не поддерживаются. Удаление процедуры выполняет команда DROP PROCEDURE, которая уже была приведена вначале, и было бы удобно этой командой завершить код самой процедуры, но это к еще одному сожалению в MySQL не поддерживается - в коде процедур не разрешено использовать эту команду в принципе.
Если в состав выражений процедуры входит запрос выборки данных, то результатом вызова процедуры аналогично простому запросу будет набор записей представленный объектом Recordset, который можно обработать уже описанным выше способом, средствами объекта. Если в состав пакета процедуры входит несколько запросов выборки, то наборы записей образуют последовательность, в которой следующий набор записей можно получить методом .NextRecordset(), как это показано на упрощенном примере перебора последовательности объектов Recordset и перебор записей, входящих в каждый набор:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... НаборЗаписей = Соединение.Execute("CALL myProc;"); Пока НаборЗаписей.State = 1 Цикл Если Не НаборЗаписей.Eof() Тогда НаборЗаписей.MoveFirst(); Пока Не НаборЗаписей.Eof() Цикл НаборЗаписей.MoveNext(); Сообщить(НаборЗаписей.Fields("Name").Value); КонецЦикла; КонецЕсли; НаборЗаписей = НаборЗаписей.NextRecordset(); КонецЦикла; ... |
В коде примера при получении следующего набора записей, предыдущий набор освобождается и немедленно разрушается.
Баги и фичи
Двукратное выполнение хранимой процедуры
По неустановленной причине хранимая процедура вызванная средствами ADO будет выполнена дважды, если процедура выполняет хотя бы один возврат записей выражением SELECT. Единственным способом избежать двойное выполнение является отказ от выражения SELECT в процедуре в пользу получения необходимых записей дополнительным серверным вызовом в рамках установленного сеанса соединения (в пределах сеанса временные таблицы остаются доступными для всей последовательности вызовов). В консоли Navicat описанная проблема на проявлялась.
Разрушение данных в поле набора записей
По неустановленной причине в полученном запросом наборе записей при повторном получении значения строкового/текстового поля большого объема там может оказаться значение Null вместо первоначально полученного, как это показано в примере:
1 2 3 4 5 |
Если ТипЗнч(НаборЗаписей.Fields("Text").Value) = Тип("Строка") Тогда // в следующем присвоении из-за разрушения данных в поле набора записей // Текст может получить значение Null Текст = НаборЗаписей.Fields("Text").Value; ... |
///
Прочие замечания
Если установка соединения с сервером MySQL должна выполняться автоматически, то для автоматической авторизации придется где-то хранить реквизиты логина и пароля. Распространенная традиция хранить логин и пароль в реквизитах ИБ рискованна по многим причинам, поэтому будет гораздо безопасней сохранить их в системной или в пользовательской ветке реестра. Технология работы с реестром в приложении 1С:Предприятие описана в публикации Программная работа с реестром Windows.
Источники
- Использование библиотеки ADO (Microsoft ActiveX Data Object) //script-coding.com
- Пример подключения к MySQL базе на //helpf.pro
- Взаимодействие 1С с ADODB на //mudritskiy.blogspot.com
- о MySQL и другие интересные вопросы на //www.cyberguru.ru
- про ALTER PROCEDURE в публикации Хранимые процедуры в MySQL на //habr.com
- старенький сайт //www.mysql.ru (умер в 2010 году)