(图片来源于SqlSugar官网,5年5.0)
大家假期已经结束了吧,还有80天左右就要到2021年了,你准备好了么?BCVP(Blog.Core&Vue Project)项目已经开源2年多,从来没有停更过,网上出现了很多仿品,当然这是好事儿,我从一开始也是这么鼓励大家的,第一要学习知识点,第二如果学会了自己动手搭一搭,这样不仅自己有了一定的深入理解,从全局上巩固,另外也可以对他人有一个借鉴和参考的不同版本,不过还是建议可以稍微稍稍的说一下,灵感/思路/学习受老张的帮助、影响和借鉴,想必你也明白,一边开源,一边讲解,一边建立社区回答问题,是一个常人无非想象的毅力。最近打算成立一个基于BCVP的开发者社区,感兴趣的可以留言,一起来个Business版本,两三个人即可,是那种真的想设计的,看缘分吧。
今天继续推进BCVP项目的往下进行,新开了一个需求,这个需求来自于网友的提问:目前BlogCore项目默认使用的是int作为主键,并自增,平时开发的时候int或者long这个都是很常见的,但是如果说,我就不想用int,习惯了Guid,当然也为了更方便迁移数据,因为int会乱序,特别是在多库的时候。那这个时候如果我想把int主键,改成guid,工作量是多大,需要改多少地方,怎么处理逻辑,前端修改哪些地方,等等等等。
所以我就尝试了这个新课题:使用泛型主键,这样拿到这个项目的时候,自己修改下主键类型,就可以运行了,不过目前还没有百分百完善,int主键已经调通,其他类型主键,比如Guid或者自定义string还没有完成生产化,但是放心,我肯定会完善的,最终的目的是下载项目后,可以满足自定义配置。
做这个需求的目的,一是为了灵活框架,二也是为了给大家提供一个思路。
别一上来就说没用,你可以不用我的框架,但是这个思路还是可以了解下的,平时ORM中是如何控制的,而且泛型在项目开发中的作用特别大。好啦,下边就先简单说一下思路吧,当然离不开SqlSugar的强大支持。
1、自定义特性
配置服务SqlsugarSetup
既然要实现泛型主键,那我们就需要对主键进行处理,因为只有int类型的主键才需要自增,其他类型的是不需要的,当然如果在非int类型的主键上配置自增了也是会报错的。
在SqlsugarSetup.cs服务类中,配置自定义特性:
listConfig.Add(new ConnectionConfig(){ConfigId = m.ConnId.ObjToString().ToLower(),ConnectionString = m.Connection,DbType = (DbType)m.DbType,IsAutoCloseConnection = true,IsShardSameThread = true,AopEvents = new AopEvents{OnLogExecuting = (sql, p) =>{if (Appsettings.app(new string[] { "AppSettings", "SqlAOP", "Enabled" }).ObjToBool()){Parallel.For(0, 1, e =>{MiniProfiler.Current.CustomTiming("SQL:", GetParas(p) + "【SQL语句】:" + sql);LogLock.OutSql2Log("SqlLog", new string[] { GetParas(p), "【SQL语句】:" + sql });});}}},MoreSettings = new ConnMoreSettings(){//IsWithNoLockQuery = true,IsAutoRemoveDataCache = true},// 从库SlaveConnectionConfigs = listConfig_Slave,// 自定义特性ConfigureExternalServices = new ConfigureExternalServices(){EntityService = (property, column) =>{if (column.IsPrimarykey && property.PropertyType == typeof(int)){column.IsIdentity = true;}}},InitKeyType = InitKeyType.Attribute}
核心的就是ConfigureExternalServices这个方法,如果是主键,并且是int,才会增加它的自增属性,否则不处理。
■ ■■■■
修改实体基类RootEntityTkey
这里我重写了一个基于泛型主键的实体基类RootEntityTkey,因为有了上边的配置,所以就不需要在主键上增加自增了,只需要配置一个属性:是否为主键即可,因为肯定不为空,另一个参数IsNullable可以不写:
现在配置好了自定义特性,那就开始今天的重头戏了——设计泛型。
■ ■■■■
2、设计泛型主键结构
实体基类增加泛型参数
上边我们已经重新设计了一个实体基类,在它的基础上,我们可以先增加一个泛型参数:
public class RootEntityTkey<Tkey> where Tkey : IEquatable<Tkey>{/// <summary>/// ID/// 泛型主键Tkey/// </summary>[SugarColumn(IsNullable = false, IsPrimaryKey = true)]public Tkey Id { get; set; }}
这都是很简单的,就是新增了一个参数Tkey,我就不多说了,只要是用过泛型的肯定一眼就能明白,如果看不明白,可以学习下基础知识了。
这里有一个小疑问,你可能会说,那我int类型有一个数字自增,但是如果其他类型的时候,如何配置默认值呢,别担心Sqlsugar已经提供了Guid的默认值,你可以查看源码,是这么设计的:
这样的话,我们的实体类的如果是Guid,就算是一个空的对象实例,存入的时候也会有值,具体的写法我下文会举例说明的。
定义好了基类,那我们就需要动手数据库实体类了,可能稍微复杂一点,因为会涉及另一个重要的概念。
■ ■■■■
普通实体模型继承基类,并传递参数
刚刚已经定义好了泛型基类,那现在我们来设计下实体类,这里有两个情况,一种是普通的类结构,比如角色表自己不和其他交互,只有主键Id,另一种是有外键的复杂的类结构,比如用户角色表中,有Uid和Rid。咱们先说下普通类型的。
/// <summary>
/// 角色表
/// </summary>
public class Role : RootEntityTkey<int>
{// 因为继承了RootEntityTkey,所以就不用写主键Id了/// <summary>///获取或设置是否禁用,逻辑上的删除,非物理删除/// </summary>[SugarColumn(IsNullable = true)]public bool? IsDeleted { get; set; }/// <summary>/// 角色名/// </summary>[SugarColumn(ColumnDataType = "nvarchar", Length = 50, IsNullable = true)]public string Name { get; set; }// 等等其他的字段...}
这里用角色表Role举例,直接继承父类RootEntityTkey<int>,然后定义该实体除主键以外的属性和字段等即可,还是很简单的,也是很普通的写法。
■ ■■■■
复杂的实体模型
上边写了简单的方案,但是平时开发肯定不会是这样的,不免会出现有关系的情况,也就是外键的问题,比如用户角色关系表UserRole,它里边除了主键Id以外,肯定也会包含Uid和Rid,那如何设计呢,如果单纯的继承RootEntityTkey肯定是不行的,因为如果这么操作了,这个关系表中肯定就不能和User表或者Role表保持一致了,所以这三个字段都应该设计成泛型的格式,那如何设计的?
我参照着实体泛型基类,又单独针对特定的有外键需求的实体,抽离了一个中间父类,请注意我的命名:实体类-->父类(非必须)-->泛型基类,用UserRole来举例。
1、还是先定义UserRole的实体类内容
/// <summary>
/// 用户跟角色关联表
/// 基础表
/// </summary>
public class UserRole : UserRoleRoot<int>
{/// <summary>///获取或设置是否禁用,逻辑上的删除,非物理删除/// </summary>[SugarColumn(IsNullable = true)]public bool? IsDeleted { get; set; }/// <summary>/// 创建ID/// </summary>[SugarColumn(IsNullable = true)]public int? CreateId { get; set; }// 其他属性字段
}
2、然后抽离父类,对外键和Pid等单纯处理
/// <summary>
/// 用户跟角色关联表
/// 父类
/// </summary>
public class UserRoleRoot<Tkey> where Tkey : IEquatable<Tkey>
{/// <summary>/// 用户ID/// </summary>public Tkey UserId { get; set; }/// <summary>/// 角色ID/// </summary>public Tkey RoleId { get; set; }
}
3、最后将抽离的父类来继承泛型基类
/// <summary>
/// 用户跟角色关联表
/// 父类
/// </summary>
public class UserRoleRoot<Tkey> : RootEntityTkey<Tkey> where Tkey : IEquatable<Tkey>
{/// <summary>/// 用户ID/// </summary>public Tkey UserId { get; set; }/// <summary>/// 角色ID/// </summary>public Tkey RoleId { get; set; }
}
这样不仅可以满足这种外键的问题,也可以来处理一些特殊的情况,比如Pid,你想一下,主键如果都泛型了,总不能Pid父id这种还是int吧,这里用接口表的抽离父类举例:
/// <summary>
/// 接口API地址信息表
/// 父类
/// </summary>
public class ModulesRoot<Tkey> : RootEntityTkey<Tkey> where Tkey : IEquatable<Tkey>
{/// <summary>/// 父ID/// </summary>[SugarColumn(IsNullable = true)]public Tkey ParentId { get; set; }}
BlogCore项目我已经修改完成,最终的结果是这样的:
核心的就是RootTkey这个文件夹下,就是这次修改的主要部分,其他的实体模型基本不用修改,只需要继承特定的专属父类/基类即可: RootEntityTkey<Tkey>。
■ ■■■■
3、其他重要提醒
不要把抽离的父类生成到数据库
在BlogCore项目中,我用的是自动CodeFirst并可以生成种子数据,当生成表结构的时候,我是根据命名空间来处理的,你在设计抽离的父类,比如UserRoleRoot<Tkey>的时候,注意修改命名空间,别生成到了数据库里,当然肯定也生成不进去,会报错的,这里只是提个醒,因为是CodeFirst的逻辑是根据命名空间:
// 创建数据库表,遍历指定命名空间下的class,
// 注意不要把其他命名空间下的也添加进来。
Console.WriteLine("Create Tables...");
var modelTypes = from t in Assembly.GetExecutingAssembly().GetTypes()where t.IsClass && t.Namespace == "Blog.Core.Model.Models"select t;
modelTypes.ToList().ForEach(t =>
{if (!myContext.Db.DbMaintenance.IsAnyTable(t.Name)){Console.WriteLine(t.Name);myContext.Db.CodeFirst.InitTables(t);}
});
当然,你也可以自己优化下,比如来个特性,或者继承一个接口啥的来限制只有实体模型才可以生成到数据库等等,看你的需要了。
■ ■■■■
生成种子数据的时候,反序列化要注意
我也同时优化了种子数据json的反序列化,比如整型用的是0,不是"0",这样的问题。
然后反序列化的方法也改用Newtonsoft.Json组件了,之前我之前自己写的,在反序列化的时候有不识别null的问题,所以需要配置一个setting来处理掉null,具体的代码,可以查看DBSeed.cs 这个文件。这里举一个例子:
JsonSerializerSettings setting = new JsonSerializerSettings();JsonConvert.DefaultSettings = new Func<JsonSerializerSettings>(() =>{//日期类型默认格式化处理setting.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat;setting.DateFormatString = "yyyy-MM-dd HH:mm:ss";//空值处理setting.NullValueHandling = NullValueHandling.Ignore;//setting.Converters.Add(new BoolConvert("是,否"));return setting;});if (!await myContext.Db.Queryable<TopicDetail>().AnyAsync())
{var data = JsonConvert.DeserializeObject<List<TopicDetail>>(FileHelper.ReadFile(string.Format(SeedDataFolder, "TopicDetail"), Encoding.UTF8), setting);myContext.GetEntityDB<TopicDetail>().InsertRange(data);Console.WriteLine("Table:TopicDetail created success!");
}
else
{Console.WriteLine("Table:TopicDetail already exists...");
}
■ ■■■■
项目如何初始化自定义主键类型
现在我的项目中,已经完全配置好了int类型的模式了,如果你想使用Guid的话,应该如何操作呢,很简单,只需要直接修改下泛型参数就行,这里用Advertisement举例子说明下:
1、修改泛型参数为Guid:
public class Advertisement : RootEntityTkey<Guid>
{// 属性字段等...
}
2、执行Add操作
var ad = await _advertisementServices.Add(new Advertisement());
3、注意仓储执行方法
因为之前我们都是使用的int作为主键,然后用的.ExecuteReturnIdentityAsync()方法,这样返回的是对应的id。
但是现在用了Guid以后,就不能这么用了,因为这样使用的话,这个方法是无效的.ExecuteReturnIdentityAsync(),不仅不会正常的返回id值,也无非自动生成Guid的默认值,你可以使用.ExecuteCommandAsync(),当然可以直接使用.ExecuteReturnEntityAsync()这个方法,来返回实体,然后从实体里,获取对应的Id,这样的话,不论是int还是Guid,都能返回出来了。
4、查看效果
设置了Guid以后,就可以看看效果了,上边的0000-000-0000-000这样的值,就是因为使用的.ExecuteReturnIdentityAsync(),下边的是正常的使用Command方法,或者直接ReturnEntity的方法。
总体来说还是很方便的。
好啦,今天的分享暂时就是这样的,希望能提供一个思路,无论是对BCVP项目,还是你自己的项目。
end
BCVP开发者社区推荐
欢迎你来