2011-09-23 81 views
7

學院有不同的組織他們的部門的方式。有些學校去School -> Term -> Department。其他人有中間步驟,最長的是School -> Sub_Campus -> Program -> Term -> Division -> Department有趣的樹/分層數據結構問題

School,TermDepartment是唯一一個總是存在於學校的「樹」部門。這些類別的順序不會改變,我給你的第二個例子是最長的。每一步都是1:N的關係。

現在,我不知道如何建立表之間的關係。例如,哪些列在Term?其母公司可能是Program,Sub_CampusSchool。哪一個取決於學校的系統。我可以想象設置Term表有所有這些外鍵(這些都將默認爲NULL),但我不確定這是在這裏做事的規範方式。

+0

我不知道你問 - 你想一個解決將多個不同的分層數據模型擬合到相同的數據庫實現中的問題?或者說明如何實現單個層次模型的解決方案? –

+0

任何一個,以較適合此問題爲準。 – babonk

回答

3

以下是一種設計可能性:

此選項利用您的特殊約束。基本上,通過引入通用節點,將所有層次概括爲最長形式的層次。如果學校沒有「分校」,那麼只需將其分配一個名爲「主」的通用分校。例如,School -> Term -> Department可以被認爲與School -> Sub_Campus = Main -> Program=Main -> Term -> Division=Main -> Department相同。在這種情況下,當學校沒有該節點時,我們將一個名爲「Main」的節點指定爲默認節點。現在,您可以爲這些通用節點指定一個布爾標誌屬性,以指示它們只是佔位符,並且此標誌可以讓您在中間層或UX中根據需要過濾掉它。

這種設計將允許您像往常一樣利用所有的關係約束,並簡化代碼中缺少節點類型的處理。

3

我建議你最好使用一個通用表格,包含id的實體字段和自引用字段。

每個相關表將包含指向實體的id(1:1)的字段。在某種程度上,每個表都是實體表的一個孩子。

+0

_parent_字段沒問題,但它需要特定於數據庫的支持才能執行分層查詢(例如,Oracle的「通過事先結構連接」)。使查詢更容易的另一種方法是在單個編碼列中表示層次結構,其中子樹被定義爲該列的字符串前綴。 –

+0

是的,但那不是問題,並且增加這不是很大的交易。我最近爲這個人寫了關於這個問題http://stackoverflow.com/questions/7181489/problem-with-hierarchical-database-model/7181872#7181872 – MarianP

+0

@evil otto:錯了。數據庫_model_可以是遞歸的,但是從數據庫表中提取數據的_query_可以簡單地迭代「直到找不到更多的父記錄」。這是大多數客戶端應用程序無論如何都會這樣做的方式。 – wildplasser

1

我將首先討論實現單層次模型(只是1:N關係)的關係。

讓我們用你的例子School -> Term -> Department

這裏的代碼,我生成使用MySQLWorkbench(我刪除了一些東西,以使其更清晰):

-- ----------------------------------------------------- 
-- Table `mydb`.`school` 
-- ----------------------------------------------------- 
-- each of these tables would have more attributes in a real implementation 
-- using varchar(50)'s for PKs because I can -- :) 

CREATE TABLE IF NOT EXISTS `mydb`.`school` (
    `school_name` VARCHAR(50) NOT NULL , 
    PRIMARY KEY (`school_name`) 
); 

-- ----------------------------------------------------- 
-- Table `mydb`.`term` 
-- ----------------------------------------------------- 
CREATE TABLE IF NOT EXISTS `mydb`.`term` (
    `term_name` VARCHAR(50) NOT NULL , 
    `school_name` VARCHAR(50) NOT NULL , 
    PRIMARY KEY (`term_name`, `school_name`) , 
    FOREIGN KEY (`school_name`) 
    REFERENCES `mydb`.`school` (`school_name`) 
); 

-- ----------------------------------------------------- 
-- Table `mydb`.`department` 
-- ----------------------------------------------------- 
CREATE TABLE IF NOT EXISTS `mydb`.`department` (
    `dept_name` VARCHAR(50) NOT NULL , 
    `term_name` VARCHAR(50) NOT NULL , 
    `school_name` VARCHAR(50) NOT NULL , 
    PRIMARY KEY (`dept_name`, `term_name`, `school_name`) , 
    FOREIGN KEY (`term_name` , `school_name`) 
    REFERENCES `mydb`.`term` (`term_name` , `school_name`) 
); 

