Сложное ограничение внешнего ключа в SQLAlchemy - PullRequest
9 голосов
/ 06 декабря 2011

У меня есть две таблицы, SystemVariables и VariableOptions.SystemVariables не требует пояснений, а VariableOptions содержит все возможные варианты выбора для всех переменных.

VariableOptions имеет внешний ключ, variable_id, в котором указано, какая переменная являетсявариант для.SystemVariables имеет внешний ключ, choice_id, в котором указывается, какой параметр выбран в данный момент.

Я обошел круговые отношения, используя use_alter на choice_id и post_update наSystemVariables 'choice отношения.Однако я хотел бы добавить дополнительное ограничение базы данных, которое будет гарантировать, что choice_id является действительным (т. Е. Оно ссылается на параметр, который ссылается на него).

Логика, которая мне нужна, при условии, что sysVar представляет строку в таблице SystemVariables, в основном:

VariableOptions[sysVar.choice_id].variable_id == sysVar.id

Но я не знаю, как создать такой тип ограничения с использованием SQL, декларативного или любого другого метода.При необходимости я мог бы просто проверить это на уровне приложения, но я хотел бы иметь это на уровне базы данных, если это возможно.Я использую Postgres 9.1.

Возможно ли это?

Ответы [ 3 ]

11 голосов
/ 06 декабря 2011

Вы можете реализовать это без подвохов .Просто расширяйте внешний ключ , ссылаясь на выбранную опцию, чтобы включить variable_id в дополнение к choice_id.

Вот рабочая демонстрация.Временные столы, так что вы можете легко играть с ним:

CREATE TEMP TABLE systemvariables (
  variable_id integer PRIMARY KEY
, variable    text
, choice_id   integer
);

INSERT INTO systemvariables(variable_id, variable)
VALUES
  (1, 'var1')
, (2, 'var2')
, (3, 'var3');

CREATE TEMP TABLE variableoptions (
  option_id integer PRIMARY KEY
, option text
, variable_id integer REFERENCES systemvariables(variable_id)
                      ON UPDATE CASCADE ON DELETE CASCADE
, UNIQUE (option_id, variable_id) -- needed for the foreign key
);

ALTER TABLE systemvariables
ADD CONSTRAINT systemvariables_choice_id_fk
   FOREIGN KEY (choice_id, variable_id)
   REFERENCES variableoptions(option_id, variable_id);

INSERT INTO variableoptions
VALUES
  (1, 'var1_op1', 1)
, (2, 'var1_op2', 1)
, (3, 'var1_op3', 1)
, (4, 'var2_op1', 2)
, (5, 'var2_op2', 2)
, (6, 'var3_op1', 3);

Выбор соответствующего параметра разрешен:

UPDATE systemvariables SET choice_id = 2 WHERE variable_id = 1;
UPDATE systemvariables SET choice_id = 5 WHERE variable_id = 2;
UPDATE systemvariables SET choice_id = 6 WHERE variable_id = 3;

Но нет выхода из линии:

UPDATE systemvariables SET choice_id = 7 WHERE variable_id = 3;
UPDATE systemvariables SET choice_id = 4 WHERE variable_id = 1;
ERROR:  insert or update on table "systemvariables" violates foreign key constraint "systemvariables_choice_id_fk"
DETAIL: Key (choice_id,variable_id)=(4,1) is not present in table "variableoptions".

Вуаля .Именно то, что вы хотели.


Все ключевые столбцы NOT NULL

Мне кажется, я нашел лучшее решение в следующем ответе:

Решение вопроса @ ypercube в комментариях , чтобы избежать записей с неизвестной ассоциацией, делающих все ключевые столбцы NOT NULL, включая внешние ключи.

Круговая зависимость обычно делает это невозможным.Это классическая проблема куриное яйцо : один из них должен быть первым, чтобы породить другого.Но природа нашла способ обойти это, и Postgres: отложенные ограничения внешнего ключа .

