SQL问题,相互约束,“外键约束失败”

时间:2012-03-30 17:49:04

标签: mysql sql orm symfony doctrine-orm

我遇到了相互制约的问题。 我希望有两个表,每个表都有另一个约束。

我正在使用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,因为我想在最后一条消息上显示带有信息的线程,并且在没有对最后一条消息的引用的情况下进行(分页)查询是一场彻头彻尾的噩梦... < / p>

谢谢!

4 个答案:

答案 0 :(得分:2)

我能想到的最佳解决方案是在Thread表上为FK添加ON DELETE CASCADE约束。这样,如果删除线程,相关的消息也会自动删除。

同样,您需要在Messages表FK上添加ON DELETE SET NULL约束,这样如果删除了Thread中的最后一条消息,它会在Thread表上将last_message_id设置为NULL。

或者你可以只进行逻辑(软)删除而不是硬删除,这也可以解决问题。

ETA:

现在您已经发布了约束,这是您必须修改的那个:

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

答案 1 :(得分:2)

FOREIGN KEY约束中的循环路径难以处理,您的问题就是一个例子。如果你能避免它们,那就去做吧。这是重新设计表格的一种方法:

首先,在UNIQUE KEY上的表message中添加(thread_id, message_id)(或者将其作为主键,如果Doctrine可以执行此操作。这意味着 - 对于MySQL- {{1}不会自动递增,而是由ORM生成。如果您计划让应用程序直接或通过其他ORM访问数据库,您可能不希望这样。)

然后将message(id)移动到与last_message_id通过化合物message具有一对一关系的新表格。在此表中,(thread_id, message_id)将是唯一的,因此每个线程都只有最后一条消息。

我会在这里编写SQL代码。此页面将帮助您处理可能产生稍微不同结构的Doctrine代码:Compound Primary and Foreign Keys

thread_id

新表,用于存储每个线程的最后一条消息:

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)并适当更改FK约束(如@ Eric的提议)。这在设计阶段不那么繁琐,你可以用一张表来处理。在这种方法中,您必须小心插入和删除的顺序 - 正如您的示例所示。


作为第三种选择,您是否认为您的表中确实需要NULL列?这不是一个计算(从两个表)值,你跳过整个问题?如果它是thread(last_message_id)我会理解这一点,但最后一条消息只是另一个表中的最后一行,按时间排序。您可以通过查询找到它,并且您不需要(再次)将其存储在数据库中,除非有性能原因。

答案 2 :(得分:0)

如果你有相互的约束(即每个消息都有一个线程,每个线程都有一条消息)为什么你不能将它组合成一个表?似乎更有意义

答案 3 :(得分:0)

此解决方案不需要更改架构,顺便说一下,您必须撤消。

如果要删除线程,该线程上的消息也没有意义,所以:

-- 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>

(免责声明:我没有测试这个确切的代码,但是类似的代码)