闭包表—树状结构数据的数据库表设计
闭包表模型
闭包表(Closure Table)是一种通过空间换时间的模型,它是用一个专门的关系表(其实这也是我们推荐的归一化方式)来记录树上节点之间的层级关系以及距离。
场景
我们 基于 django
orm
实现一个文件树,文件夹直接可以实现无限嵌套
models
# 文件详情表(主要用于记录文件名)
class DmFileDetail(models.Model):
file_name = models.CharField("文件名(文件夹名)", max_length=50)
is_file = models.BooleanField("是否是文件", default=False)
user_id = models.IntegerField("用户id", default=0)
create_time = models.IntegerField("创建时间", default=0)
update_time = models.IntegerField("创建时间", default=0)
is_del = models.BooleanField("是否删除", default=False)
class Meta:
db_table = 'inchat_dm_file_detail'
verbose_name = verbose_name_plural = u'数字人文件详情表'
def __str__(self):
return self.file_name
# 文件关系表(主要用户记录文件之间的关联,即路径)
class DmFileRelation(models.Model):
ancestor_id = models.IntegerField("祖先节点ID")
descendant_id = models.IntegerField("子孙节点ID")
depth = models.IntegerField("深度(层级)", db_index=True)
user_id = models.IntegerField("用户id", default=0, db_index=True)
is_del = models.BooleanField("是否删除", default=False)
class Meta:
db_table = 'inchat_dm_file_relation'
index_together = ('ancestor_id', 'descendant_id')
verbose_name = verbose_name_plural = u'数字人文件关系表'
id | file_name |
---|---|
1 | AAA |
2 | aaa.pdf |
id | ancestor_id | descendant_id | depth |
---|---|---|---|
1 | 1 | 1 | 0 |
2 | 2 | 2 | 0 |
3 | 1 | 2 | 1 |
增删改查
class DmRelationNode:
"""
关系节点
"""
NAME = "DmRelationNode"
RELATION_CLIENT = DmFileRelation
@classmethod
def insert_relation_node(cls, node_id, user_id, parent_id):
"""
插入新的关系节点
"""
# 自身
insert_self = cls.RELATION_CLIENT(
ancestor_id=parent_id,
descendant_id=node_id,
user_id=user_id,
depth=1
)
insert_list = []
# 获取父节点所有祖先
parent_relation = cls.RELATION_CLIENT.objects.filter(descendant_id=parent_id) \
.values_list('ancestor_id', 'depth')
for ancestor_id, depth in parent_relation:
insert_data = cls.RELATION_CLIENT(
ancestor_id=ancestor_id,
descendant_id=node_id,
depth=depth + 1,
user_id=user_id
)
insert_list.append(insert_data)
# 插入自身
insert_list.append(insert_self)
logger.info('%s insert_relation_node.node_id:%s,parent_id:%s,insert_list:%s', cls.NAME, node_id, parent_id,
insert_list)
ret = cls.RELATION_CLIENT.objects.bulk_create(insert_list)
logger.info('%s insert_relation_node.node_id:%s,parent_id:%s,ret_list:%s', cls.NAME, node_id, parent_id, ret)
return ret
@classmethod
def get_ancestor_relation(cls, node_id):
"""
获取某个节点的所有祖先节点
"""
arges = ['ancestor_id', 'descendant_id', 'depth']
ancestor_relation_list = cls.RELATION_CLIENT.objects.filter(descendant_id=node_id, is_del=False).values(*arges)
relation_map = dict()
relation_dict = relation_map
for ancestor in ancestor_relation_list:
relation_dict['id'] = ancestor['ancestor_id']
if ancestor['ancestor_id'] != node_id:
relation_dict['children'] = {}
relation_dict = relation_dict['children']
return ancestor_relation_list
@classmethod
def get_descendant_relation(cls, node_id):
"""
获取所有的子节点
"""
arges = ['ancestor_id', 'descendant_id', 'depth']
descendant_relation_list = cls.RELATION_CLIENT.objects.filter(ancestor_id=node_id, is_del=False).values(*arges)
return descendant_relation_list
@classmethod
def get_direct_relation(cls, user_id):
"""
获取所有直系
"""
arges = ['ancestor_id', 'descendant_id', 'depth']
direct_relation = cls.RELATION_CLIENT.objects.filter(depth=1, user_id=user_id, is_del=False).values(*arges)
return direct_relation
@classmethod
def get_children_node(cls, node_id):
"""
获取某节点的子节点
"""
children_node = cls.RELATION_CLIENT.objects.filter(depth=1, ancestor_id=node_id, is_del=False) \
.values_list('descendant_id', flat=True)
return children_node
@classmethod
def remove_node(cls, node_id):
"""
删除节点
"""
logger.info('%s remove_node. node_id:%s', cls.NAME, node_id)
query = Q(ancestor_id=node_id, is_del=False) | Q(descendant_id=node_id, is_del=False)
res = cls.RELATION_CLIENT.objects.filter(query).update(is_del=True)
logger.info('%s remove_node. node_id:%s,count:%s', cls.NAME, node_id, res)
return res
以下 是一些常规的操作
class DmFileTree:
"""
DM文件树
"""
NAME = "DmFileTree"
DETAIL_CLIENT = DmFileDetail
RELATION_NODE_CLIENT = DmRelationNode
FILE_SAVE_DIR = 'media/dm/'
@classmethod
def get_file_map(cls, user_id):
"""
获取用户所有文件文件名
"""
file_detail = cls.DETAIL_CLIENT.objects.filter(user_id=user_id).values('id', 'file_name', 'path', 'is_file')
file_map = dict()
for file in file_detail:
file_dict = dict(
id=file['id'],
name=file['file_name'],
is_file=file['is_file'],
filePath=cls.FILE_SAVE_DIR + file['path'] + file['file_name']
)
file_map[file['id']] = file_dict
return file_map
@classmethod
def add_file(cls, user_id, file_name, parent_id, path='', is_file=False):
"""
新建文件(夹)
"""
kwargs = dict(
file_name=file_name,
path=path,
is_file=is_file,
user_id=user_id,
create_time=get_cur_timestamp()
)
file_obj = cls.DETAIL_CLIENT.objects.create(**kwargs)
if not file_obj:
logger.error('%s add_file failed. kwargs:%s', cls.NAME, kwargs)
return False
res = cls.RELATION_NODE_CLIENT.insert_relation_node(node_id=file_obj.id, user_id=user_id, parent_id=parent_id)
if not res:
return False
return dict(id=file_obj.id, name=file_name)
@classmethod
def get_file_path(cls, file_id):
"""
获取文件路径
"""
ancestor_query = cls.RELATION_NODE_CLIENT.get_ancestor_relation(file_id)
ancestor = map(lambda x: x['ancestor_id'], ancestor_query)
# 过滤0
ancestor = list(filter(lambda x: x > 0, ancestor))
# 排序
ancestor.sort()
path = '/'.join(map(str, ancestor))
return '/' + path + '/' if path else '/'
@classmethod
def get_all_files(cls, user_id):
# 获取所有文件名字典
file_map = cls.get_file_map(user_id)
# 查询所有子目录及文件
files_relation_list = cls.RELATION_NODE_CLIENT.get_direct_relation(user_id)
file_info = {a['descendant_id']: file_map.get(a['descendant_id']) or {} for a in files_relation_list}
tree = cls.list_to_tree(files_relation_list, file_info)
return tree
@classmethod
def get_child_files(cls, user_id, parent_id):
"""
获取下级文件
"""
# 获取所有文件名字典
file_map = cls.get_file_map(user_id)
file_list = cls.RELATION_NODE_CLIENT.get_children_node(node_id=parent_id)
files = map(lambda x: dict(id=x, name=file_map.get(x) or ''), file_list)
return files
@staticmethod
def list_to_tree(data, node_dict):
"""
将节点列表转换成树形结构字典
:param data: 带有 id 和 parent_id 属性的节点列表
:param node_dict: 单节点的数据结构字典
:return: 树形结构字典
"""
tree = []
# 遍历每一个节点,将其添加到父节点的字典或根节点列表中
for item in data:
id = item['descendant_id']
parent_id = item['ancestor_id']
# 如果父节点为 None,则将当前节点添加到根节点列表中
if not parent_id:
tree.append(node_dict[id])
# 如果父节点存在,则将当前节点添加到父节点的 children 属性中
else:
parent = node_dict[parent_id]
if 'children' not in parent:
parent['children'] = []
parent['children'].append(node_dict[id])
return tree
@classmethod
def delete_file(cls, file_id):
"""
文件删除
"""
res1 = cls.DETAIL_CLIENT.objects.filter(id=file_id).update(is_del=True)
logger.info('%s delete_file. file_id:%s, count:%s', cls.NAME, file_id, res1)
res2 = cls.RELATION_NODE_CLIENT.remove_node(file_id)
return res1, res2
@classmethod
def search_file(cls, file_name):
"""
搜索文件
"""
query_set = cls.DETAIL_CLIENT.objects.filter(file_name__icontains=file_name) \
.values('id', 'file_name', 'path', 'is_file')
file_list = []
for file in query_set:
file_dict = dict(
id=file['id'],
name=file['file_name'],
is_file=file['is_file'],
filePath='media/dm_upload' + file['path']
)
file_list.append(file_dict)
return file_list
@classmethod
def get_file_url(cls, file_id, file_obj=None):
"""
获取文件下载链接
"""
file_url = ''
if not file_obj:
file_obj = cls.DETAIL_CLIENT.objects.filter(id=file_id).first()
if file_obj:
file_url = 'http://127.0.0.1:8000/' + cls.FILE_SAVE_DIR + file_obj.path + file_obj.file_name
return file_url
除此之外,还有移动、复制文件(夹)。移动就是先删除再新增