CREATE TEMP TABLE systemvariables (
  variable_id integer PRIMARY KEY
, variable    text
, choice_id   integer NOT NULL
);

CREATE TEMP TABLE variableoptions (
  option_id   integer PRIMARY KEY
, option      text
, variable_id integer NOT NULL
     REFERENCES systemvariables(variable_id)
     ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
, UNIQUE (option_id, variable_id) -- needed for the foreign key
);

ALTER TABLE systemvariables
ADD CONSTRAINT systemvariables_choice_id_fk FOREIGN KEY (choice_id, variable_id)
   REFERENCES variableoptions(option_id, variable_id)
   DEFERRABLE INITIALLY DEFERRED; -- no CASCADING here!

Новые переменные и связанные с ними опции должны быть вставлены вта же транзакция:

BEGIN;

INSERT INTO systemvariables (variable_id, variable, choice_id)
VALUES
  (1, 'var1', 2)
, (2, 'var2', 5)
, (3, 'var3', 6);

INSERT INTO variableoptions (option_id, option, variable_id)
VALUES
  (1, 'var1_op1', 1)
, (2, 'var1_op2', 1)
, (3, 'var1_op3', 1)
, (4, 'var2_op1', 2)
, (5, 'var2_op2', 2)
, (6, 'var3_op1', 3);

END;

Ограничение NOT NULL не может быть отложено, оно вступает в силу немедленно.Но ограничение внешнего ключа может , потому что мы определили его таким образом.Проверяется в конце транзакции, что позволяет избежать проблемы с куриным яйцом.

В этом отредактированном сценарии оба внешних ключа откладываются .Вы можете вводить переменные и параметры в произвольной последовательности.

Возможно, вы заметили, что первое ограничение внешнего ключа не имеет модификатора CASCADE.(Не имеет смысла разрешать каскадные изменения в variableoptions.variable_id.

С другой стороны, второй внешний ключ имеет модификатор CASCADE и, тем не менее, определен как отложенный. Это имеет некоторые ограничения. Руководство :

Ссылочные действия, кроме проверки NO ACTION, не могут быть отложены, даже если ограничение объявлено отложенным.

NO ACTION является значением по умолчанию.

Таким образом, проверки ссылочной целостности на INSERT откладываются, но объявленные каскадные действия на DELETE и UPDATE - нет. Следующее не разрешено в PostgreSQL 9.0или 9.1 , поскольку ограничения применяются после каждого оператора:

UPDATE option SET var_id = 4 WHERE var_id = 5;
DELETE FROM var WHERE var_id = 5;

Подробности:

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

4 голосов
/ 07 декабря 2011

РЕДАКТИРОВАТЬ: Выпуск SQLAlchemy версии 0.7.4 (выпущенный в тот же день, когда я начал спрашивать об этой проблеме, 7/12 / '11!), Содержит новое значение autoincrement для первичных ключей, котороетакже являются частью внешних ключей, ignore_fk.Документация также была расширена, чтобы включить хороший пример того, что я первоначально пытался выполнить.

Все теперь хорошо объяснено здесь .

Если вы хотите увидетькод, который я придумал перед вышеприведенным выпуском, проверьте историю изменений этого ответа.

2 голосов
/ 07 декабря 2011

Мне действительно не нравятся циклические ссылки. Обычно есть способ избежать их. Вот подход:

SystemVariables 
---------------
  variable_id 
  PRIMARY KEY (variable_id)


VariableOptions 
---------------
  option_id 
  variable_id 
  PRIMARY KEY (option_id)
  UNIQUE KEY (variable_id, option_id) 
  FOREIGN KEY (variable_id) 
    REFERENCES SystemVariables(variable_id)


CurrentOptions
--------------
  variable_id 
  option_id 
  PRIMARY KEY (variable_id)
  FOREIGN KEY (variable_id, option_id)
    REFERENCES VariableOptions(variable_id, option_id)
...