代码已上传Github+Gitee,文末有地址
书接上文:《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之六 || API项目整体搭建 6.1 仓储》,我们简单的对整体项目进行搭建,用到了项目中常见的仓储模式+面向接口编程,核心的一共是六层,当然你也可以根据自己的需求进行扩展,比如我在其他的项目中会用到Common层,当然我们这个项目接下来也会有,或者我还会添加Task层,主要是作为定时项目使用,我之前用的是Task Schedule,基本能满足需求。
缘起
在上一节中,我们最后提出了两个问题,不知道大家是否还记得,这里还重新说明一下:
1、如果每个仓储文件都需要把一个一个写出来,至少是四遍,会不会太麻烦,而且无法复用,失去了面向接口编程的意义;
2、每次接口调用的时候,需要引入很多命名空间,比如Blog.Core.IServices;Blog.Core.Services;Blog.Core.Repository等等
对就是这两个问题,相信聪明的大家也都能看懂,或许还能给出相应的解决办法,比如泛型仓储,比如依赖注入,当然,如果你有更好的办法,欢迎留言,我会把你的想法写下了,让大家一起进步。这里先简单说下问题1中为什么要四遍,仓储模式的基本就是如何将持久化动作和对象获取方式以及领域模型Domain Model结合起来,进一步:如何更加统一我们的语言(Ubiquitous Language),一个整合持久化技术的好办法是仓储Repositories。明白了这个问题,你就知道,定义仓储,首先需要定义IRepository接口(1),然后再Repository中实现(2),接着在IService层中引用这些接口,同时也可以自定义扩展业务逻辑接口(3),最后在Service层中去实现(4),这就是四层。
问题明白了,我们就要动手做起来,思考了下,如果干巴巴直接写泛型仓储,会比较干涩,所以我考虑今天先把数据持久化做出来,一个轻量级的ORM框架——SqlSugar。
零、今天完成的蓝色部分
一、在Blog.Core.IRepository 层中添加CURD接口
还记得昨天我们实现的Sum接口么,今天在仓储接口 IAdvertisementRepository.cs 添加CURD四个接口,首先需要将Model层添加引用,这个应该都会,以后不再细说,如下:
namespace Blog.Core.IRepository { public interface IAdvertisementRepository { int Sum(int i, int j); int Add(Advertisement model); bool Delete(Advertisement model); bool Update(Advertisement model); List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression); } }
编译项目,提示错误,别慌!很正常,因为我们现在只是添加了接口,还没有实现接口。
二、在Blog.Core.Repository 层中实现Blog.Core.IRepository 所定义的CURD接口
当然,我们还是在AdvertisementRepository.cs文件中操作,这里我有一个小技巧,不知道大家是否用到过,因为我比较喜欢写接口,这样不仅可以不暴露核心代码,而且也可以让用户调用的时候,直接看到简单的接口方法列表,而不去管具体的实现过程,这样的设计思路还是比较提倡的,如下图:
你先看到了继承的接口有红色的波浪线,证明有错误,然后右键该接口,点击 Quick Actions and Refactorings...,也就是 快速操作和重构 ,你就会看到VS的智能提示,双击左侧的Implement interface,也就是实现接口,如下图:
Visual Studio真是宇宙第一IDE,没的说 [手动点赞],然后就创建成功了,你就可以去掉throw处理,自定义代码编写了,当然,如果你不习惯或者害怕出错,那就手动写吧,也是很快的。
namespace Blog.Core.Repository { public class AdvertisementRepository : IAdvertisementRepository { public int Add(Advertisement model) { throw new NotImplementedException(); } public bool Delete(Advertisement model) { throw new NotImplementedException(); } public List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression) { throw new NotImplementedException(); } public int Sum(int i, int j) { return i + j; } public bool Update(Advertisement model) { throw new NotImplementedException(); } } }
这个时候我们重新编译项目,嗯!意料之中,没有错误,但是具体的数据持久化如何写呢?
三、引用轻量级的ORM框架——SqlSugar
首先什么是ORM, 对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。这些概念我就不细说了,自从开发这些年,一直在讨论的问题就是用ADO.NET还是用ORM框架,还记得前几年面试的时候,有一个经理问:
如果一个项目,你是用三层架构ADO,还是用ORM中的EF?
大家可以自由留言,我表示各有千秋吧,一个产品的存在即有合理性,我平时项目中也有ADO,也有EF,不过本系列教程中基于面向对象思想,面向接口思想,当然还有以后的面向切面编程(AOP),还是使用ORM框架,不过是一个轻量级的,EF比较重,我在我其他的项目中用到了.Net MVC 6.0 + EF Code First 的项目,如果大家需要,我也开源出去,方法Github上,请文末留言吧~
关于ORM有一些常见的框架,如SqlSugar、Dapper、EF、NHeberneit等等,这些我都或多或少的了解过,使用过,至于你要问我为啥用SqlSugar,只要一个原因,作者是中国人,嗯!没错,这里给他打个广告,本系列中的前端框架Vue,也是我们中国的,Vue作者尤雨溪,这里也祝福大家都能有自己的成绩,为国人争光!
扯远了,开始动手引入框架:
开始,我们需要先向 Repository 层中引入SqlSugar,如下:
1)直接在类库中通过Nuget引入 sqlSugarCore,一定是Core版本的!,我个人采用这个办法,因为项目已经比较成型
2)Github下载源码,然后项目引用(点击跳转到Github下载页)
注意:为什么要单独在仓储层来引入ORM持久化接口,是因为,降低耦合,如果以后想要换成EF或者Deper,只需要修改Repository就行了,其他都不需要修改,达到很好的解耦效果。
编译一切正常,继续
首先呢,你需要了解下sqlsugar的具体使用方法,http://www.codeisbug.com/Doc/8,你先自己在控制台可以简单试一试,这里就不细说了,如果大家有需要,我可以单开一个文章,重点讲解SqlSugar这一块。
1、在Blog.Core.Repository新建一个sugar文件夹,然后添加两个配置文件,BaseDBConfig.cs 和 DbContext.cs ,这个你如果看了上边的文档,那这两个应该就不是问题。
namespace Blog.Core.Repository { public class BaseDBConfig { public static string ConnectionString = File.ReadAllText(@"D:\my-file\dbCountPsw1.txt").Trim(); //正常格式是 //public static string ConnectionString = "server=.;uid=sa;pwd=sa;database=BlogDB"; //原谅我用配置文件的形式,因为我直接调用的是我的服务器账号和密码,安全起见 } }
//DbContext.cs,一个详细的上下文类,看不懂没关系,以后我会详细讲解
namespace Blog.Core.Repository { public class DbContext { private static string _connectionString; private static DbType _dbType; private SqlSugarClient _db; /// <summary> /// 连接字符串 /// Blog.Core /// </summary> public static string ConnectionString { get { return _connectionString; } set { _connectionString = value; } } /// <summary> /// 数据库类型 /// Blog.Core /// </summary> public static DbType DbType { get { return _dbType; } set { _dbType = value; } } /// <summary> /// 数据连接对象 /// Blog.Core /// </summary> public SqlSugarClient Db { get { return _db; } private set { _db = value; } } /// <summary> /// 数据库上下文实例(自动关闭连接) /// Blog.Core /// </summary> public static DbContext Context { get { return new DbContext(); } } /// <summary> /// 功能描述:构造函数 /// 作 者:Blog.Core /// </summary> private DbContext() { if (string.IsNullOrEmpty(_connectionString)) throw new ArgumentNullException("数据库连接字符串为空"); _db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = _connectionString, DbType = _dbType, IsAutoCloseConnection = true, IsShardSameThread = true, ConfigureExternalServices = new ConfigureExternalServices() { //DataInfoCacheService = new HttpRuntimeCache() }, MoreSettings = new ConnMoreSettings() { //IsWithNoLockQuery = true, IsAutoRemoveDataCache = true } }); } /// <summary> /// 功能描述:构造函数 /// 作 者:Blog.Core /// </summary> /// <param name="blnIsAutoCloseConnection">是否自动关闭连接</param> private DbContext(bool blnIsAutoCloseConnection) { if (string.IsNullOrEmpty(_connectionString)) throw new ArgumentNullException("数据库连接字符串为空"); _db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = _connectionString, DbType = _dbType, IsAutoCloseConnection = blnIsAutoCloseConnection, IsShardSameThread = true, ConfigureExternalServices = new ConfigureExternalServices() { //DataInfoCacheService = new HttpRuntimeCache() }, MoreSettings = new ConnMoreSettings() { //IsWithNoLockQuery = true, IsAutoRemoveDataCache = true } }); } #region 实例方法 /// <summary> /// 功能描述:获取数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <returns>返回值</returns> public SimpleClient<T> GetEntityDB<T>() where T : class, new() { return new SimpleClient<T>(_db); } /// <summary> /// 功能描述:获取数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <param name="db">db</param> /// <returns>返回值</returns> public SimpleClient<T> GetEntityDB<T>(SqlSugarClient db) where T : class, new() { return new SimpleClient<T>(db); } #region 根据数据库表生产实体类 /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> public void CreateClassFileByDBTalbe(string strPath) { CreateClassFileByDBTalbe(strPath, "Km.PosZC"); } /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> /// <param name="strNameSpace">命名空间</param> public void CreateClassFileByDBTalbe(string strPath, string strNameSpace) { CreateClassFileByDBTalbe(strPath, strNameSpace, null); } /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> /// <param name="strNameSpace">命名空间</param> /// <param name="lstTableNames">生产指定的表</param> public void CreateClassFileByDBTalbe( string strPath, string strNameSpace, string[] lstTableNames) { CreateClassFileByDBTalbe(strPath, strNameSpace, lstTableNames, string.Empty); } /// <summary> /// 功能描述:根据数据库表生产实体类 /// 作 者:Blog.Core /// </summary> /// <param name="strPath">实体类存放路径</param> /// <param name="strNameSpace">命名空间</param> /// <param name="lstTableNames">生产指定的表</param> /// <param name="strInterface">实现接口</param> public void CreateClassFileByDBTalbe( string strPath, string strNameSpace, string[] lstTableNames, string strInterface, bool blnSerializable = false) { if (lstTableNames != null && lstTableNames.Length > 0) { _db.DbFirst.Where(lstTableNames).IsCreateDefaultValue().IsCreateAttribute() .SettingClassTemplate(p => p = @" {using} namespace {Namespace} { {ClassDescription}{SugarTable}" + (blnSerializable ? "[Serializable]" : "") + @" public partial class {ClassName}" + (string.IsNullOrEmpty(strInterface) ? "" : (" : " + strInterface)) + @" { public {ClassName}() { {Constructor} } {PropertyName} } } ") .SettingPropertyTemplate(p => p = @" {SugarColumn} public {PropertyType} {PropertyName} { get { return _{PropertyName}; } set { if(_{PropertyName}!=value) { base.SetValueCall(" + "\"{PropertyName}\",_{PropertyName}" + @"); } _{PropertyName}=value; } }") .SettingPropertyDescriptionTemplate(p => p = " private {PropertyType} _{PropertyName};\r\n" + p) .SettingConstructorTemplate(p => p = " this._{PropertyName} ={DefaultValue};") .CreateClassFile(strPath, strNameSpace); } else { _db.DbFirst.IsCreateAttribute().IsCreateDefaultValue() .SettingClassTemplate(p => p = @" {using} namespace {Namespace} { {ClassDescription}{SugarTable}" + (blnSerializable ? "[Serializable]" : "") + @" public partial class {ClassName}" + (string.IsNullOrEmpty(strInterface) ? "" : (" : " + strInterface)) + @" { public {ClassName}() { {Constructor} } {PropertyName} } } ") .SettingPropertyTemplate(p => p = @" {SugarColumn} public {PropertyType} {PropertyName} { get { return _{PropertyName}; } set { if(_{PropertyName}!=value) { base.SetValueCall(" + "\"{PropertyName}\",_{PropertyName}" + @"); } _{PropertyName}=value; } }") .SettingPropertyDescriptionTemplate(p => p = " private {PropertyType} _{PropertyName};\r\n" + p) .SettingConstructorTemplate(p => p = " this._{PropertyName} ={DefaultValue};") .CreateClassFile(strPath, strNameSpace); } } #endregion #region 根据实体类生成数据库表 /// <summary> /// 功能描述:根据实体类生成数据库表 /// 作 者:Blog.Core /// </summary> /// <param name="blnBackupTable">是否备份表</param> /// <param name="lstEntitys">指定的实体</param> public void CreateTableByEntity<T>(bool blnBackupTable, params T[] lstEntitys) where T : class, new() { Type[] lstTypes = null; if (lstEntitys != null) { lstTypes = new Type[lstEntitys.Length]; for (int i = 0; i < lstEntitys.Length; i++) { T t = lstEntitys[i]; lstTypes[i] = typeof(T); } } CreateTableByEntity(blnBackupTable, lstTypes); } /// <summary> /// 功能描述:根据实体类生成数据库表 /// 作 者:Blog.Core /// </summary> /// <param name="blnBackupTable">是否备份表</param> /// <param name="lstEntitys">指定的实体</param> public void CreateTableByEntity(bool blnBackupTable, params Type[] lstEntitys) { if (blnBackupTable) { _db.CodeFirst.BackupTable().InitTables(lstEntitys); //change entity backupTable } else { _db.CodeFirst.InitTables(lstEntitys); } } #endregion #endregion #region 静态方法 /// <summary> /// 功能描述:获得一个DbContext /// 作 者:Blog.Core /// </summary> /// <param name="blnIsAutoCloseConnection">是否自动关闭连接(如果为false,则使用接受时需要手动关闭Db)</param> /// <returns>返回值</returns> public static DbContext GetDbContext(bool blnIsAutoCloseConnection = true) { return new DbContext(blnIsAutoCloseConnection); } /// <summary> /// 功能描述:设置初始化参数 /// 作 者:Blog.Core /// </summary> /// <param name="strConnectionString">连接字符串</param> /// <param name="enmDbType">数据库类型</param> public static void Init(string strConnectionString, DbType enmDbType = SqlSugar.DbType.SqlServer) { _connectionString = strConnectionString; _dbType = enmDbType; } /// <summary> /// 功能描述:创建一个链接配置 /// 作 者:Blog.Core /// </summary> /// <param name="blnIsAutoCloseConnection">是否自动关闭连接</param> /// <param name="blnIsShardSameThread">是否夸类事务</param> /// <returns>ConnectionConfig</returns> public static ConnectionConfig GetConnectionConfig(bool blnIsAutoCloseConnection = true, bool blnIsShardSameThread = false) { ConnectionConfig config = new ConnectionConfig() { ConnectionString = _connectionString, DbType = _dbType, IsAutoCloseConnection = blnIsAutoCloseConnection, ConfigureExternalServices = new ConfigureExternalServices() { //DataInfoCacheService = new HttpRuntimeCache() }, IsShardSameThread = blnIsShardSameThread }; return config; } /// <summary> /// 功能描述:获取一个自定义的DB /// 作 者:Blog.Core /// </summary> /// <param name="config">config</param> /// <returns>返回值</returns> public static SqlSugarClient GetCustomDB(ConnectionConfig config) { return new SqlSugarClient(config); } /// <summary> /// 功能描述:获取一个自定义的数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <param name="sugarClient">sugarClient</param> /// <returns>返回值</returns> public static SimpleClient<T> GetCustomEntityDB<T>(SqlSugarClient sugarClient) where T : class, new() { return new SimpleClient<T>(sugarClient); } /// <summary> /// 功能描述:获取一个自定义的数据库处理对象 /// 作 者:Blog.Core /// </summary> /// <param name="config">config</param> /// <returns>返回值</returns> public static SimpleClient<T> GetCustomEntityDB<T>(ConnectionConfig config) where T : class, new() { SqlSugarClient sugarClient = GetCustomDB(config); return GetCustomEntityDB<T>(sugarClient); } #endregion } }
2、然后在刚刚我们实现那四个方法的AdvertisementRepository.cs中,重写构造函数,编辑统一Sqlsugar实例方法,用到了私有属性,为以后的单列模式做准备。
private DbContext context; private SqlSugarClient db; private SimpleClient<Advertisement> entityDB; internal SqlSugarClient Db { get { return db; } private set { db = value; } } public DbContext Context { get { return context; } set { context = value; } } public AdvertisementRepository() { DbContext.Init(BaseDBConfig.ConnectionString); context = DbContext.GetDbContext();
db = context.Db; entityDB = context.GetEntityDB<Advertisement>(db); }
3、正式开始写持久化逻辑代码(注意:我在Model层中,添加了全局的数据类型转换方法,UtilConvert,这样就不用每次都Convert,而且也解决了为空转换异常的bug)
public static class UtilConvert { /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static int ObjToInt(this object thisValue) { int reval = 0; if (thisValue == null) return 0; if (thisValue != null && thisValue != DBNull.Value && int.TryParse(thisValue.ToString(), out reval)) { return reval; } return reval; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static int ObjToInt(this object thisValue, int errorValue) { int reval = 0; if (thisValue != null && thisValue != DBNull.Value && int.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static double ObjToMoney(this object thisValue) { double reval = 0; if (thisValue != null && thisValue != DBNull.Value && double.TryParse(thisValue.ToString(), out reval)) { return reval; } return 0; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static double ObjToMoney(this object thisValue, double errorValue) { double reval = 0; if (thisValue != null && thisValue != DBNull.Value && double.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static string ObjToString(this object thisValue) { if (thisValue != null) return thisValue.ToString().Trim(); return ""; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static string ObjToString(this object thisValue, string errorValue) { if (thisValue != null) return thisValue.ToString().Trim(); return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static Decimal ObjToDecimal(this object thisValue) { Decimal reval = 0; if (thisValue != null && thisValue != DBNull.Value && decimal.TryParse(thisValue.ToString(), out reval)) { return reval; } return 0; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static Decimal ObjToDecimal(this object thisValue, decimal errorValue) { Decimal reval = 0; if (thisValue != null && thisValue != DBNull.Value && decimal.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static DateTime ObjToDate(this object thisValue) { DateTime reval = DateTime.MinValue; if (thisValue != null && thisValue != DBNull.Value && DateTime.TryParse(thisValue.ToString(), out reval)) { reval = Convert.ToDateTime(thisValue); } return reval; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <param name="errorValue"></param> /// <returns></returns> public static DateTime ObjToDate(this object thisValue, DateTime errorValue) { DateTime reval = DateTime.MinValue; if (thisValue != null && thisValue != DBNull.Value && DateTime.TryParse(thisValue.ToString(), out reval)) { return reval; } return errorValue; } /// <summary> /// /// </summary> /// <param name="thisValue"></param> /// <returns></returns> public static bool ObjToBool(this object thisValue) { bool reval = false; if (thisValue != null && thisValue != DBNull.Value && bool.TryParse(thisValue.ToString(), out reval)) { return reval; } return reval; } }
最终的仓储持久化是:
public class AdvertisementRepository : IAdvertisementRepository { private DbContext context; private SqlSugarClient db; private SimpleClient<Advertisement> entityDB; internal SqlSugarClient Db { get { return db; } private set { db = value; } } public DbContext Context { get { return context; } set { context = value; } } public AdvertisementRepository() { DbContext.Init(BaseDBConfig.ConnectionString); context = DbContext.GetDbContext();
db = context.Db; entityDB = context.GetEntityDB<Advertisement>(db); } public int Add(Advertisement model) { //返回的i是long类型,这里你可以根据你的业务需要进行处理 var i = db.Insertable(model).ExecuteReturnBigIdentity(); return i.ObjToInt(); } public bool Delete(Advertisement model) { var i = db.Deleteable(model).ExecuteCommand(); return i > 0; } public List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression) { return entityDB.GetList(whereExpression); } public int Sum(int i, int j) { return i + j; } public bool Update(Advertisement model) { //这种方式会以主键为条件 var i = db.Updateable(model).ExecuteCommand(); return i > 0; } }
四、在 Blog.Core.IServices 层设计CURD接口,和仓储接口一样,在Blog.Core.Services去实现
这里不细说,记得添加引用,最终的代码是:
namespace Blog.Core.IServices { public interface IAdvertisementServices { int Sum(int i, int j); int Add(Advertisement model); bool Delete(Advertisement model); bool Update(Advertisement model); List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression); } }
namespace Blog.Core.Services { public class AdvertisementServices : IAdvertisementServices { public IAdvertisementRepository dal = new AdvertisementRepository(); public int Sum(int i, int j) { return dal.Sum(i, j); } public int Add(Advertisement model) { return dal.Add(model); } public bool Delete(Advertisement model) { return dal.Delete(model); } public List<Advertisement> Query(Expression<Func<Advertisement, bool>> whereExpression) { return dal.Query(whereExpression); } public bool Update(Advertisement model) { return dal.Update(model); } } }
都是很简单,如果昨天的Sum方法你会了,这个肯定都会。
五、Controller测试接口,数据库Sql生成语句在wwwroot文件中
实现工作,根据id获取数据
这里为了调试方便,我把权限验证暂时注释掉
//[Authorize(Policy ="Admin")]
然后修改我们的其中一个Get方法,根据id获取信息
// GET: api/Blog/5 /// <summary> /// /// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet("{id}", Name = "Get")] public List<Advertisement> Get(int id) { IAdvertisementServices advertisementServices = new AdvertisementServices(); return advertisementServices.Query(d => d.Id == id); }
接下来运行调试,在我们接口文档中,直接点击调试
得到的结果是如果,虽然是空的,但是返回结果http代码是200,因为表中没数据嘛
六、结语
好啦,今天的讲解就到这里,你简单的了解了什么是ORM,以及其中的SqlSugar,然后呢,仓储模式的具体使用,最后还有真正的连接数据库,获取到数据,下一节中,我们继续来解决两大问题,来实现泛型仓储。