简介:这是一套面向Java桌面应用开发者与课程设计学习者的JavaFX家谱管理系统完整项目源码,围绕Family-Tree核心功能展开,适合希望掌握JavaFX界面开发与数据库持久化结合的初中级开发者参考。压缩包共239个文件,约3.45MB,以24个java源文件、34个class编译文件、12个fxml界面布局、10个xml配置及SQLite相关data数据文件为主,另含jar依赖、docx说明与md文档,覆盖从Person、Family类设计到TreeView树形展示的完整结构。项目通过自定义TreeItem将家庭成员映射为节点,配合按钮、文本框实现增删改操作,并借助JDBC与SQLite完成数据持久化,同时利用CSS样式表与动画特效优化交互体验。目前已有1619人学习下载,读者可从中获取数据结构设计、界面构建与数据库集成的完整实现思路,并在此基础上扩展搜索、导出报告等功能。
1. 从 Family-Tree.zip 说起:一个压缩包背后藏着的家谱数据工程
Family-Tree.zip 这个标题看起来简单,但它指向的是一类非常典型的需求:把家族关系数据用程序管理起来,支持查询、可视化和持久化存储。很多人第一次接触这类项目,是因为想给自己家族修一份电子族谱——老一辈手写的纸质家谱翻起来费劲,亲戚关系一多就容易乱,谁是谁的堂叔、谁跟谁隔了几代,光靠脑子根本记不住。这个压缩包里通常包含的是一套完整的家谱管理源码或数据文件,可能涉及数据结构定义、关系查询逻辑、前端展示页面以及示例数据。它适合两类人:一类是想直接跑起来看效果的开发者,另一类是打算基于它二次开发、接入自己家族数据的工程师。核心要解决的问题就三个:关系怎么存、查询怎么快、展示怎么清楚。接下来我会按实际落地的顺序,把从解压到跑通再到改造成自己可用的全过程拆开讲。
2. 家谱数据建模:从「人」到「关系」的表结构设计
2.1 为什么不用简单的父子表
很多人第一反应是建一张表,字段放 id、name、father_id、mother_id,觉得够用了。这种设计在只查直系亲属时没问题,但家谱场景里高频出现的需求是「查某人的所有堂兄弟」「查两代人之间的所有路径」「查某人往上五代的祖先链」。用 father_id 递归查询,每查一代就要多一次自连接,五代就是五次,性能掉得厉害,SQL 写起来也容易出错。
常见做法是采用闭包表(Closure Table)或路径枚举(Path Enumeration)来存关系。闭包表额外建一张关系表,每一行存 ancestor_id、descendant_id、depth,把任意两个人之间的代际关系都拍平。路径枚举则是在每个人记录里存一个类似 “1/3/7/12” 的路径字符串,表示从根到当前节点的链路。两种方案各有取舍:闭包表写入稍慢但查询极快,路径枚举写入快但路径长度有限制。
我一般会选闭包表,因为家谱数据写入频率远低于查询频率,而且闭包表对「查所有后代」「查共同祖先」这类操作非常友好。
2.2 建表 SQL 与索引策略
下面是一套最小可用的表结构,包含人员表、关系闭包表和婚姻表。婚姻关系在家谱里不能忽略,否则再婚、入赘等情况会丢数据。
-- 人员表:存基本信息 CREATE TABLE person ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, gender TINYINT NOT NULL COMMENT '1-男 2-女 0-未知', birth_date DATE, death_date DATE, generation INT COMMENT '代际编号,从始祖算起', note VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 关系闭包表:存任意两人之间的代际路径 CREATE TABLE relation_closure ( ancestor_id BIGINT NOT NULL, descendant_id BIGINT NOT NULL, depth INT NOT NULL COMMENT '相隔代数,自己到自己为0', PRIMARY KEY (ancestor_id, descendant_id), INDEX idx_descendant (descendant_id, depth), INDEX idx_ancestor (ancestor_id, depth) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 婚姻表:存配偶关系 CREATE TABLE marriage ( id BIGINT PRIMARY KEY AUTO_INCREMENT, husband_id BIGINT NOT NULL, wife_id BIGINT NOT NULL, marry_date DATE, divorce_date DATE, status TINYINT DEFAULT 1 COMMENT '1-存续 2-离异 3-丧偶', INDEX idx_husband (husband_id), INDEX idx_wife (wife_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:person 表存个体,relation_closure 存代际闭包,marriage 单独存配偶关系。闭包表的 depth 字段是关键,depth=0 表示自己,depth=1 表示父子,depth=2 表示祖孙,以此类推。索引建在 descendant_id 和 ancestor_id 上,因为查询通常是「某人的所有后代」或「某人的所有祖先」。
参数说明:generation 字段用于快速筛选同代人,比如查所有第 5 代成员。gender 用 TINYINT 而不是 ENUM,方便后续扩展。note 字段留给族谱里的备注信息,比如「迁居某地」「某支失考」。
提示:如果家族人数超过 5000,闭包表的行数会膨胀到 O(n²) 级别,此时建议只存 depth ≤ 10 的关系,更远的代际用路径枚举辅助。
2.3 插入数据时闭包表怎么维护
新增一个人时,不能只插 person 表,还要同步更新闭包表。假设新增节点 C,其父亲是 A,那么 C 与 A 的所有祖先都要建立关系。
def add_person(cursor, name, gender, father_id=None, mother_id=None): cursor.execute( "INSERT INTO person (name, gender) VALUES (%s, %s)", (name, gender) ) new_id = cursor.lastrowid # 自己到自己的关系 cursor.execute( "INSERT INTO relation_closure (ancestor_id, descendant_id, depth) VALUES (%s, %s, 0)", (new_id, new_id) ) # 继承父系和母系的祖先链 for parent_id in filter(None, [father_id, mother_id]): cursor.execute(""" INSERT INTO relation_closure (ancestor_id, descendant_id, depth) SELECT ancestor_id, %s, depth + 1 FROM relation_closure WHERE descendant_id = %s """, (new_id, parent_id)) return new_id逻辑说明:先插入人员记录拿到 new_id,然后插入自反关系。接着对每个已知父/母,把该父/母的所有祖先关系复制一份,descendant_id 换成 new_id,depth 加 1。这样新节点就自动挂到了整条祖先链上。
参数说明:father_id 和 mother_id 可以为空,表示始祖或信息缺失。filter(None, ...) 用来跳过空值。这个操作在事务里执行,避免中途失败导致闭包表不完整。
3. 关系查询实战:从「查三代」到「找共同祖先」
3.1 查某人的所有直系祖先
有了闭包表,查祖先就是一句 SQL 的事。
-- 查 id=100 的所有祖先,按代际从近到远 SELECT p.id, p.name, p.generation, rc.depth FROM relation_closure rc JOIN person p ON p.id = rc.ancestor_id WHERE rc.descendant_id = 100 AND rc.depth > 0 ORDER BY rc.depth ASC;逻辑说明:descendant_id 固定为 100,depth 大于 0 排除自己,按 depth 升序就是父亲、祖父、曾祖父的顺序。JOIN person 表拿姓名和代际编号。
参数说明:如果只想查三代以内,加AND rc.depth <= 3。如果要查某一支的祖先,加AND p.generation >= 某值。
3.2 查两个人的共同祖先
这是家谱里最常用的功能之一——判断两个人是什么亲戚关系。
-- 查 id=100 和 id=200 的共同祖先 SELECT p.id, p.name, rc1.depth AS depth_from_100, rc2.depth AS depth_from_200 FROM relation_closure rc1 JOIN relation_closure rc2 ON rc1.ancestor_id = rc2.ancestor_id JOIN person p ON p.id = rc1.ancestor_id WHERE rc1.descendant_id = 100 AND rc2.descendant_id = 200 ORDER BY (rc1.depth + rc2.depth) ASC;逻辑说明:rc1 是 100 的祖先链,rc2 是 200 的祖先链,两者 ancestor_id 相等就是共同祖先。depth 之和越小,共同祖先越近。第一行结果就是最近共同祖先。
参数说明:如果结果为空,说明两人没有共同祖先(至少在当前数据集里)。depth 之和为 2 表示两人是亲兄弟(共同父亲,各隔 1 代),为 4 表示堂兄弟(共同祖父,各隔 2 代)。
3.3 查某人的所有堂兄弟
堂兄弟的定义是:共同祖父,但父亲不同。用闭包表可以两步查出来。
-- 先找到 id=100 的祖父 WITH grandpa AS ( SELECT ancestor_id FROM relation_closure WHERE descendant_id = 100 AND depth = 2 ) -- 再找祖父的所有孙辈,排除自己和亲兄弟 SELECT p.id, p.name FROM relation_closure rc JOIN person p ON p.id = rc.descendant_id WHERE rc.ancestor_id = (SELECT ancestor_id FROM grandpa) AND rc.depth = 2 AND rc.descendant_id != 100 AND rc.descendant_id NOT IN ( SELECT descendant_id FROM relation_closure WHERE ancestor_id = (SELECT ancestor_id FROM grandpa) AND depth = 2 AND descendant_id IN ( SELECT descendant_id FROM relation_closure WHERE ancestor_id = (SELECT id FROM person WHERE id = 100) ) );逻辑说明:先用 CTE 找到祖父,再找祖父的所有 depth=2 的后代(即孙辈),然后排除自己和亲兄弟。亲兄弟的判断是「父亲的儿子」,这里用嵌套子查询实现。
参数说明:这个查询在人数多时性能一般,实际项目中我会把堂兄弟关系预计算到一张缓存表里,避免每次实时算。
注意:闭包表在数据量大时,共同祖先查询的 JOIN 成本会上升。如果 QPS 高,建议对 depth 加限制,比如只查 depth ≤ 6 的共同祖先。
4. 可视化落地:用 D3.js 把家谱画成树
4.1 数据导出格式设计
前端画树需要层级数据,后端从闭包表查出来的扁平结果要转成嵌套 JSON。常见做法是查所有人员,然后在内存里按父子关系组装。
def build_tree(cursor, root_id): cursor.execute("SELECT id, name, gender, father_id FROM person") rows = cursor.fetchall() nodes = {r['id']: {'id': r['id'], 'name': r['name'], 'gender': r['gender'], 'children': []} for r in rows} root = None for r in rows: node = nodes[r['id']] if r['father_id'] and r['father_id'] in nodes: nodes[r['father_id']]['children'].append(node) if r['id'] == root_id: root = node return root逻辑说明:先把所有人转成字典节点,再遍历一遍挂父子关系。father_id 为空或不在数据集里的节点就是根节点候选。
参数说明:root_id 指定从谁开始画。如果家族有多个始祖,可以传一个虚拟根节点,把所有始祖挂上去。
4.2 D3.js 树形布局的关键参数
D3 的 tree layout 有几个参数直接影响家谱的可读性。
const treeLayout = d3.tree() .nodeSize([80, 200]) // 节点间距:水平80,垂直200 .separation((a, b) => a.parent === b.parent ? 1 : 1.5); // 同父节点间距1,不同父1.5 const root = d3.hierarchy(treeData); treeLayout(root); // 画连线 svg.selectAll('.link') .data(root.links()) .enter() .append('path') .attr('d', d3.linkVertical() .x(d => d.x) .y(d => d.y) );逻辑说明:nodeSize 控制节点之间的像素距离,separation 控制兄弟节点之间的间距倍数。linkVertical 生成垂直方向的贝塞尔曲线。
参数说明:nodeSize 的第一个值是水平间距,第二个是垂直间距。家谱通常垂直间距要大一些,因为要放姓名和生卒年。separation 返回 1.5 表示不同父亲的节点间距是兄弟节点的 1.5 倍,避免不同支系混在一起。
4.3 大族谱的分页与懒加载
家族人数超过 500 时,一次性渲染整棵树会卡顿。我一般会做两级懒加载:默认只展开到第三代,点击某个节点再加载其子树。
function toggleNode(d) { if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } update(d); }逻辑说明:用 _children 暂存折叠的子树,点击时切换。update 函数重新计算布局并更新 SVG。
参数说明:初始数据里只放三代,更深的节点用 children 为空但标记 hasMore 的方式,点击时再发请求拉取。
提示:D3 的 tree layout 在节点超过 2000 时递归会爆栈,建议改用 d3.flextree 或自己实现迭代布局。
5. 避坑与排查:家谱项目里最容易翻车的 5 个地方
5.1 闭包表插入时漏了自反关系
现象:查某人的祖先时,结果里没有自己,但查 depth=0 又查不到。
原因:插入人员时只插了与父辈的关系,忘了插(new_id, new_id, 0)这条自反记录。
解决:在 add_person 函数里,插入人员后立刻插入自反关系,再处理父辈关系。自反关系是闭包表递归的基础,缺了它后续所有查询都会少一层。
5.2 婚姻关系没单独建表,再婚数据丢失
现象:族谱里某人有两段婚姻,但只显示了一个配偶。
原因:把配偶信息直接塞在 person 表的 spouse_id 字段里,只能存一个。
解决:单独建 marriage 表,支持多段婚姻。查询配偶时用WHERE husband_id = ? OR wife_id = ?,再根据 gender 判断对方是夫还是妻。
5.3 代际编号 generation 算错,导致同代人排不到一起
现象:可视化时同辈人不在同一水平线上。
原因:generation 是手动填的,有人填错或没填。
解决:generation 应该由程序自动计算——插入时取父辈 generation 的最大值加 1。如果父辈缺失,默认从 1 开始。不要依赖人工输入。
5.4 前端树形布局节点重叠
现象:D3 画出来的树,兄弟节点挤在一起,名字看不清。
原因:nodeSize 的垂直间距太小,或者 separation 返回值太小。
解决:把 nodeSize 的第二个值调到 180 以上,separation 对不同父节点返回 1.5 到 2。如果还挤,考虑把姓名竖排或缩小字号。
5.5 查询共同祖先时结果为空但实际有共同祖先
现象:两个人明明是同宗,但共同祖先查询返回空。
原因:闭包表只存了 depth ≤ 某值的关系,或者数据导入时只导入了直系,旁系没挂上。
解决:检查闭包表的 depth 最大值,确认没有截断。如果是分批导入,确保每个节点插入时都正确继承了父辈的完整祖先链。可以用SELECT MAX(depth) FROM relation_closure快速验证。
6. 进阶技巧:用递归 CTE 替代闭包表做轻量查询
闭包表虽然查询快,但维护成本不低,尤其是数据频繁变动时。如果家族人数在 2000 以内,我其实更推荐用递归 CTE 直接查,省掉闭包表的同步逻辑。
-- 用递归 CTE 查 id=100 的所有祖先 WITH RECURSIVE ancestors AS ( SELECT id, name, father_id, mother_id, 0 AS depth FROM person WHERE id = 100 UNION ALL SELECT p.id, p.name, p.father_id, p.mother_id, a.depth + 1 FROM person p JOIN ancestors a ON p.id = a.father_id OR p.id = a.mother_id WHERE a.depth < 10 ) SELECT * FROM ancestors WHERE depth > 0 ORDER BY depth;逻辑说明:锚点成员是 id=100 自己,递归成员通过 father_id 或 mother_id 往上找。depth < 10 限制递归深度,防止死循环。
参数说明:depth 限制根据实际代数调整,一般 10 代足够。如果家族有循环引用(比如近亲结婚导致环),递归 CTE 会无限循环,必须加 depth 限制。
这个方案的取舍是:查询比闭包表慢,但写入简单,不需要维护额外表。我一般会在项目初期用递归 CTE 快速验证,数据量上来后再迁移到闭包表。
验证方法:拿同一个查询分别在闭包表和递归 CTE 上跑,对比结果是否一致。如果一致,说明闭包表维护正确。如果不一致,优先检查闭包表的自反关系和 depth 计算。
最后说个血泪经验:家谱项目最怕的不是技术难,而是数据录入不规范。我见过太多因为 father_id 填错导致整棵树断掉的案例。建议在插入数据前先做一轮校验——检查每个人是否有且仅有一个父节点(除非是始祖),检查是否存在环,检查 generation 是否连续。这些校验脚本花不了多少时间,但能省下后面大量排查的功夫。希望帮到你。
本文还有配套的精品资源,点击获取