次のようにツリー(ネストされたカテゴリ)が保存されています。
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は、トリガーとプロシージャでのトランザクションまたはテーブルのロックを許可していません。ノードを編集する場合は、これらの詳細に注意する必要があります。