I have a self referential database table:
CREATE TABLE category (
id INT PRIMARY KEY,
name VARCHAR,
parent_id INT REFERENCES category (id)
);
And the following INSERTS
:
INSERT INTO category (id, name, parent_id) VALUES (1, 'Programming', null);
INSERT INTO category (id, name, parent_id) VALUES (2, 'Python', 1);
INSERT INTO category (id, name, parent_id) VALUES (3, 'Metaclasses', 2);
INSERT INTO category (id, name, parent_id) VALUES (4, 'Java', 1);
INSERT INTO category (id, name, parent_id) VALUES (5, 'Reflection', 4);
INSERT INTO category (id, name, parent_id) VALUES (6, 'Homebrewing', null);
INSERT INTO category (id, name, parent_id) VALUES (7, 'Yeast', 6);
My goal is to output the table in the following hierarchical format:
Programming
Python
Metaclasses
Java
Reflection
Homebrewing
Yeast
Here is my solution:
class Category(object):
#This is actually a SQLAlchemy table
#I've included this for others to reproduce easier
def __init__(self, id, name, parent_id=None):
self.id = id
self.name = name
self.parent_id = parent_id
def __repr__(self):
return "Category(id={}, name='{}', parent_id={})".format(self.id, self.name, self.parent_id)
def _append_tree(category, tree, parent=None):
# The only reason to include parent is for the find_ascendant generator
tree.append({'parent': parent, 'category': category, 'children': []})
# This looks fine to me. Can it be better?
def add_node(category, tree):
if category.parent_id is None:
_append_tree(category, tree)
return 1
for child in tree:
if category.parent_id == child['category'].id:
# child obj has a ref to his parent
_append_tree(category, child['children'], child)
return 1
if add_node(category, child['children']):
return 1
return 0
def print_tree(tree, depth=0):
for child in tree:
print ' ' * depth + child['category'].name
print_tree(child['children'], depth + 1)
# This works, but looks bad; any suggestions?
def iter_tree(tree, depth=0):
for child in tree:
yield depth, child['category']
for i in iter_tree(child['children'], depth + 1):
yield i
This is the function for finding all ascendants for a leaf.
def _find_ascendants(child):
yield child['category']
if child['parent'] is not None:
for i in _find_ascendants(child['parent']):
yield i
def find_ascendants(category, tree):
if category.parent_id is None:
return
for child in tree:
if category.parent_id == child['category'].id:
# We've found the parent category. Return the parent's parent
for i in _find_ascendants(child):
yield i
# No need to check any more nodes
return
# No match; try children
for i in find_ascendants(category, child['children']):
yield i
This has the entire flow to show that it works:
tree = []
# The order by returns NULLS FIRST, so root nodes will always be populated before children
for category in session.query(Category).order_by(Category.parent_id):
add_node(category, tree)
print_tree(tree)
for depth, child in iter_tree(tree):
print '----' * depth, child.name
for i in find_ascendants(Category(id=3, name='Metaclasses', parent_id=2), tree):
print i
# or, to reproduce without a database:
tree = []
categories = [
Category(1, 'Programming', None),
Category(6, 'Homebrewing', None),
Category(2, 'Python', 1),
Category(4, 'Java', 1),
Category(3, 'Metaclasses', 2),
Category(5, 'Reflection', 5),
Category(7, 'Yeast', 6)
]
for category in categories:
add_node(category, tree)
print_tree(tree)
for depth, child in iter_tree(tree):
print '----' * depth, child.name
for i in find_ascendants(Category(id=3, name='Metaclasses', parent_id=2), tree):
print i
I am notably concerned about the iter_tree
generator and the find_ascendants
generator. I would also appreciate any suggestions to improve add_node
.
Here is an example of how I want to use the find_ascendants
generator.
The user creates a post and applies two categories to it, Maybe Metaclasses and Yeast, for example. I want to find all the parent categories of Metaclasses and Yeast and then apply them to the post as well.
# User provides this
category_ids = [3, 7] # Metaclasses and Yeast
# Construct tree
tree = []
for category in db.session.query(Category).order_by(Category.parent_id):
add_node(category, tree)
all_category_ids = set(category_ids)
for category in db.session.query(Category).filter(Category.id.in_(category_ids)):
ascendants = find_ascendants(category, tree)
for ascendant in ascendants:
all_category_ids.add(ascendant.id)
assert all_category_ids == set([1,2,3,6,7])
# Do something with all_category_ids here