четверг, 9 апреля 2009 г.

Циклические связи между версиями

Меня на днях попросили побольше писать про программирование. И вот как раз подвернулся интересный случай.

Часто возникает необходимость хранить в базе данных старые версии чего-нибудь. Например, банк должен хранить как текущую, так и предыдущие версии депозитного договора, потому что в какой-то момент может поменяться ставка или ещё что-нибудь. А старые версии нужно хранить, потому что до какой-то даты проценты надо расчитывать по-другому, чем сейчас, и т.д. Такая же ситуация со страховыми полисами.

У нас тоже есть такая штука для хранения заказов на печать книг: бывает, что первоначально в типографию отправили неправильный заказ (тип бумаги, количество и пр.), и его надо оправить повторно. В таблице "заказ" в основном хранятся поля, которые никогда не меняются, например, "поставщик". А в "версиях" то, что менятся чаще, например, сумма или дата заказа.
Вот примерно такую схему сделали индусы:

Order (OrderId, OrderNo, SupplierId, CurrentVersionId, ......)

Version (VersionId, OrderedDate, Amount, OrderId, VersionNo, ........)

Как видите, тут циклическая ссылка: в заказе есть поле "id текущей версия", а в версиях есть поле "id заказа". Спорное решение, но иногда применяется.

Кроме первичного ключа VersionId, в таблице Version имеется еще дополнительный уникальный ключ (OrderId, VersionNo). Логично - для каждого конкретного заказа номера версий должны быть уникальными ("1,2,3", но не "1,1,1,2,2,3,3").

Изредка заказы нужно удалять из базы данных. Естественно, просто так удалить заказ нельзя, потому что к нему привязаны версии, а текущую версию удалить нельзя, потому что она используется в таблице "заказы". Чтобы обойти это, делают так:

UPDATE Version SET OrderId = NULL WHERE VersionId = 123
DELETE FROM Order WHERE OrderId = 456
DELETE FROM Version WHERE VersionId = 123

Первая команда не очень хорошая: что будет, если удалять несколько заказов одновременно? У нас могут появится несколько записей, в которых сочетание полей (OrderId, VersionNo) будет совпадать, потому что OrderId = NULL, а VersionNo = 1, 2.... К счастью, такого никогда не случается, потому что пользователи не удаляют заказы вручную. Это автоматически делает серверный процесс. Этот процесс - только один, поэтому по идее он ни с кем не конфликтует.

Но сегодня я сделал потрясающее открытие: еще три года назад из-за какого сбоя в таблице Version у нас образовалась одна запись с OrderId = NULL. Все эти годы она не давала выполняться команде UPDATE Version SET OrderId = NULL WHERE VersionId = 123. Но этого никто не замечал. Серверный процесс, наверное, писал про ошибку в какие-то журналы, но никто их не читал. И так - три года.

P.S. Вспомнилась другая немного похожая проблема. Если в Oracle вставить лишнюю запись в таблицу dual, то есть хорошие шансы, что куча запросов перестанет работать. В отличие от MS SQL, в Oracle после SELECT надо обязательно писать FROM. Т.е. даже если ты просто хочешь, к примеру, получить текущую дату, то ее все равно надо как бы считывать из таблицы: SELECT SYSDATE FROM Dual. В Dual нет никакой полезной информации, только одна "фиктивная" запись. Все программисты расчитывают, что она одна и только одна, поэтому пишут что-то вроде SELECT * FROM Balance WHERE BalDate = (SELECT TRUNC(SYSDATE) FROM Dual) . Но я проверял - Oracle позволяет вставлять в Dual записи, как в и любую "обычную" таблицу.

3 комментария:

Анонимный комментирует...

премию дадут?

или снова скажут, что ты виноват)))

пиши побольше про типографские и издательские дела и нюансы, плиз:)
не только программерские

Анонимный комментирует...

Отлично написано. Сам разрабатываю e-comm сайты и знаю сколько процессов в системе бегает одновременно. Обычно на мелкие errors никто даже не смотрит. Отследить все невозможно, плюс саппорт не всегда знает систему сам. Начинают копать если допустим недосчитались 3000 заказов.

Спасибо за статью!

Z комментирует...

Как системный аналитик скажу, что с такими системамми, с такой структурой БД.... короче руки бы поотрывал. для чего придумали effective_date и Expiration_date.. Наверное для того, чтобы потом битвином выбирать.. А про DUAL расскажешь как-нибудь при встрече. Для меня это новость, что в DUAL вообще что-то можно вставить :)

Ratings by outbrain