Мы уже не раз говорили о том, что для первичного ключа лучше всего использовать тип uniqueidentifier, который лучше с точки зрения обеспечения уникальности строк. Но с другой стороны этот тип хуже для индекса, потому что вновь добавляемые записи могут содержать GUID идентификатор меньший, чем уже существующие в таблице.
При работе с GUID полями мы получаем достаточно преимуществ и недостатков. В этой главе мы рассмотрим и то и другое, а также примеры обхода недостатков.
Давайте для иллюстрации примеров с GUID полями создадим две связанные таблицы: типы телефонов и номера телефонов. Подобные таблицы у нас уже существуют в тестовой базе данных, поэтому я взял их копию, заменил тип ключевых полей на uniqueidentifier и добавил к именам таблиц и ограничений префикс _G. Все это в виде SQL сценария можно увидеть в листинге 2.14.
Листинг 2.14. Создание таблиц, связанных через uniqueidentifier поля
-- Создаем таблицу типов телефонов CREATE TABLE tbPhoneType_G ( idPhoneType uniqueidentifier DEFAULT NEWID(), vcTypeName varchar(20), CONSTRAINT PK_idPhoneType_G PRIMARY KEY (idPhoneType), ) -- Создаем таблицу телефонов CREATE TABLE tbPhoneNumbers_G ( -- Описание полей idPhoneNumbers uniqueidentifier DEFAULT NEWID(), idPhoneType uniqueidentifier, vcPhoneNumber varchar(15), -- Описание ключей CONSTRAINT PK_idPhoneNumbers_G PRIMARY KEY (idPhoneNumbers), CONSTRAINT FK_idPhoneType_G FOREIGN KEY (idPhoneType) REFERENCES tbPhoneType_G (idPhoneType), -- Описание ограничений CONSTRAINT check_vcPhonenumber_G CHECK (vcPhonenumber LIKE '([0-9][0-9][0-9]) [0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]') )
Обратите внимание, что для первичного ключа в качестве значения по умолчанию указана функция NewID(), которая генерирует уникальный идентификатор. Если пользователь будет добавлять запись и не укажет явное значение, то оно будет сгенерировано.
Следующие два примера добавляют в таблицу типы телефонов сотовый и домашний:
-- Добавления типа телефона - сотовый INSERT INTO tbPhoneType_G VALUES (NewID(), 'Сотовый') -- Добавляем домашний тип телефона INSERT INTO tbPhoneType_G (vcTypeName) VALUES ('Домашний') Теперь посмотрим, что у нас хранится в таблице: SELECT * FROM tbPhoneType_G В моем случае результат получился следующим: idPhoneType vcTypeName ------------------------------------------------------- 2A730DD9-3F89-48FD-A7C6-7FACDE496D2E Домашний BA80361E-1398-4C58-9DF9-EC25C18A2439 Сотовый
Мне повезло. Я наглядно смог увидеть, что домашний тип телефона добавлялся позже, а при отображении он стоит первым, потому что для него сгенерирован меньший идентификатор. Это и есть недостаток, который невозможно контролировать. Единственный удачный выход – добавить в таблицу типов еще одно поле, которое будет иметь автоматически увеличиваемое значение.
Давайте очистим таблицу (DELETE FROM tbPhoneType_G) и добавим новое поле
ALTER TABLE tbPhoneType_G ADD [id] int IDENTITY(1, 1)
Снова добавьте в таблицу записи типов телефонов. Чтобы просмотреть все строки в порядке добавления в таблицу, необходимо отсортировать вывод по полю "id":
SELECT * FROM tbPhoneType_G ORDER BY [id]
От одного недостатка избавились. Давайте двигаться дальше. Теперь посмотрим, как можно добавлять записи в таблицу телефонов. В таблице телефонов нам нужно указать идентификатор типа и номер телефона. Проблема возникает именно с первым значением, потому что вводить идентификаторы вручную проблематично. Эта проблема решается следующим образом:
-- Объявляем переменную DECLARE @TypeID uniqueidentifier -- Записываем в нее идентификатор домашнего телефона SELECT @TypeID=idPhoneType FROM tbPhoneType_G WHERE vcTypeName='Домашний' -- Добавляем запись с номером телефона INSERT INTO tbPhoneNumbers_G (idPhoneType, vcPhoneNumber) VALUES (@TypeID, '(000) 912-12-12')
В этом примере сначала объявляется переменная @TypeID типа uniqueidentifier. Затем в эту переменную с помощью запроса SELECT записывается идентификатор домашнего типа телефона. Теперь у нас есть все необходимо для добавления записи, что происходит в последнем запросе.
Посмотрим, как можно увидеть записи в связанных таблицах. Тут особой разницы от других типов ключевых полей нет:
SELECT * FROM tbPhoneType_G pt, tbPhoneNumbers_G pn WHERE pt.idPhoneType=pn.idPhoneType
Допустим, что вы четко знаете, что GUID идентификатор типа телефона равен 09F972F4-CFF9-4E96-9B7C-14AA04B835F3. Как его можно использовать напрямую? Чтобы SQL запрос воспринял это число как GUID, его необходимо заключить в фигурные скобки и передать в виде строки.
Следующий пример объявляет переменную @TypeID типа uniqueidentifier и с помощью SET помещает в эту переменную явное значение, которое потом будет использоваться при добавлении записи с номером телефона:
-- Объявляем переменню DECLARE @TypeID uniqueidentifier -- Записываем в переменную значение SET @TypeID='{09F972F4-CFF9-4E96-9B7C-14AA04B835F3}' -- Вставляем запись INSERT INTO tbPhoneNumbers_G (idPhoneType, vcPhoneNumber) VALUES (@TypeID, '(000) 112-10-10')
Обратите внимание, что значение GUID сначала заключается в фигурные скобки и после этого еще и в одинарные кавычки. Если что-то из этого не указать, сервер вернет ошибку.
Зная идентификатор, можно добавить строку одной командой INSERT INTO. Например:
INSERT INTO tbPhoneNumbers_G (idPhoneType, vcPhoneNumber) VALUES ('{09F972F4-CFF9-4E96-9B7C-14AA04B835F3}', '(000) 222-10-10')
Благодаря использованию GUID идентификаторов, упрощается добавление записей сразу в две таблицы. Например, в следующем запросе в таблицу типов добавляется новая строка "Сотовый личный" и тут же мы добавляем номер телефона с соответствующим типом:
-- Объявляем переменную DECLARE @TypeID uniqueidentifier -- Генерируем GUID значение SET @TypeID=NewID() -- Вставляем запись в таблицу типов INSERT INTO tbPhoneType_G VALUES (@TypeID, 'Сотовый личный') -- Вставляем запись в таблицу телефонов INSERT INTO tbPhoneNumbers_G (idPhoneType, vcPhoneNumber) VALUES (@TypeID, '(901) 111-11-11')
В этом примере, в переменную @TypeID заносится результат выполнения функции NewID(), т.е. сгенерированное значение. После этого, с этим идентификатором добавляются строки в таблицы типов телефонов и в таблицу номеров телефонов.
У нас достаточно знаний, чтобы усложнить пример и сделать проверку, существует ли в таблице нужный телефон и добавлять запись только в том случае, если не существует. Если нужный тип уже есть в таблице, то нужно использовать его GUID. Все это реализовано в листинге 2.15.
Листинг 2.15. Добавление типа только если это нужно
Объявляем переменную DECLARE @TypeID uniqueidentifier -- Проверяем, есть ли нужная запись в таблице IF NOT EXISTS (SELECT idPhoneType FROM tbPhoneType_G WHERE vcTypeName='Сотовый личный') BEGIN -- Если не существует, то генерируем GUID и добавляем и добавляем SET @TypeID=NewID() INSERT INTO tbPhoneType_G VALUES (@TypeID, 'Сотовый личный') print 'Создаем тип "Сотовый личный"'; END ELSE BEGIN -- Если существует, то ищем GUID в таблице print 'Такой тип уже существует, используем его'; SELECT @TypeID=idPhoneType FROM tbPhoneType_G WHERE vcTypeName='Сотовый личный' END -- Вставляем строку INSERT INTO tbPhoneNumbers_G (idPhoneType, vcPhoneNumber) VALUES (@TypeID, '(901) 111-11-11')
Я надеюсь, что вы разберетесь с кодом примера, но на всякий случай я добавил комментарии, которые помогут в трудную минуту.
Как видите, проблемы при использовании GUID есть, но они решаемы. А ведь преимущества от использования GUID затмевают все недостатки. Главное преимущество – возможность самостоятельного задания первичного ключа, без риска нарушения целостности. Это преимущество особенно проявляется при разработке клиентского приложения на языках высокого уровня, например, Delphi или C++. На этих языках пользователь может вводить данные на стороне клиента и сохранять их в памяти. Когда данные накопятся, их можно одним блоком сохранить на сервере.
Пока данные находятся в памяти клиентской машине, первичный ключ с автоматически увеличиваемым полем не может содержать значения, а значит нельзя создавать связей. Если первичный ключ имеет тип GUID, то программа может сама создать значение для ключа и использовать его для создания связей в памяти клиентской машине с данными, которые еще даже не существуют на сервере. Сейчас уже почти во всех языках есть собственные возможности генерирования GUID значений, результат которых абсолютно одинаков.