下面是數據模型的MySQLWorkbench版本:
MySQLWorkbench version

,你可以請參見school,位於層次結構的頂部,它只有school_name作爲其關鍵字,而department包含三部分關鍵字,包括其所有父項的關鍵字。

這個解決方案的關鍵點

  • 採用自然鍵 - 但可以重構使用代理鍵(SO question - 對多列外鍵約束UNIQUE一起)每一級
  • 的嵌套添加一列到鍵
  • 每個表的PK是上面的表的整個PK加上該表的特定附加列

現在爲您的問題的第二部分。

我對問題的解釋
有一個分層數據模型。然而,一些應用程序需要所有表格,而其他應用程序僅使用其中一些表格,而忽略其他表格。我們希望能夠實現1個單一數據模型,並將其用於這兩種情況。

您可以使用上面給出的解決方案,並且,正如ShitalShah提到的那樣,將默認值添加到任何不會使用的表中。讓我們來看一些數據,例如,使用上面給出的模型,在這裏我們只希望保存SchoolDepartment信息(無Term S):

+-------------+ 
| school_name | 
+-------------+ 
| hogwarts | 
| uCollege | 
| uMatt  | 
+-------------+ 
3 rows in set (0.00 sec) 

+-----------+-------------+ 
| term_name | school_name | 
+-----------+-------------+ 
| default | hogwarts | 
| default | uCollege | 
| default | uMatt  | 
+-----------+-------------+ 
3 rows in set (0.00 sec) 

+-------------------------------+-----------+-------------+ 
| dept_name      | term_name | school_name | 
+-------------------------------+-----------+-------------+ 
| defense against the dark arts | default | hogwarts | 
| potions      | default | hogwarts | 
| basket-weaving    | default | uCollege | 
| history of magic    | default | uMatt  | 
| science      | default | uMatt  | 
+-------------------------------+-----------+-------------+ 
5 rows in set (0.00 sec) 

要點

  • 有一個默認值在school中的每個值在term中 - 如果您在應用程序不需要的層次結構深處有一張表,可能會很煩人
  • 由於表架構不會更改,可以使用相同的查詢
  • 查詢很容易編寫和便攜式
  • SO似乎認爲default應區別

有色還有另外一個解決方案,在數據庫中存儲的樹木。比爾卡爾文討論它here, starting around slide 49,但我不認爲這是你想要的解決方案。卡爾文的解決方案適用於任何規模的樹木,而您的例子似乎相對靜態。此外,他的解決方案還帶有自己的一系列問題(但不是全部?)。


我希望能幫到你的問題。

+0

馬特,你似乎沒有看到Sub_Campuses,Programs和Divisions? – babonk

+0

@babonk你是對的;我想節省空間,並保持示例不會太長。它是不是太明顯,它不清楚它如何映射到OP? –

1

對於在關係數據庫中擬合分層數據的一般問題,常用的解決方案是鄰接表(如您的示例中的父子鏈接)和nested sets。正如維基百科文章中指出的那樣,Oracle的Tropashko提出了一個替代方案nested interval solution,但它仍然相當模糊。

您的情況的最佳選擇取決於您將如何查詢結構以及您正在使用哪個數據庫。櫻桃採摘的文章:

查詢使用嵌套組可以預計會比查詢更快 使用存儲過程來遍歷鄰接表,因此是缺乏本地遞歸查詢 構建數據庫 更快選項比如MySQL

但是:

嵌套組是插入很慢,因爲它需要更新LFT 和rgt插入後表中的所有記錄。這會導致 大量的數據庫抖動,因爲許多行被重寫,索引 重建。

此外,根據您的結構將如何進行查詢,你可以選擇一個NoSQL的風格規格化Department表,與nullable外鍵的所有可能的父母,完全避免遞歸查詢。

3
-- Enforcing a taxonomy by self-referential (recursive) tables. 
-- Both the classes and the instances have a recursive structure. 
-- The taxonomy is enforced mostly based on constraints on the classes, 
-- the instances only need to check that {their_class , parents_class} 
-- form a valid pair. 
-- 
DROP schema school CASCADE; 
CREATE schema school; 

