Pers.narod.ru. Обучение. Access VBA - редактируем связь многие-ко-многим и программно выполняем запрос с параметрами формы

Статья написана в учебных целях, хотя в ней есть и пара интересных неучебных нюансов.

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

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

Сама по себе реализация такого редактора в Access несложна, вот весь процесс.

1. Создаем новую базу данных и сохраняем ее.

2. В окне базы данных на вкладке "Таблицы" создаем в режиме конструктора 3 таблицы:

Обратите внимание, что оба поля я сделал ключевыми - это поможет избежать дублирования связей - например, объект 1 не должен иметь две одинаковых связи с категорией 1. Чтобы сделать оба поля ключевыми, нужно при нажатой клавише Ctrl выделить их, щелкая по области ключа, а затем вызвать правой кнопкой пункт меню.

3. Идем Сервис, Схема данных..., добавляем в окно схемы все 3 таблицы и связываем их, перетаскивая поля мышкой так, чтобы получились связи, показанные на первом рисунке. В появившемся окне "Изменение связей" нужно включить все флажки для обеспечения целостности данных при добавлении или удалении записей.
изменение связей

4. Закрыв и сохранив схему, вносим по несколько записей в таблицы "Категории" и "Объекты".

5. Формы для работы с категориями и объектами по отдельности или в связке "главная и подчиненная таблица" сделать легко. Для последнего, например, достаточно перейти на вкладку Формы, вызвать Мастер форм, добавить для формы все поля таблиц "Категории" и "Объекты", а на следующем шаге определить главную и починенную формы. Но нас интересует сейчас не это. Главное, что мы хотим сделать - спроектировать форму "Связи" для редактирования наших данных.

6. Вызываем Конструктор форм, получаем новую пустую форму. Если окна "Раздел: область данных" (на самом деле это окно свойств) нет на экране, вызываем его, выбрав в окне формы правой кнопкой мыши пункт меню Свойства.

7. В выпадающем списке окна свойств выбираем "Форма" и назначаем на вкладке Данные нашей форме источником данных таблицу "Связи".
окно свойств формы

8. С помощью меню Вид, Панель элементов убеждаемся, что панель с интерфейсными элементами доступна. Находим на ней элемент "Список" и добавляем его на форму, при этом должен вызваться мастер "Создание списков":
вызов мастера списков

Если мастера для этого или других элементов не вызываются, причин может быть 2: не нажата кнопка "Мастера" на Панели элементов или не установлены соответствующие компоненты Access.

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

9. Аналогичным образом создаем список для отображения объектов из таблицы "Объекты", а называться он будет "СписокОбъекты".

10. В принципе, все готово. Наша форма работает, в чем можно убедиться, открыв ее кнопкой Вид
кнопка Вид и выполнение формы

Проблема состоит в том, что при повторном добавлении связи Access начинает ругаться стандартными сообщениями ("Изменения не были внесены из-за повторяющихся значений в индексе...") и, более того, не дает сохранить последние внесенные изменения. Напишем небольшую процедуру на VBA для решения проблемы и лучшей обработки записей.

11. Вернувшись в режим конструктора, добавим на свободное место кнопку и с помощью мастера "Создание кнопок" назначим ей действие "добавить запись" из категории действий "Обработка записей". Все остальное можно настроить по вкусу.

12. При выбранной кнопке в окне свойств перейдем на вкладку События и обратимся к коду сгенерированного нами обработчика:
вызов кода обработчика события

13. Мы перепишем код сгенерированной Access процедуры так, чтобы он отслеживал ситуацию, когда в списках категорий и объектов ничего не выбрано, а также не давал повторно добавить уже существующую связь. Для последнего действия нам понадобиться выполнить из кода на VBA дополнительный запрос по извлечению выбранных на форме кода категории и кода объекта из таблицы "Связи". Если этот запрос вернет пустой результат, значит, такой связи еще нет и следует добавить запись. Сгенерировать такой запрос на вкладке "Запросы" и потом просто вызвать его, к сожалению, не получится. Дело в том, что Access не видит взятых из формы параметров запроса, если запрос выполняется программно. Ошибка, как правило, возникает со следующим текстом "Too few parameters. Expected Число" ("Слишком мало параметров. Ожидалось Число"). Эта ошибка возникает, если команда или один из нижележащих запросов содержит обращения к формам или собственные параметры, - все эти обращения будут восприняты как параметры, которым не передано значение.

