6

次のようにツリー(ネストされたカテゴリ)が保存されています。

CREATE TABLE `category` (
  `category_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `category_name` varchar(100) NOT NULL,
  `parent_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`category_id`),
  UNIQUE KEY `category_name_UNIQUE` (`category_name`,`parent_id`),
  KEY `fk_category_category1` (`parent_id`,`category_id`),
  CONSTRAINT `fk_category_category1` FOREIGN KEY (`parent_id`) REFERENCES `category` (`category_id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci

クライアント側の言語(PHP)にノード情報(子+親)をフィードして、メモリ内にツリーを構築できるようにする必要があります。PHPコードを微調整することはできますが、すべての親が子の前に来るような順序で行を取得できれば、操作ははるかに簡単になると思います。各ノードのレベルを知っていれば、それを行うことができます。

SELECT category_id, category_name, parent_id
FROM category
ORDER BY level -- No `level` column so far :(

ノードレベルを計算する方法(表示、保存されたルーチンなど)を考えられますか?リアルタイムでなくても大丈夫だと思います。ノードの変更時に再計算する必要があります。

最初の更新:これまでの進捗状況

Amarghoshによるフィードバックに基づいて、これらのトリガーを作成しました。

DROP TRIGGER IF EXISTS `category_before_insert`;

DELIMITER //

CREATE TRIGGER `category_before_insert` BEFORE INSERT ON `category` FOR EACH ROW BEGIN
    IF NEW.parent_id IS NULL THEN
        SET @parent_level = 0;
    ELSE
        SELECT level INTO @parent_level
        FROM category
        WHERE category_id = NEW.parent_id;
    END IF;

    SET NEW.level = @parent_level+1;
END//

DELIMITER ;


DROP TRIGGER IF EXISTS `category_before_update`;

DELIMITER //

CREATE TRIGGER `category_before_update` BEFORE UPDATE ON `category` FOR EACH ROW BEGIN
    IF NEW.parent_id IS NULL THEN
        SET @parent_level = 0;
    ELSE
        SELECT level INTO @parent_level
        FROM category
        WHERE category_id = NEW.parent_id;
    END IF;

    SET NEW.level = @parent_level+1;
END//

DELIMITER ;

挿入や変更には問題なく機能するようです。ただし、削除には機能しません。行がON UPDATE CASCADE外部キーから更新された場合、MySQLサーバーはトリガーを起動しません。

最初の明白なアイデアは、削除の新しいトリガーを作成することです。ただし、テーブルのトリガーは、categoriesこの同じテーブルの他の行を変更することはできません。

DROP TRIGGER IF EXISTS `category_after_delete`;

DELIMITER //

CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
    /*
     * Raises an error, see below
     */
    UPDATE category SET parent_id=NULL
    WHERE parent_id = OLD.category_id;
END//

DELIMITER ;

エラー:

グリッド編集エラー:SQLエラー(1442):このストアド関数/トリガーを呼び出したステートメントによって既に使用されているため、ストアド関数/トリガーのテーブル'category'を更新できません。

2番目の更新:実用的な解決策(間違っていることが証明されない限り)

私の最初の試みはかなり賢明でしたが、解決できない問題を見つけました。トリガーから一連の操作を起動すると、MySQLは同じテーブルの他の行を変更できなくなります。ノードの削除ではすべての子孫のレベルを調整する必要があるため、壁にぶつかりました。

最後に、ここからコードを使用してアプローチを変更しました。ノードが変更されたときに個々のレベルを修正するのではなく、すべてのレベルを計算するコードがあり、編集するたびにトリガーします。計算が遅く、データのフェッチには非常に複雑なクエリが必要なため、テーブルにキャッシュします。私の場合、エディションはまれであるはずなので、これは許容できる解決策です。

1.キャッシュレベルの新しいテーブル:

CREATE TABLE `category_level` (
  `category_id` int(10) NOT NULL,
  `parent_id` int(10) DEFAULT NULL, -- Not really necesary
  `level` int(10) NOT NULL,
  PRIMARY KEY (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci

2.レベルを計算するヘルパー関数

それがどのように機能するかを本当に理解したとしても、それ自体では有用なものは何も返されません。代わりに、セッション変数にデータを格納します。

CREATE FUNCTION `category_connect_by_parent_eq_prior_id`(`value` INT) RETURNS int(10)
    READS SQL DATA
BEGIN
    DECLARE _id INT;
    DECLARE _parent INT;
    DECLARE _next INT;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET @category_id = NULL;

    SET _parent = @category_id;
    SET _id = -1;

    IF @category_id IS NULL THEN
        RETURN NULL;
    END IF;

    LOOP
        SELECT  MIN(category_id)
        INTO    @category_id
        FROM    category
        WHERE   COALESCE(parent_id, 0) = _parent
            AND category_id > _id;
        IF @category_id IS NOT NULL OR _parent = @start_with THEN
            SET @level = @level + 1;
            RETURN @category_id;
        END IF;
        SET @level := @level - 1;
        SELECT  category_id, COALESCE(parent_id, 0)
        INTO    _id, _parent
        FROM    category
        WHERE   category_id = _parent;
    END LOOP;
END

3.再計算プロセスを開始する手順

基本的に、ヘルパー関数によって支援されるレベルを取得する複雑なクエリをカプセル化します。

CREATE PROCEDURE `update_category_level`()
    SQL SECURITY INVOKER
BEGIN
    DELETE FROM category_level;

    INSERT INTO category_level (category_id, parent_id, level)
    SELECT hi.category_id, parent_id, level
    FROM (
        SELECT category_connect_by_parent_eq_prior_id(category_id) AS category_id, @level AS level
        FROM (
            SELECT  @start_with := 0,
                @category_id := @start_with,
                @level := 0
            ) vars, category
        WHERE @category_id IS NOT NULL
        ) ho
    JOIN category hi ON hi.category_id = ho.category_id;
END

4.キャッシュテーブルを最新の状態に保つためのトリガー

CREATE TRIGGER `category_after_insert` AFTER INSERT ON `category` FOR EACH ROW BEGIN
    call update_category_level();
END

CREATE TRIGGER `category_after_update` AFTER UPDATE ON `category` FOR EACH ROW BEGIN
    call update_category_level();
END

CREATE TRIGGER `category_after_delete` AFTER DELETE ON `category` FOR EACH ROW BEGIN
    call update_category_level();
END

5.既知の問題

  • ノードが頻繁に変更される場合は、かなり最適ではありません。
  • MySQLは、トリガーとプロシージャでのトランザクションまたはテーブルのロックを許可していません。ノードを編集する場合は、これらの詳細に注意する必要があります。
4

3 に答える 3

3

ここには、階層内のレベル、リーフ ノード、ループなどを識別する方法を含む、MySQL の階層クエリに関する優れた一連の記事があります。

于 2010-06-09T09:40:43.803 に答える
2

サイクルがない場合(グラフではなく常にツリーである場合level)、デフォルトでゼロ(最上位)に設定されているフィールドと、レベルを(親のレベル+1)を更新するたびにparent_id

CREATE TRIGGER setLevelBeforeInsert BEFORE INSERT ON category
FOR EACH ROW
BEGIN
IF NEW.parent_id IS NOT NULL THEN
SELECT level INTO @pLevel FROM category WHERE id = NEW.parent_id;
SET NEW.level = @pLevel + 1;
ELSE 
SET NEW.level = 0;
END IF;
END;
于 2010-06-09T09:26:29.437 に答える
0

これまでのところコラムはありませんlevel:(

うーん * 肩をすくめる *
このレベル フィールドを手動で作成したところです。
マテリアライズド パスのように、挿入後に更新が 1 回だけで、これらすべての派手なトリガーがないとします。たとえば、第3レベルの
ようになるフィールド000000100000210000022

そのため、メモリ内にツリーを構築できます。

テーブル全体を PHP に取り込もうとしているのであれば、ここで問題はないと思います。少し再帰関数を使用すると、ネストされた配列ツリーを取得できます。

PHPコードを微調整することはできますが、操作はもっと簡単になると思います

まあまあ。
あなたがこれまでに得たコードは、私には「とても単純」ではないようです:)

于 2010-06-10T08:18:20.320 に答える