我遇到了相互制约的问题。 我希望有两个表,每个表都有另一个约束。
我正在使用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>
谢谢!
答案 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>
(免责声明:我没有测试这个确切的代码,但是类似的代码)