递归MySQL触发器,它调用同一个表和相同的触发器

时间:2012-07-03 22:38:17

标签: mysql sql triggers

我正在为一个php网站写一个简单的论坛。我正在尝试计算每个类别的帖子数。现在,类别可以属于另一个类别,其中根类别被定义为具有NULL parent_category_id。使用此体系结构,类别可以具有无限数量的子类别,并使表结构保持相当简单。

为了简单起见,我们可以说类别表有3个字段:category_idparent_category_idpost_count。我不认为剩下的数据库结构是相关的,所以我现在就把它留下来。

另一个触发器是调用类别表导致此触发器运行。我想要的是更新帖子计数,然后递归遍历每个父类别,增加帖子数。

DELIMITER $$

CREATE TRIGGER trg_update_category_category_post_count BEFORE UPDATE ON categories FOR EACH ROW
BEGIN
IF OLD.post_count != NEW.post_count THEN
  IF OLD.post_count < NEW.post_count THEN
    UPDATE categories SET post_count = post_count + 1 WHERE categories.category_id = NEW.parent_category_id;
  ELSEIF OLD.post_count > NEW.post_count THEN
    UPDATE categories SET post_count = post_count - 1 WHERE categories.category_id = NEW.parent_category_id;
  END IF;
END IF;
END $$

DELIMITER ;

我得到的错误是:

#1442 - Can't update table 'categories' in stored function/trigger because it is already used by statement which invoked this stored function/trigger. 

我认为你可以在每个页面加载上执行count()来计算总帖子,但在大型论坛上,这会减慢事情的速度,如此处多次讨论(例如Count posts with php or store in database)。因此,为了将来打样,我将帖子计数存储在表格中。为了更进一步,我认为我会使用触发器更新这些计数而不是PHP。

我理解MySQL在同一个正在更新的表上运行触发器是有限制的,这是导致此错误的原因(即停止无限循环)但在这种情况下,一旦到达类别,循环肯定会停止一个NULL parent_category_id?无论是调整此触发器还是完全不同的东西,都必须有某种解决方案。感谢。

编辑我很欣赏这可能不是最好的做事方式,但这是我能想到的最好的方法。我想如果你把父母类别更改为另一个类别会使事情变得混乱,但这可以通过另一个重新同步一切的触发器来解决。我对如何解决这个问题的其他建议持开放态度。

3 个答案:

答案 0 :(得分:2)

我通常建议不要使用触发器,除非你确实真的需要;递归触发器是一种很好的方法来引入真正难以重现的错误,并要求开发人员理解一个看似简单的操作的副作用 - “我所做的就是在类别表中插入一条记录,现在整个数据库都已锁定“起来。我已经看过几次这样的事情 - 没有人做过任何错误或愚蠢的事情,这只是一种带有副作用的风险。

所以,一旦你证明你需要,我只会求助于触发器;我不是依赖于基于通用性的陌生人的意见,而是设置了一个测试环境,下载了几百万个测试记录,并尝试优化“计算页面加载的帖子”解决方案,以便它可以工作。

可能对此有帮助的数据库设计是Joe Celko's "nested set" schema - 这需要一段时间才能完成,但查询速度非常快。

只有当你知道自己有一个问题,除了通过预先计算帖子数以外,你真的无法解决这个问题,我会考虑基于触发器的方法。我将“帖子计数”分成一个单独的表格;这可以让你的设计更清洁,并且应该绕过递归触发器问题。

答案 1 :(得分:1)

最简单的解决方案是获取每个类别的所有帖子,然后使用脚本/编程语言将它们链接在一起:

例如在php中:

<?php
// category: id, parent, name
// posts: id, title, message
$sql = "select *, count(posts.id) From category left join posts ON posts.cat = category.id Group by category.id";
$query = mysql_query($sql);
$result = array();
while($row = mysql_fetch_assoc($query)){
  $parent = $row['parent'] == null ? 0 : $row['parent'];
  $result[$parent][] = $row;
}
recur_count(0);
var_dump($result);
function recur_count($depth){
    global $result;

    var_dump($result[$depth],$depth); 
       foreach($result[$depth] as $id =>  $o){
          $count = $o['count'];
          if(isset($result[$o['id']])){
             $result[$depth][$id]['count']  += recur_count($o['id']);
           }   

       }   
        return $count;

}

答案 2 :(得分:0)

好的,对于任何想知道我是如何解决这个问题的人,我都使用了触发器和PHP的混合物。

我没有让每个类别更新它的父级,而是将它留给以下结构:帖子更新它的线程,然后一个帖子用帖子计数更新它的类别。

然后我使用PHP从数据库中提取所有类别,并使用以下内容循环添加每个帖子计数值:

function recursiveCategoryCount($categories)
{
    $count = $categories['category']->post_count;

    if(!is_null($categories['children']))
        foreach($categories['children'] as $child)
            $count += recursiveCategoryCount($child);

    return $count;  
}

最糟糕的是,不是PHP在每个页面加载时添加每个帖子,它只会增加总类别帖子(取决于您所在树中的哪个节点)。这应该是非常有效的,因为您将总计算从1000s减少到10s或100s,具体取决于您的类别数量。我还建议每周运行一个脚本来重新计算帖子计数,以防它们变得不同步,就像phpBB一样。如果我遇到使用触发器的问题,那么我会将该功能移到代码中。感谢大家的建议。