CREATE TABLE school.category 
    (id INTEGER NOT NULL PRIMARY KEY 
    , category_name VARCHAR 
); 
INSERT INTO school.category(id, category_name) VALUES 
    (1, 'School') 
    , (2, 'Sub_campus') 
    , (3, 'Program') 
    , (4, 'Term') 
    , (5, 'Division') 
    , (6, 'Department') 
    ; 

-- This table contains a list of all allowable {child->parent} pairs. 
-- As a convention, the "roots" of the trees point to themselves. 
-- (this also avoids a NULL FK) 
CREATE TABLE school.category_valid_parent 
    (category_id INTEGER NOT NULL REFERENCES school.category (id) 
    , parent_category_id INTEGER NOT NULL REFERENCES school.category (id) 
); 
ALTER TABLE school.category_valid_parent 
    ADD PRIMARY KEY (category_id, parent_category_id) 
    ; 

INSERT INTO school.category_valid_parent(category_id, parent_category_id) 
    VALUES 
    (1,1) -- school -> school 
    , (2,1) -- subcampus -> school 
    , (3,1) -- program -> school 
    , (3,2) -- program -> subcampus 
    , (4,1) -- term -> school 
    , (4,2) -- term -> subcampus 
    , (4,3) -- term -> program 
    , (5,4) -- division --> term 
    , (6,4) -- department --> term 
    , (6,5) -- department --> division 
    ; 

CREATE TABLE school.instance 
    (id INTEGER NOT NULL PRIMARY KEY 
    , category_id INTEGER NOT NULL REFERENCES school.category (id) 
    , parent_id INTEGER NOT NULL REFERENCES school.instance (id) 
    -- NOTE: parent_category_id is logically redundant 
    -- , but needed to maintain the constraint 
    -- (without referencing a third table) 
    , parent_category_id INTEGER NOT NULL REFERENCES school.category (id) 
    , instance_name VARCHAR 
);  -- Forbid illegal combinations of {parent_id, parent_category_id} 
ALTER TABLE school.instance ADD CONSTRAINT valid_cat UNIQUE (id,category_id); 
ALTER TABLE school.instance 
    ADD FOREIGN KEY (parent_id, parent_category_id) 
     REFERENCES school.instance(id, category_id); 
    ; 
    -- Forbid illegal combinations of {category_id, parent_category_id} 
ALTER TABLE school.instance 
    ADD FOREIGN KEY (category_id, parent_category_id) 
     REFERENCES school.category_valid_parent(category_id, parent_category_id); 
    ; 

INSERT INTO school.instance(id, category_id 
    , parent_id, parent_category_id 
    , instance_name) VALUES 
    -- Zulo 
    (1,1,1,1, 'University of Utrecht') 
    , (2,2,1,1, 'Uithof') 
    , (3,3,2,2, 'Life sciences') 
    , (4,4,3,3, 'Bacherlor') 
    , (5,5,4,4, 'Biology') 
    , (6,6,5,5, 'Evolutionary Biology') 
    , (7,6,5,5, 'Botany') 
    -- Nulo 
    , (11,1,11,1, 'Hogeschool Utrecht') 
    , (12,4,11,1, 'Journalistiek') 
    , (13,6,12,4, 'Begrijpend Lezen') 
    , (14,6,12,4, 'Typvaardigheid') 
    ; 

    -- try to insert an invalid instance 
INSERT INTO school.instance(id, category_id 
    , parent_id, parent_category_id 
    , instance_name) VALUES 
    (15, 6, 3,3, 'Procreation'); 

WITH RECURSIVE re AS (
    SELECT i0.parent_id AS pa_id 
    , i0.parent_category_id AS pa_cat 
    , i0.id AS my_id 
    , i0.category_id AS my_cat 
    FROM school.instance i0 
    WHERE i0.parent_id = i0.id 
    UNION 
    SELECT i1.parent_id AS pa_id 
    , i1.parent_category_id AS pa_cat 
    , i1.id AS my_id 
    , i1.category_id AS my_cat 
    FROM school.instance i1 
    , re 
    WHERE re.my_id = i1.parent_id 
) 
SELECT re.* 
    , ca.category_name 
    , ins.instance_name 
    FROM re 
    JOIN school.category ca ON (re.my_cat = ca.id) 
    JOIN school.instance ins ON (re.my_id = ins.id) 
    -- WHERE re.my_id = 14 
    ; 

