Расширение набора базовых классов .NET Framework
Страница 2. Представление первичных/внешних ключей базы данных


 

Представление первичных/внешних ключей базы данных

При разработке многоуровневых приложений баз данных возникает проблема генерации первичных ключей. Например, в таблице ORDERS есть поле CustomerID, ссылающееся на запись в таблице CUSTOMERS. Предположим, пользователь добавил нового покупателя в таблицу CUSTOMERS и заказ этого покупателя в таблицу ORDERS. Внесенные изменения кэшировались в DataSet на стороне клиента. После этого вызываются методы customersDataAdapter.Update(customersTable) и ordersDataAdapter.Update(ordersTable) для сохранения изменений на сервере. Проблема заключается в том, как выбрать значение первичного ключа для таблицы CUSTOMERS, чтобы при сохранении изменений на сервере не нарушилась ссылочная целостность между таблицами ORDERS и CUSTOMERS.

Одним из решений может быть использование автоинкрементного поля для первичного ключа. Причем, в базе данных значение первичного ключа инкрементируется с шагом +1, а в наборе данных на стороне клиента с шагом -1 (свойство DataColumn.AutoIncrement устанавливается в True, свойство DataColumn.AutoIncrementStep устанавливается в -1). Тогда после выполнения предложения INSERT для каждого клиента в процессе работы метода customersDataAdapter.Update(customersTable) нужно получить значение первичного ключа только что вставленной записи (например, выполнив "SELECT @@IDENTITY" для SQL Server), а затем заменить значение первичного ключа соответствующей строки в таблице CUSTOMERS и значение внешнего ключа всех строк в таблице ORDERS, которые ссылаются на данного покупателя. Этот метод довольно сложен и не отличается надежностью.

Альтернативным решением может быть использование GUID качестве значения первичного ключа, т.е. первичный ключ для новой строки в таблице Customers может получаться вызовом метода Guid.NewGuid() на стороне клиента. Вероятность того, что две записи в таблице CUSTOMERS получат одно и то же значение GUID, очень мала. Однако, значения GUID не очень удобны в работе. В .NET Framework Guid представляет собой структуру, а не класс, т.е. относится к value-, а не к reference- типу. Это приводит к тому, что почти всякая операция с типом Guid приводит к копированию 16 байт из одного места памяти в другое. Использование Guid в качестве первичного ключа на практике выливается в большой объем используемой памяти и низкое быстродействие.

Для решения этой проблемы в AcedUtils введен класс AcedKey, который представляет собой эквивалент Guid, но обрабатывается как класс, а не как структура. Экземпляры класса AcedKey являются неизменяемыми. В них есть свойства: A, B, C, D типа System.UInt32, которые представляют, соответственно, старшую, среднуюю-старшую, среднюю-младшую и младшую части 128-битного значения. По аналогии с классом System.String в классе AcedKey используется пул идентификаторов, т.е. при запросе на создание нового ключа выполняется поиск во внутреннем хэше ключей. Если такой ключ присутствует в хэше, возвращается ссылка на этот экземпляр, иначе создается новый экземпляр класса AcedKey и помещается в хэш. При использовании SQL Server в качестве сервера базы данных столбцы первичных и внешних ключей должны иметь тип uniqueidentifier. На других серверах экземпляры класса AcedKey могут храниться в виде строк.

С помощью метода ToBase85() класса AcedKey может быть получено строковое представление ключа в кодировке Base85 длиной до 20 символов, а с помощью метода FromBase85() - восстановлен исходный экземпляр AcedKey из строки в кодировке Base85. Эта кодировка используется для более компактного чем в Base64 представления бинарных значений. Каждые 4 байта бинарных данных представляются в виде 5 символов в кодировке Base85. Основные методы для работы с кодировками находятся в классе Aced.G.

Теперь рассмотрим, как сгенерировать ключ AcedKey. Проще всего получить новый ключ из значения Guid вызовом: AcedKey.FromGuid(Guid.NewGuid()). Однако, теоретически, все же существует вероятность генерации двух одинаковых значений Guid. Кроме того, метод Guid.NewGuid() не отличается высоким быстродействием. В данном случае нам не нужен глобальный уникальный идентификатор, достаточно получить идентификатор, уникальный в пределах базы данных. Тогда можно воспользоваться следующим приемом.

В базе данных создаем таблицу с одним столбцом и одной строкой, где хранится значение типа Int64. В SQL Server это будет столбец типа bigint. При подключении к базе данных каждого нового клиента увеличиваем этот счетчик на 1 и возвращаем полученное значение в клиенту. В клиентском приложении в некотором классе есть статические поля: _nA, _nB, _nC, _nD типа UInt32. Значение x типа Int64, которое пришло с сервера, сохраняется в полях _nC и _nD:

public void UpdateKeyBase(long x)
{
_nA = 0;
_nB = 0;
ulong ux = (ulong)x;
_nC = (uint)ux;
_nD = (uint)(ux >> 32);
}

Тогда новое значение идентификатора для первичного ключа может быть сгенерировано следующим методом:

public AcedKey NewKey()
{
if (_nA != 0xFFFFFFFFu)
_nA++;
else
{
_nA = 0;
if (_nB != 0xFFFFFFFFu)
_nB++;
else
throw new Exception("There are no more available keys.");
}
return AcedKey.FromABCD(_nA, _nB, _nC, _nD);
}

В классе AcedKey есть статические поля Empty и Full, которые служат, соответственно, для представления значений NULL и NOT NULL в полях внешних ключей. Есть еще методы для сравнения ключей, расчета комбинированного значения, т.е. своего рода контрольной суммы для набора ключей, чтения и сохранения ключа в массиве байт и т.д.

 
« Предыдущая статья   Следующая статья »