Модель Rails с внешним ключом к самой себе

У меня есть схема БД Oracle, которая включает в себя таблицу «пользователи». Эта таблица имеет два ненулевых внешних ключа к редактору и создателю, которые также являются пользователями.

Дамп схемы выглядит так:

  create_table "users", :force => true do |t|
    t.integer "creator_id",                :precision => 38, :scale => 0, :null => false
    t.integer "editor_id",                 :precision => 38, :scale => 0, :null => false
  end

  add_foreign_key "users", "users", :column => "creator_id", :name => "r_user_creatorid", :dependent => :nullify
  add_foreign_key "users", "users", :column => "editor_id", :name => "r_user_editorid", :dependent => :nullify

Моя модель пользователя выглядит следующим образом:

class User < ActiveRecord::Base
  belongs_to :creator, :class_name => "User"
  belongs_to :editor, :class_name => "User"

  validates_presence_of :creator, :editor
end

Проблема возникает, когда я пытаюсь сохранить первого пользователя. Других пользователей пока не существует, но у меня не может быть пустого editor_id или Creator_id. Если я попытаюсь установить редактор и создатель для себя, я получу переполнение стека.

Теоретически логично, что у всех пользователей (кроме первого) есть создатель и редактор. Есть ли способ сделать это без временного удаления ненулевого ограничения?


person AaronThomson    schedule 05.04.2011    source источник
comment
Подобные проблемы типа курицы и яйца делают внешние ключи немного неприятными. Большинство приложений Rails не используют их по соглашению, обходя большую часть этой головной боли и принимая на себя потенциальный риск несогласованных ссылок.   -  person tadman    schedule 05.04.2011


Ответы (2)


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

«Теоретически имеет смысл, что у всех пользователей (кроме первого) есть создатель и редактор. Есть ли способ добиться этого, не удаляя временно ненулевое ограничение?»

Суть в том, что если у первого пользователя нет СОЗДАТЕЛЯ или РЕДАКТОРА, тогда нет «временного»: вам нужно отказаться от обязательного ограничения. Если вы сделаете это, проблема с ограничением рекурсивного внешнего ключа исчезнет.


Альтернативой является введение того, что Аристотель называл Перводвигателем, Пользователем, Творцом которого является он сам. Учитывая эту таблицу:

create table t72
( userid number not null
  , creator number not null
  , editor number not null
  , constraint t72_pk primary key (userid)
  , constraint t72_cr_fk foreign key (creator) 
                references t72 (userid)
  , constraint t72_ed_fk foreign key (editor) 
                references t72 (userid)
)
/

создать такого пользователя довольно просто:

SQL> insert into t72 values (1,1,1)
  2  /

1 row created.

SQL> commit;

Commit complete.

SQL>

Так почему же это не каноническое решение? Ну, это приводит к немного странной модели данных, которая может создать хаос с иерархическими запросами, как только мы добавим еще несколько пользователей.

SQL> select lpad(' ', level-1)|| u.userid as userid
  2          , u.name
  3          , u.editor
  4  from t72 u
  5  connect by
  6     prior userid = editor
  7  start with userid=1
  8  /
ERROR:
ORA-01436: CONNECT BY loop in user data



no rows selected

SQL> 

В основном базе данных не нравится, что USERID является ее собственным редактором. Однако есть обходной путь — ключевое слово NOCYCLE (введено в версии 10g). Это говорит базе данных игнорировать циклические ссылки в иерархии:

SQL> select lpad(' ', level-1)|| u.userid as userid
  2          , u.name
  3          , u.editor
  4  from t72 u
  5  connect by nocycle
  6     prior userid = editor
  7  start with userid=1
  8  /

USERID     NAME           EDITOR
---------- ---------- ----------
1          ONE                 1
 2         TWO                 1
  3        THREE               2
  4        FOUR                2
  5        FIVE                2
  6        SIX                 2
   7       SEVEN               6

7 rows selected.

SQL>

Здесь это не имеет значения, потому что данные по-прежнему правильно иерархичны. Но что произойдет, если мы сделаем это:

SQL> update t72 set editor = 7
  2  where userid = 1
  3  /

1 row updated.

SQL> 

Мы теряем связь (1 -> 7). Мы можем использовать псевдостолбец CONNECT_BY_ISNOCYCLE, чтобы увидеть, какая строка циклически повторяется.

SQL> select lpad(' ', level-1)|| u.userid as userid
  2          , u.name
  3          , u.editor
  4          , connect_by_iscycle
  5  from t72 u
  6  connect by nocycle
  7     prior userid = editor
  8  start with userid=1
  9  /

USERID     NAME           EDITOR CONNECT_BY_ISCYCLE
---------- ---------- ---------- ------------------
1          ONE                 7                  0
 2         TWO                 1                  0
  3        THREE               2                  0
  4        FOUR                2                  0
  5        FIVE                2                  0
  6        SIX                 2                  0
   7       SEVEN               6                  1

7 rows selected.

SQL>  

Oracle имеет множество дополнительных функций, облегчающих работу с иерархическими данными в чистом SQL. Это все есть в документации. Узнайте больше.

person APC    schedule 05.04.2011

Я бы подумал, что вы уберете ограничение NOT NULL (т. е. разрешите первому пользователю иметь NULL для создателя и редактора).

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

CONSTRAINT creator_required CHECK (creator IS NOT NULL OR userid = 1)
CONSTRAINT editor_required CHECK (editor IS NOT NULL OR userid = 1)
person Jeffrey Kemp    schedule 05.04.2011