Почему так происходит? По этому поводу в справке Microsoft MSDN написано примерно следующее:
NOTE: В DAO Вы должны явно присвоить значение параметру. При использовании DoCmd.OpenQuery Вы этого делать не должны, т.к. DAO использует операции низкого уровня, что даёт Вам большую свободу в использовании параметров (т.е. Вы можете сами присвоить параметру значение переменной, а не использовать ссылку на форму), но Вы должны выполнить служебные действия, которые Access делает "за кулисами" при исполнении DoCmd. С другой стороны, DoCmd работает на более высоком уровне, чем DAO. Выполняя DoCmd, Microsoft Access делает некоторые предположения о том, как поступить с параметрами, и не дает Вам никакой свободы в этом отношении.

Попросту говоря, при выполнении запроса непосредственно из окна Access он выполняется с помощью движка JET, который, будучи встроен в Access, "знает" о наличии форм и пытается найти их поля и подставить значения. При выполении запроса из кода методом Execute или иным, запрос выполняется с помощью библиотеки DAO, которая, будучи внешней, ничего "не знает" о формах ACCESS, поэтому все недостающие значения считает неопределенными.

В Интернете можно встретить советы предварительно обработать все параметры процедурой вида

Dim q As DAO.QueryDef, p As DAO.Parameter 
  Set q = CurrentDb.QueryDefs("ИмяЗапроса") 'как обычного запроса Select,
  'так и INSERT/UPDATE; в запросах на удаление это не помогает
  For Each p In q.Parameters 
    p.Value = Eval(p.Name) 
  Next 
  q.Execute 
  q.close: Set q=Nothing

Однако, для работы этого кода нужно, во-первых, иметь подключенную DAO (в окне редактора Visual Basic при остановленной программе вызвать Tools, References (или Сервис, Ссылки), найти и включить в списке библиотеку Microsoft DAO 3.6 Object Library), во-вторых, работа кода все-таки не гарантируется и в этом случае.

Мы хотим обойтись стандартным кодом

Dim rst As Recordset
CurrentDb.OpenRecordset ("строка запроса")

однако, едва избавившись от ошибок с недостающимим параметрами, получим сообщение о нессответствии типов (type mismatch, ошибка с кодом 13)!

Вся проблема состоит в том, что объект RecordSet есть и в библиотеке DAO, и в используемой Access по умолчанию библиотеке ADODb! Таким образом, наличие прямой ссылки на DAO, как в показанной выше процедуре, не гарантирует работоспособность кода - может возникать куча заморочек, связанных с тем, какая библиотека подключена в данный момент и у какой выше приоритет.

Поискав (и не найдя) ответ по всему Интернету я догадался, наконец, описать RecordSet как Variant, то есть, без указания типа:

 Dim rs

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

Private Sub КнопкаДобавить_Click()
If ([Forms]![Связи]![СписокКатегории] < 1) Or ([Forms]![Связи]![СписокОбъекты] < 1) Then
 MsgBox "Выберите категорию и объект", vbOKOnly, "Сообщение"
 Exit Sub
Else
 Dim rs
 Dim l1, l2 As Integer
 l1 = (Forms![Связи]![СписокКатегории])
 l2 = (Forms![Связи]![СписокОбъекты])
 Dim s As String
 s = "SELECT Связи.[Код категории],Связи.[Код объекта] FROM Связи WHERE (([Код категории]=" & l1 & ") AND ([Код объекта]=" & l2 & "))"
 Set rs = CurrentDb.OpenRecordset(s)
 If rs.RecordCount = 0 Then
   DoCmd.GoToRecord , , acNewRec
 Else
  MsgBox "Уже есть такая связь", vbOKOnly, "Сообщение"
  Exit Sub
 End If
End If
End Sub

Нам остается отключить для формы встроенную навигацию (свойства "Область выделения" и "Кнопки перехода" со вкладки "Макет" окна свойств формы), добавить с помощью мастера свою навигацию и получить работающее приложение.

Кстати, стандартные сообщения для кнопок навигации, генерируемые Access, можно заменить на свои более осмысленные, например, код

MsgBox Err.Description

на

 MsgBox "Достигнуто начало базы данных", vbOKOnly, "Сообщение" 

Это пример можно скачать и доделать, ведь область применения отношений "многие-ко-многим" так же широка, как сами эти отношения.

Скачать редактор связи "многие-ко-многим": base_mn.zip, 32 Кб

Рейтинг@Mail.ru
вверх гостевая; E-mail
Hosted by uCoz