В чём проблема
MS Access не предоставляет нам возможности выбора способа уникальной идентификации строк таблиц. Единственный вариант - счётчик. Правда, надо отдать ему должное - в подавляющем большинстве случаев этого достаточно.
Трудности начинаются, когда требуется консолидировать данные из разных баз. Очевидно, что даже использование счётчиков со случайными значениями не гарантирует отсутствия конфликтов. Разрешать такие ситуации не просто, главным образом, из-за того, что в них втягиваются кроме одной главной ещё и масса подчинённых таблиц. При репликации также возникают подобные трудности.
Другой неприятной особенностью счётчика явлается то, что иногда значения всё же дублируются, особенно при отсутствии уникального индекса для такого поля. Способы решения
Традиционный метод генерации уникального значения основывается на создании специальной таблицы с полем, которое изменяется по заданному алгоритму при каждом новом обращении к нему клиентских приложений. С полученным таким образом значением можно провести ещё какие-нибудь манипуляции и записать в ключевое поле. Недостатком способа является его явная трудоёмкость и сомнительная надежность. В частности, функцию, используемую для получения такого уникального идентификатора, нельзя использовать в свойстве "Значение по умолчанию" поля таблицы.
Другим методом является выделение диапазонов счётчика для каждой из баз. Такой способ очень даже не плох. Недостатком можно считать необходимость заранее планировать количество и размер диапазонов. Иначе, кому-то может просто не хватить номеров. Кроме того, установка начальных значений счётчиков всё же требует дополнительных действий.
Третий подход подразумевает создание выражения, результатом которого было бы значение, подходящее для использования в качестве уникального идентификатора. Также требуется возможность указать это выражение в свойстве "Значение по умолчанию" поля таблицы. Ниже подробно описаны два таких выражения: одно строкового, а другое числового типа. Идентификатор типа String
Получение уникального значения в данном случае основано на генерации строки из 16-ти символов. Первым идёт самый левый из аргументов командной строки. Значение командной строки можно задать не только в параметре запуска /cmd, но и из программы:
Application.SetOption "Command-Line Arguments", "W"
За ним следуют шестнадцатеричные значения текущей даты и времени. Оставшиеся 7 символов занимает случайное число (также в шестнадцатеричном формате). В итоге получается следующее:
ID = Left(Command(), 1) & Hex(Now() Mod 2 ^ 16) & Hex((Now() - Int(Now())) * 65535) & Hex(Rnd() * (2 ^ 28-1))
Здесь:
Left(Command(), 1) |
любой подходящий символ (должен быть первым в аргументе командной строки) |
Hex(Now() Mod 2 ^ 16) |
дата по модулю 65536 (чтобы было не более 4 символов) |
Hex((Now() - Int(Now())) * 65535) |
время масштабированное до 65535 (чтобы было не более 4 символов) |
Hex(Rnd() * (2 ^ 28-1)) |
случайное число в диапазоне от 0 до 2^28-1 (чтобы было не более 7 символов) | |
При таком способе, количество клиентов в системе ограничено числом допустимых символов для первого (левого) байта в строке. Определённо, таковых найдётся не менее сотни. Если нужно больше, то можно увеличить количество начальных символов. Размер идентификатора при этом, разумеется, тоже возрастёт. Дополнительным плюсом является то, что такой идентификатор можно без всяких переделок использовать в качестве ключа для TreeView. Идентификатор типа Currency
Этот способ создания уникального идентификатора является развитием предыдущего. Он позволяет вдвое уменьшить размер поля и ускорить все операции с ним, так как работа с числами происходит быстрее, чем со строками. Функция получается, правда, более громоздкая:
ID = IIf(Val(Command()) > 63, 1, -1) * (CCur(Abs(Val(Command()) - 64) * (2 ^ 57) / 10000) + CCur(CCur((Int(Now()) Mod 2 ^ 15) * (2 ^ 42) / 10000) + CCur(((Now() - Int(Now())) * 65535) * (2 ^ 26) / 10000) + CCur(Rnd() * (2 ^ 26 - 1)) / 10000))
Аргумент командной строки (то, что стоит за /cmd) должен быть числом в диапазоне от 1 до 127. Именно столько клиентов может быть в системе, использующей данный способ генерации уникального идентификатора. Присвоить этому аргументу значение, например, 127 можно следующей командой:
Application.SetOption "Command-Line Arguments", "127"
Число типа Currency лежит в диапазоне от -(2^63)/10000 до (2^63-1)/10000. Поэтому всё время приходится делить на 10000 и преобразовывать промежуточные результаты к типу Currency, поскольку Access постоянно норовит использовать Double. Если представить значение типа Currency как целое число, то использование его разрядов выглядит следующим образом:
Биты |
Использование в идентификаторе |
Старший 63-й |
Знак (из аргумента командной строки) |
6 (с 57-го по 62-й) |
Номер (из аргумента командной строки) |
15 (с 42-го по 56-й) |
Дата |
16 (с 26-го по 41-й) |
Время |
Младшие 26 (с 0-го по 25-й) |
Случайное число | |
Знак числа (большая или меньшая половина номеров)
IIf(Val(Command()) > 63, 1, -1)
Номер из меньшей (1...63) или большей (64...127) половин. Чтобы уместиться в тип Currency, номер не должен превышать 63. Из-за этого использована функция Abs.
CCur(Abs(Val(Command()) - 64) * (2 ^ 57) / 10000)
Дата. Деление по модулю 2 ^ 15 позволяет втиснуться в отведенные 15 бит, сохраняя уникальность при этом около 80 лет.
CCur(CCur((Int(Now()) Mod 2 ^ 15) * (2 ^ 42) / 10000)
Время с точностью до ~1,3 секунды. В промежутке от 00:00:00 до 23:59:59 принимает значение от 0 до 0,99999 и масштабируется до нужных 16 бит умножением на 65535.
CCur(((Now() - Int(Now())) * 65535) * (2 ^ 26) / 10000)
Случайное число в диапазоне от 0 до 2 ^ 26 - 1. Уменьшение диапазона приводит к возникновению повторов.
CCur(Rnd() * (2 ^ 26 - 1)) / 10000)) Заключение
Второй способ, очевидно, является более предпочтительным, так как позволяет создавать более компактные идентификаторы. Кроме того, он позволяет относительно безболезненно перейти от использования счётчиков, так как работа со строками гораздо сильнее отличается от работы с типами Long или Currency, чем они отличаются друг от друга.
Однако, есть и недостатки. Прежде всего, это ограниченное количество клиентов в системе (не более 127), вызванное сложностью втиснуться в рамки 8-ми байтного числа. Также, нет полной гарантии, что случайные числа не повторятся в пределах тех 1,3 секунды, когда уникальность ключа определяется исключительно работой генератора случайных чисел. В случае такого повтора возникнет ошибка дублирования, которая потребует повтора всей операции добавления записей. Правда, надо отдать должное Access'совской функции Rnd(): она достаточно долго генерирует неповторяющийся поток значений. |