輸出:

INSERT 0 11 
ERROR: insert or update on table "instance" violates foreign key constraint "instance_category_id_fkey1" 
DETAIL: Key (category_id, parent_category_id)=(6, 3) is not present in table "category_valid_parent". 
pa_id | pa_cat | my_id | my_cat | category_name |  instance_name 
-------+--------+-------+--------+---------------+----------------------- 
    1 |  1 |  1 |  1 | School  | University of Utrecht 
    11 |  1 | 11 |  1 | School  | Hogeschool Utrecht 
    1 |  1 |  2 |  2 | Sub_campus | Uithof 
    11 |  1 | 12 |  4 | Term   | Journalistiek 
    2 |  2 |  3 |  3 | Program  | Life sciences 
    12 |  4 | 13 |  6 | Department | Begrijpend Lezen 
    12 |  4 | 14 |  6 | Department | Typvaardigheid 
    3 |  3 |  4 |  4 | Term   | Bacherlor 
    4 |  4 |  5 |  5 | Division  | Biology 
    5 |  5 |  6 |  6 | Department | Evolutionary Biology 
    5 |  5 |  7 |  6 | Department | Botany 
(11 rows) 

BTW:我離開了屬性。我建議他們可以通過EAV類型的數據模型將其掛鉤到相關類別。

+0

注意:這完全是關於「約束最小化」。在這種情況下,即使拓撲以表格形式表示(因此:靈活),約束也可確保拓撲。允許拓撲的更改不會調用添加或更改約束。 – wildplasser

0

我會制定這樣一個非常靈活的方式和什麼似乎意味着是最簡單的,以及:

應該只有一個表,讓我們把它叫做category_nodes:

-- possible content, of this could be stored in another table and create a 
-- 1:N -> category:content relationship 
drop table if exists category_nodes; 
create table category_nodes (
    category_node_id int(11) default null auto_increment, 
    parent_id int(11) not null default 1, 
    name varchar(256), 
    primary key(category_node_id) 
); 
-- set the first 2 records: 
insert into category_nodes (parent_id, name) values(-1, 'root'); 
insert into category_nodes (parent_id, name) values(-1, 'uncategorized'); 

所以表中的每條記錄都有一個唯一的ID,一個父ID和一個名稱。

現在在前2次插入之後:在category_nodes中category_node_id爲0的是根節點(所有節點的父節點,無論有多少degres遠離第二個僅用於一個小助手,在未分類節點處設置一個未分類節點。category_node_id = 1,這也是PARENT_ID的defalt值插入到表時

現在想象的根類是學校,期限和部門你會:

insert into category_nodes (parent_id, name) values (0, 'School'); 
insert into category_nodes (parent_id, name) values (0, 'Term'); 
insert into category_nodes (parent_id, name) values (0, 'Dept'); 

然後讓所有的根類別:

select * from category_nodes where parent_id = 0; 

現在想象一個更復雜的模式:

-- School -> Division -> Department 
-- CatX -> CatY 
insert into category_nodes (parent_id, name) values (0, 'School'); -- imaging gets pkey = 2 
insert into category_nodes (parent_id, name) values (2, 'Division'); -- imaging gets pkey = 3 
insert into category_nodes (parent_id, name) values (3, 'Dept'); 
-- 
insert into category_nodes (parent_id, name) values (0, 'CatX'); -- 5 
insert into category_nodes (parent_id, name) values (5, 'CatY'); 

我們得到學校的所有子類別,例如:

select * from category_nodes where parent_id = 2; 
-- or even 
select * from category_nodes where parent_id in (select category_node_id from category_nodes 
    where name = 'School' 
); 

等。由於默認= 1與PARENT_ID,插入到「未分類」類別變得簡單:

<?php 
$name = 'New cat name'; 
mysql_query("insert into category_nodes (name) values ('$name')"); 

乾杯