在软件开发中,尽量或者不使用自增主键id参与业务逻辑的开发。
—— 忘了叫什么的作者
软件开发七年多了,最近突发奇想,想对平时开发中,经常遇到的,但是比较鸡肋的一些开发技巧和方案做个系统性的归纳和思考,比如软件开发中,到底要不要创建主外键?比如多个级联关系中,到底要不要以自增主键id为唯一标识?
在一年前我写过一篇文章《实现业务数据的同步迁移 · 思路一》,说的就是如何针对BlogCore项目中的数据做一次迁移,这几天一直在写部门权限的业务逻辑,本地开发好后,比如添加了几个菜单和接口,然后也做了权限的分配,然后需要把数据同步到线上的数据库,同时为了让初学者看到效果,还需要生成种子数据,所以就用了之前的迁移接口,发现不太好用,主要就是之前是整个库迁移可以,要是针对最近一两天添加的权限部分,就不好处理了,所以我做了优化和改进,可以实现,针对任意的permission权限做同步迁移,包括module接口和三表关系的同步迁移。
在写迁移的过程中,我开始思考一个问题,为什么要这么复杂呢,有没有其他方案呢,这里先简单说下如果涉及到表数据迁移,特别是复杂级联表关系数据的迁移应该怎么办?
1、万能的String字符串做标识
曾经很多次,想对整个项目做一次大改,把所有的表主键都用Guid,直接用字符串来做唯一标识,然后表与表之间通过这个字符串做关联,这样数据做迁移关系的时候,就可以很好的解决自增id的问题(id不一致问题),随便导入导出就行,看起来是很不错的。
不过这是老生常谈了,主要有几个顾虑:
1、历史数据肯定就不能随便动了,伤筋动骨的;
2、全部用字符串做主键,肯定对性能会有影响;
3、还需要考虑一定的可读性,和下一条;
当然,我也考虑过另一个场景,还用自增id做主键,只不过增加一个字符串字段参与业务逻辑开发,id就不参与了,这种混合开发针对特定的、不是很多很复杂的表还行,但是如果都相互冗余,会加重开发的复杂度,重构也会变难,因为在更新数据的时候,还要考虑更新这个字符串标识,得不偿失。
所以到目前,我还是没有真正使用这个方案,新项目打算尝试一下。那接下来就说一下,如果全部是自增主键id做业务关联,如何实现数据的迁移。
2、Blog.Core复杂表迁移实践
在Blog.Core项目中,权限关系五个表的相爱相杀,相互关联:
Modules表:存放所有的接口API列表,主键Mid;
Permission表:存放前端菜单路由列表,并且有父Pid和接口Mid;
ModulePermission关系表:可以做多对多(目前用不到,舍弃);
Role表:存放所有角色列表,主键Rid;
RoleModulePermission表:三表主键关系表;
平时我们本地有一个测试数据库,然后开发好后,会导出一份数据,无论是Sql还是Json都是可以的,需要导入到生产数据库中,那本地设计的配置的那些id就鸡肋了,因为两个库肯定都经过风吹日晒,不同步了,直接用导出的mid、pid、rid来导入到数据库的话,肯定会存在问题,所以这种方案直接pass掉了,除非你的库是新的。
我的方案就是通过代码的方案,用树的形式,导入,这样用新的pid做关系键就能实现目的。
1、初始化菜单树 InitPermissionTree
从表结构可以看出来,我们的接口Module是来服务菜单的,没有菜单,要接口也是无用的,所以迁移的核心就是菜单树,那首先需要做的就是初始化这个菜单树,很常见的思路就是采用递归的方案。
代码语言:javascript复制 private void InitPermissionTree(List<Permission> permissionsTree, List<Permission> all, List<Modules> apis)
{
foreach (var item in permissionsTree)
{
// 给子节点赋值
item.Children = all.Where(d => d.Pid == item.Id).ToList();
// 给接口信息赋值
item.Module = apis.FirstOrDefault(d => d.Id == item.Mid);
// 向下递归
InitPermissionTree(item.Children, all, apis);
}
}
过程很简单的,这样就得到一个完整的,包含子节点和接口模型的一棵大树。
2、过滤菜单树 FilterPermissionTree
有了完整的树,我们可以直接导入,但是这适用于第一次,如果后期每次小改动,这样都全部导入是不对的,就需要对菜单做个过滤。
测试库中,为了测试,肯定会有一个范围,比如id≥122的是这次业务开发的,所以就需要做个过滤,那我们就需要对刚刚的那个大树,做个过滤,只保留这个分支的就行,当然还是从主干开始的:
代码语言:javascript复制 // 找到当前分支的根节点
private void FilterPermissionTree(List<Permission> permissionsAll, List<int> actionPermissionId, List<int> filterPermissionIds)
{
actionPermissionId = actionPermissionId.Distinct().ToList();
var doneIds = permissionsAll.Where(d => actionPermissionId.Contains(d.Id) && d.Pid == 0).Select(d => d.Id).ToList();
filterPermissionIds.AddRange(doneIds);
var hasDoIds = permissionsAll.Where(d => actionPermissionId.Contains(d.Id) && d.Pid != 0).Select(d => d.Pid).ToList();
if (hasDoIds.Any())
{
FilterPermissionTree(permissionsAll, hasDoIds, filterPermissionIds);
}
}
// 筛查要操作的核心分支
List<int> filterPermissionIds = new();
FilterPermissionTree(permissionsAllList, actionPermissionIds, filterPermissionIds);
permissions = permissions.Where(d => filterPermissionIds.Contains(d.Id)).ToList();
3、开始保存菜单球 SavePermissionTreeAsync
上边咱们准备好了数据,接下来就把菜单树保存下来,顺便也把菜单附属品的接口Module做保存,这块代码就稍微多了些,主要通过递归的方式,因为是一棵树,要注意的就是,以前保存过的,肯定不要再保存了,只需要获取id就行,注意的是需要开启事务哟,这里巧用了读写分离的方案,具体的详细内容可以参考这个文章《实现业务数据的同步迁移 · 思路一》:
代码语言:javascript复制private async Task SavePermissionTreeAsync(List<Permission> permissionsTree, List<PM> pms, int permissionId = 0)
{
var parendId = permissionId;
foreach (var item in permissionsTree)
{
PM pm = new PM();
// 保留原始主键id
pm.PidOld = item.Id;
pm.MidOld = (item.Module?.Id).ObjToInt();
var mid = 0;
// 接口
if (item.Module != null)
{
var moduleModel = (await _moduleServices.Query(d => d.LinkUrl == item.Module.LinkUrl)).FirstOrDefault();
if (moduleModel != null)
{
mid = moduleModel.Id;
}
else
{
mid = await _moduleServices.Add(item.Module);
}
pm.MidNew = mid;
Console.WriteLine($"Moudle Added:{item.Module.Name}");
}
// 菜单
if (item != null)
{
var permissionModel = (await _permissionServices.Query(d => d.Name == item.Name && d.Pid == item.Pid && d.Mid == item.Mid)).FirstOrDefault();
item.Pid = parendId;
item.Mid = mid;
if (permissionModel != null)
{
permissionId = permissionModel.Id;
}
else
{
permissionId = await _permissionServices.Add(item);
}
pm.PidNew = permissionId;
Console.WriteLine($"Permission Added:{item.Name}");
}
pms.Add(pm);
await SavePermissionTreeAsync(item.Children, pms, permissionId);
}
}
4、新Pid和Mid保存,并迁移
之前咱们说过,迁移最大的痛点,就是关系id已经发生了变化,那咱们就需要在保存的时候将id做个备份记录:
代码语言:javascript复制 public class PM
{
public int PidOld { get; set; }
public int MidOld { get; set; }
public int PidNew { get; set; }
public int MidNew { get; set; }
}
然后在RoleModulePermission关系表中,就需要获取到这个新的值,做循环保存即可,这里就不需要递归了。
到这里就完全迁移完成了,感兴趣可以试试吧!