Проблема SQL, взаимные ограничения, «ограничение внешнего ключа не выполняется» - PullRequest
2 голосов
/ 30 марта 2012

У меня проблема с взаимными ограничениями.Я хочу иметь две таблицы, каждая из которых имеет ограничение на другую.

Я работаю с Doctrine2 (но это не связано с проблемой), вот мой упрощенный код:

SQL:

CREATE TABLE IF NOT EXISTS `thread` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `last_message_id` int(11) DEFAULT NULL,
  `subject` varchar(255) NOT NULL
  PRIMARY KEY (`id`),
  UNIQUE KEY `UNIQ_C023F2BBBA0E79C3` (`last_message_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

ALTER TABLE `thread`
ADD CONSTRAINT `FK_C023F2BBBA0E79C3` FOREIGN KEY (`last_message_id`) REFERENCES `message` (`id`);

CREATE TABLE IF NOT EXISTS `message` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `thread_id` int(11) DEFAULT NULL,
  `body` longtext NOT NULL
  PRIMARY KEY (`id`),
  KEY `IDX_9E4E8B5FA76ED395` (`user_id`),
  KEY `IDX_9E4E8B5FE2904019` (`thread_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

ALTER TABLE `message`
ADD CONSTRAINT `FK_9E4E8B5FE2904019` FOREIGN KEY (`thread_id`) REFERENCES `thread` (`id`) ON DELETE CASCADE;

Отображение Doctrine2 (которое сгенерировало приведенный выше код SQL):

<?php

class Thread
{
    /* @ORM\OneToOne() */
    private $lastMessage;
}

class Message
{
    /* @ORM\ManyToOne() */
    private $thread;
}

И когда я пытаюсь удалить поток или сообщение, я получаю (логически) ошибку: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails ('thread', CONSTRAINT 'FK_C023F2BBBA0E79C3' FOREIGN KEY ('last_message_id') REFERENCES 'message' ('id'))

Итак, есть ли способ избежать этой ошибки?Или я должен забыть взаимные ограничения?Что-нибудь?

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

Спасибо!

Ответы [ 4 ]

2 голосов
/ 30 марта 2012

Лучшее решение, которое я могу придумать, это добавить ограничение ON DELETE CASCADE в FK таблицы Thread.Таким образом, если вы удалите тему, соответствующие сообщения также будут автоматически удалены.

Аналогичным образом, вам нужно добавить ограничение ON DELETE SET NULL в таблицу сообщений FK, чтобы при удалении последнего сообщения в потоке для last_message_id в таблице потоков было задано значение NULL.

Или вы можете просто сделать логическое (мягкое) удаление вместо жесткого удаления, что также решит проблему.

ETA:

Теперь, когда вы опубликовали ограничения, этотот, который вы должны изменить:

ALTER TABLE `thread`
ADD CONSTRAINT `FK_C023F2BBBA0E79C3` FOREIGN KEY (`last_message_id`) 
REFERENCES `message` (`id`) ON DELETE SET NULL;
2 голосов
/ 30 марта 2012

Круговые пути в ограничениях FOREIGN KEY трудны для решения, и ваша проблема является примером. Если вы можете избежать их, сделайте это. Вот один из способов переделать ваши столы:

Сначала добавьте UNIQUE KEY в таблицу message на (thread_id, message_id) (или сделайте его Первичным ключом, если Doctrine может это сделать. Для MySQL это будет означать, что message(id) не будет автоматически увеличено) но произведенный ORM. Вы можете не захотеть этого, если планируете иметь приложения, которые обращаются к базе данных напрямую или через другие ORM).

Затем переместите last_message_id в новую таблицу, которая имеет отношение 1 к 1 с message, хотя составная (thread_id, message_id). В этой таблице thread_id будет уникальным, поэтому у каждого потока будет только одно последнее сообщение.

Я напишу код SQL здесь. Эта страница поможет вам с кодом Doctrine, который может иметь несколько иную структуру: Составные первичные и внешние ключи

CREATE TABLE IF NOT EXISTS `thread` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  ---`last_message_id` int(11) DEFAULT NULL,   --- REMOVED: last_message 
  `subject` varchar(255) NOT NULL
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;


CREATE TABLE IF NOT EXISTS `message` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `thread_id` int(11) NOT NULL,                 --- why was it NULL ?
  `body` longtext NOT NULL
  PRIMARY KEY (`id`),
  KEY `IDX_9E4E8B5FA76ED395` (`user_id`),
  ---KEY `IDX_9E4E8B5FE2904019` (`thread_id`),  --- REMOVED, not needed any more 
                                                --- because we have a this key
  UNIQUE KEY (thread_id, id)                 --- ADDED, needed for the FK below
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

ALTER TABLE `message`
ADD CONSTRAINT `FK_9E4E8B5FE2904019` 
  FOREIGN KEY (`thread_id`) 
  REFERENCES `thread` (`id`) 
  ON DELETE CASCADE;

И новая таблица, для хранения последнего сообщения для каждой темы:

CREATE TABLE IF NOT EXISTS `thread_last_message` (
  `message_id` int(11) NOT NULL,
  `thread_id` int(11) NOT NULL,            
  PRIMARY KEY (`thread_id`),
  KEY (`thread_id`, message_id`),
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

ALTER TABLE `thread_last_message`              --- which just means 
ADD CONSTRAINT `FK_something`                  --- that every 
  FOREIGN KEY (`thread_id`, `message_id`)      --- thread's last message
  REFERENCES `message` (`thread_id`, `id`)     --- is a message
  ON DELETE CASCADE;

Другая возможность состоит в том, чтобы иметь столбец thread(last_message_id) NULL и соответствующим образом изменять ограничения FK (как предложение @ Eric). Это менее суетно на этапе проектирования, и вам приходится иметь дело с одним столом меньше. В этом подходе вы должны быть осторожны с порядком вставок и удалений - как показывает ваш пример.


Как третий вариант, подумали ли вы, действительно ли вам нужен столбец thread(last_message_id) в вашей таблице? Не может ли это быть вычисленное (из двух таблиц) значение, и вы пропустите всю проблему? Если бы это было best_message_id, я бы это понял, но последнее сообщение - это только последняя строка в другой таблице, упорядоченная по времени. Вы можете найти это с помощью запроса, и вам не нужно сохранять его (снова) в базе данных, если нет причин для повышения производительности.

0 голосов
/ 22 ноября 2013

Это решение не требует изменения схемы, которую, кстати, вам придется отменить.

Если вы хотите удалить поток, сообщения в этом потоке также не имеют смысла, поэтому:

-- break one end of the mutual constraint
update thread set last_message_id = NULL where id = <thread_id_to_delete>;
delete from message where thread_id = <thread_id_to_delete>
delete from threads where id = <thread_id_to_delete>

(Отказ от ответственности: я не проверял этот точный код, но похожийодин)

0 голосов
/ 30 марта 2012

Если у вас есть взаимные ограничения (т.е. каждое сообщение имеет поток, а каждый поток имеет сообщение), почему вы не можете объединить это в одну таблицу? Кажется, в этом смысле больше смысла

...