符合中小企业对网站设计、功能常规化式的企业展示型网站建设
本套餐主要针对企业品牌型网站、中高端设计、前端互动体验...
商城网站建设因基本功能的需求不同费用上面也有很大的差别...
手机微信网站开发、微信官网、微信商城网站...
这篇文章运用简单易懂的例子给大家介绍如何实现EF通用数据层封装类,代码非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
创新互联建站长期为成百上千家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为杏花岭企业提供专业的成都做网站、网站设计,杏花岭网站改版等技术服务。拥有十余年丰富建站经验和众多成功案例,为您定制开发。
记得四年前在学校第一次接触到 Ling to Sql,那时候瞬间发现不用手写sql语句是多么的方便,后面慢慢的接触了许多orm框架,像 EF,Dapper,Hibernate,ServiceStack.OrmLite等。当然每种orm都有各自的优势,也有不足的地方。园子里也有很多大神开源了他们写的orm,如SqlSugar,Chloe.ORM,CYQ.Data等。先不说这些开源的orm使用度怎么样,我觉得起码从开源的精神上就很可嘉了,我也曾下载过这几位大神的源码进行学习。
所有orm最终体现的一点就是方便,减少程序员重复性的工作,当然目前还有一些公司还是使用手写sql的方式来做,我觉得整个项目都使用手写sql来做真的是有点闲到蛋疼,并不是不推荐手写sql的方式,只是个人觉得最基本的增删改查这些都手写的话,那其实考验不是能力,而是耐力。有人说手写sql的方式可控性强,性能高,我想说的是orm也能做到,关键是你怎么去使用。
orm的优点非常明显,开发便捷,但或许也是由于这个优点,让很多偷懒的程序员也会渐渐忘了sql语句的写法,我遇到过很多的程序员朋友用了EF后,手写sql,视图、存储过程这些都不想用了,我个人觉手写sql这种还是必要的。不然某一天你看到别人的程序里面写着 “exec xxxx”,你就会突然觉得“啊,好像在哪里见过.....”。所以我想说的是“该出手时还是得出手"。
Entity Framework 是微软家的orm框架,随着 Entity Framework 不断的完善强化,目前相信使用的比例相对其他的orm来说还是较高的。像我目前使用的最多的就是EF和Dapper。确实,EF用起来开发过程中会方便很多,毕竟EF走过了这么年头,无论是成熟度,还是性能等都提高了很多,也有很多开发者为EF提供了扩展功能,如entity framework extended等。而且作为.net开发者来说项目通用性也很强,资料也多,微软在这块的更新力度也很给力。不过之前刚出的EF Core也存在一些坑,毕竟还是初期阶段,相信现在后面会越来越好的。
Entity Framework 提供了三种开发模式,code first,db first,model first。目前用的最多的就属code first了。至于这三种模式的简单使用和区别,大家可以参考下这篇文章。
我曾听一些朋友说过说EF使用起来性能很差,生成的sql语句很难看等。我觉得说这种话之前还是先检查下代码或者多看下一些EF文章吧,要先确保自己没给自己挖坑,然后才能指责别人的不好。如果真心觉得EF或者其他的orm用起来很不爽,那就自己写一个吧,我也曾经和同事用Dapper扩展一个通用的orm,当时是出于一种学习和使用方便的角度。
这里提供下 EF 通用数据层父类方法,其实网上也有很多人提供了自己项目中的 EF 通用数据层父类方法,所以这里提供的并不是最优和最好的选择,只能说是可以通用的类,方便大家学习和使用,具体代码如下:
public class DbContextFactory {public DbContext GetDbContext() {string key = typeof(DBContext.DbContextFactory).Name + "XJHDbContext"; DbContext dbContext = CallContext.GetData(key) as DbContext;if (dbContext == null) { dbContext = new XJHDbContext(); CallContext.SetData(key, dbContext); }return dbContext; } }
public class DbBase {protected DbContext Db = new DbContextFactory().GetDbContext(); #region 自定义其他方法////// 执行存储过程或自定义sql语句--返回集合(自定义返回类型)/// /// /// /// ///public List Query (string sql, List parms, CommandType cmdType = CommandType.Text) {//存储过程(exec getActionUrlId @name,@ID)if (cmdType == CommandType.StoredProcedure) { StringBuilder paraNames = new StringBuilder();foreach (var sqlPara in parms) { paraNames.Append($" @{sqlPara},"); } sql = paraNames.Length > 0 ? $"exec {sql} {paraNames.ToString().Trim(',')}" : $"exec {sql} "; }var list = Db.Database.SqlQuery (sql, parms.ToArray());var enityList = list.ToList();return enityList; }/// /// 自定义语句和存储过程的增删改--返回影响的行数/// /// /// /// ///public int Execute(string sql, List parms, CommandType cmdType = CommandType.Text) {//存储过程(exec getActionUrlId @name,@ID)if (cmdType == CommandType.StoredProcedure) { StringBuilder paraNames = new StringBuilder();foreach (var sqlPara in parms) { paraNames.Append($" @{sqlPara},"); } sql = paraNames.Length > 0 ?$"exec {sql} {paraNames.ToString().Trim(',')}" : $"exec {sql} "; }int ret = Db.Database.ExecuteSqlCommand(sql, parms.ToArray());return ret; }#endregion 自定义其他方法}/// /// mssql数据库 数据层 父类/// ///public class DbBase : DbBase where T : class, new() {#region INSERT/// /// 新增 实体/// /// ///public void Insert(T model) { Db.Set ().Add(model); }/// /// 普通批量插入/// /// public void InsertRange(Listdatas) { Db.Set ().AddRange(datas); }#endregion INSERT#region DELETE/// /// 根据模型删除/// /// 包含要删除id的对象///public void Delete(T model) { Db.Set ().Attach(model); Db.Set ().Remove(model); }/// /// 删除/// /// public void Delete(Expression> whereLambda) { Db.Set ().Where(whereLambda).Delete(); }#endregion DELETE#region UPDATE/// /// 单个对象指定列修改/// /// 要修改的实体对象/// 要修改的 属性 名称/// ///public void Update(T model, List proNames, bool isProUpdate = true) {//将 对象 添加到 EF中Db.Set ().Attach(model);var setEntry = ((IObjectContextAdapter)Db).ObjectContext.ObjectStateManager.GetObjectStateEntry(model);//指定列修改if (isProUpdate) {foreach (string proName in proNames) { setEntry.SetModifiedProperty(proName); } }//忽略类修改else{ Type t = typeof(T); List proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();foreach (var item in proInfos) {string proName = item.Name;if (proNames.Contains(proName)) {continue; } setEntry.SetModifiedProperty(proName); } } }/// /// 单个对象修改/// /// ///public void Update(T model) { DbEntityEntry entry = Db.Entry (model); Db.Set ().Attach(model); entry.State = EntityState.Modified; }/// /// 批量修改/// /// /// public void Update(Expression> whereLambda, Expression > updateExpression) { Db.Set ().Where(whereLambda).Update(updateExpression); }/// /// 批量修改/// /// ///public void UpdateAll(List models) {foreach (var model in models) { DbEntityEntry entry = Db.Entry(model); entry.State = EntityState.Modified; } }/// /// 批量统一修改/// /// 要修改的实体对象/// 查询条件/// ///public void Update(T model, Expression > whereLambda, params string[] modifiedProNames) {//查询要修改的数据List listModifing = Db.Set ().Where(whereLambda).ToList(); Type t = typeof(T); List proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList(); Dictionary dictPros = new Dictionary (); proInfos.ForEach(p =>{if (modifiedProNames.Contains(p.Name)) { dictPros.Add(p.Name, p); } });if (dictPros.Count <= 0) {throw new Exception("指定修改的字段名称有误或为空"); }foreach (var item in dictPros) { PropertyInfo proInfo = item.Value;//取出 要修改的值object newValue = proInfo.GetValue(model, null);//批量设置 要修改 对象的 属性foreach (T oModel in listModifing) {//为 要修改的对象 的 要修改的属性 设置新的值proInfo.SetValue(oModel, newValue, null); } } }#endregion UPDATE#region SELECT/// /// 根据主键查询/// /// ///public T FindById(dynamic id) {return Db.Set ().Find(id); }/// /// 获取默认一条数据,没有则为NULL/// /// ///public T FirstOrDefault(Expression > whereLambda = null) {if (whereLambda == null) {return Db.Set ().FirstOrDefault(); }return Db.Set ().FirstOrDefault(whereLambda); }/// /// 获取全部数据/// ///public List GetAll(string ordering = null) {return ordering == null? Db.Set ().ToList() : Db.Set ().OrderBy(ordering).ToList(); }/// /// 带条件查询获取数据/// /// /// ///public List GetAll(Expression > whereLambda, string ordering = null) {var iQueryable = Db.Set ().Where(whereLambda);return ordering == null? iQueryable.ToList() : iQueryable.OrderBy(ordering).ToList(); }/// /// 带条件查询获取数据/// /// ///public IQueryable GetAllIQueryable(Expression > whereLambda = null) {return whereLambda == null ? Db.Set () : Db.Set ().Where(whereLambda); }/// /// 获取数量/// /// ///public int GetCount(Expression > whereLambd = null) {return whereLambd == null ? Db.Set ().Count() : Db.Set ().Where(whereLambd).Count(); }/// /// 判断对象是否存在/// /// ///public bool Any(Expression > whereLambd) {return Db.Set ().Where(whereLambd).Any(); }/// /// 分页查询/// /// 当前页码/// 每页大小/// 总条数/// 排序条件(一定要有)/// 查询添加(可有,可无)/// 是否是Order排序///public List Page (int pageIndex, int pageSize, out int rows, Expression > orderBy, Expression > whereLambda = null, bool isOrder = true) { IQueryable data = isOrder ?Db.Set ().OrderBy(orderBy) : Db.Set ().OrderByDescending(orderBy);if (whereLambda != null) { data = data.Where(whereLambda); } rows = data.Count();return data.PageBy((pageIndex - 1) * pageSize, pageSize).ToList(); }/// /// 分页查询/// /// 当前页码/// 每页大小/// 总条数/// 排序条件(一定要有)/// 查询添加(可有,可无)///public List Page(int pageIndex, int pageSize, out int rows, string ordering, Expression > whereLambda = null) {// 分页 一定注意: Skip 之前一定要 OrderByvar data = Db.Set ().OrderBy(ordering);if (whereLambda != null) { data = data.Where(whereLambda); } rows = data.Count();return data.PageBy((pageIndex - 1) * pageSize, pageSize).ToList(); }/// /// 查询转换/// ////// /// public List Select (Expression > whereLambda) {return Db.Set ().Where(whereLambda).Select ().ToList(); }#endregion SELECT#region ORTHER/// /// 执行存储过程或自定义sql语句--返回集合/// /// /// /// ///public List Query(string sql, List parms, CommandType cmdType = CommandType.Text) {return Query (sql, parms, cmdType); }/// /// 提交保存/// ///public int SaveChanges() {return Db.SaveChanges(); }/// /// 回滚/// public void RollBackChanges() {var items = Db.ChangeTracker.Entries().ToList(); items.ForEach(o => o.State = EntityState.Unchanged); }#endregion ORTHER}
上面的通用类是比较基础简单通用的,适合于单库读写操作。对于EF实现读写分离,之前网上找过类似的参考文章,很多人文章都是使用 DbCommandInterceptor拦截器来实现,具体的做法是通过拦截到sql语句,然后根据具体条件去判断是走主库还是从库。这种做法不是不行,只是个人感觉不是很好扩展,而且要在拦截器里面做限制判断。
其实说白了EF本身就是一个读写分离的orm。用过EF的人知道,EF提供访问数据库的是 DbContext这个对象,所以想实现读写分离的就很简单了,只要在程序中使用两个不同的DbContext对象,一个负责读,一个负责写就好了。
所以在上面提供的通用封装类中稍微做下修改,修改如下DbContextFactory中获取DbContext的方法,实现一个读的DbContext和一个写的DbContext对象的获取。
这里要注意下,对于读的DbContext来说,要做下设置
1.使用 Database.SetInitializer(new NullDatabaseInitializer
2.重写 SaveChanges方法,对应从库来说,只提供读取的功能,所以防止误操作,这里禁用掉SaveChanges方法,一般需要使用从读的保存方法,就对外抛出异常。
代码如下:
public class DbContextFactory {public DbContext GetWriteDbContext(){string key = typeof(DbContextFactory).Name + "WriteDbContext"; DbContext dbContext = CallContext.GetData(key) as DbContext;if (dbContext == null) { dbContext = new WriteDbContext(); CallContext.SetData(key, dbContext); }return dbContext; }public DbContext GetReadDbContext(){string key = typeof(DbContextFactory).Name + "ReadDbContext"; DbContext dbContext = CallContext.GetData(key) as DbContext;if (dbContext == null) { dbContext = new ReadDbContext(); CallContext.SetData(key, dbContext); }return dbContext; } }
对应的 DbBase类也做下修改,主要将上面的Db对象改作 MasterDb和 SlaveDb对象,并且把上面的读写方法坐下调整,修改后如下:
public class DbBase {//是否读写分离(可以配置在配置文件中)private static readonly bool IsReadWriteSeparation = true;#region EF上下文对象(主库)protected DbContext MasterDb => _masterDb.Value;private readonly Lazy_masterDb = new Lazy (() => new DbContextFactory().GetWriteDbContext());#endregion EF上下文对象(主库)#region EF上下文对象(从库)protected DbContext SlaveDb => IsReadWriteSeparation ? _slaveDb.Value : _masterDb.Value;private readonly Lazy _slaveDb = new Lazy (() => new DbContextFactory().GetReadDbContext());#endregion EF上下文对象(从库)#region 自定义其他方法/// /// 执行存储过程或自定义sql语句--返回集合(自定义返回类型)/// /// /// /// ///public List Query (string sql, List parms, CommandType cmdType = CommandType.Text) {//存储过程(exec getActionUrlId @name,@ID)if (cmdType == CommandType.StoredProcedure) { StringBuilder paraNames = new StringBuilder();foreach (var sqlPara in parms) { paraNames.Append($" @{sqlPara},"); } sql = paraNames.Length > 0 ? $"exec {sql} {paraNames.ToString().Trim(',')}" : $"exec {sql} "; }var list = SlaveDb.Database.SqlQuery (sql, parms.ToArray());var enityList = list.ToList();return enityList; }/// /// 自定义语句和存储过程的增删改--返回影响的行数/// /// /// /// ///public int Execute(string sql, List parms, CommandType cmdType = CommandType.Text) {//存储过程(exec getActionUrlId @name,@ID)if (cmdType == CommandType.StoredProcedure) { StringBuilder paraNames = new StringBuilder();foreach (var sqlPara in parms) { paraNames.Append($" @{sqlPara},"); } sql = paraNames.Length > 0 ?$"exec {sql} {paraNames.ToString().Trim(',')}" : $"exec {sql} "; }int ret = MasterDb.Database.ExecuteSqlCommand(sql, parms.ToArray());return ret; }#endregion 自定义其他方法}/// /// mssql数据库 数据层 父类/// ///public class DbBase : DbBase where T : class, new() {#region INSERT/// /// 新增 实体/// /// ///public void Insert(T model) { MasterDb.Set ().Add(model); }/// /// 普通批量插入/// /// public void InsertRange(Listdatas) { MasterDb.Set ().AddRange(datas); }#endregion INSERT#region DELETE/// /// 根据模型删除/// /// 包含要删除id的对象///public void Delete(T model) { MasterDb.Set ().Attach(model); MasterDb.Set ().Remove(model); }/// /// 删除/// /// public void Delete(Expression> whereLambda) { MasterDb.Set ().Where(whereLambda).Delete(); }#endregion DELETE#region UPDATE/// /// 单个对象指定列修改/// /// 要修改的实体对象/// 要修改的 属性 名称/// ///public void Update(T model, List proNames, bool isProUpdate = true) {//将 对象 添加到 EF中MasterDb.Set ().Attach(model);var setEntry = ((IObjectContextAdapter)MasterDb).ObjectContext.ObjectStateManager.GetObjectStateEntry(model);//指定列修改if (isProUpdate) {foreach (string proName in proNames) { setEntry.SetModifiedProperty(proName); } }//忽略类修改else{ Type t = typeof(T); List proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();foreach (var item in proInfos) {string proName = item.Name;if (proNames.Contains(proName)) {continue; } setEntry.SetModifiedProperty(proName); } } }/// /// 单个对象修改/// /// ///public void Update(T model) { DbEntityEntry entry = MasterDb.Entry (model); MasterDb.Set ().Attach(model); entry.State = EntityState.Modified; }/// /// 批量修改/// /// /// public void Update(Expression> whereLambda, Expression > updateExpression) { MasterDb.Set ().Where(whereLambda).Update(updateExpression); }/// /// 批量修改/// /// ///public void UpdateAll(List models) {foreach (var model in models) { DbEntityEntry entry = MasterDb.Entry(model); entry.State = EntityState.Modified; } }/// /// 批量统一修改/// /// 要修改的实体对象/// 查询条件/// ///public void Update(T model, Expression > whereLambda, params string[] modifiedProNames) {//查询要修改的数据List listModifing = MasterDb.Set ().Where(whereLambda).ToList(); Type t = typeof(T); List proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList(); Dictionary dictPros = new Dictionary (); proInfos.ForEach(p =>{if (modifiedProNames.Contains(p.Name)) { dictPros.Add(p.Name, p); } });if (dictPros.Count <= 0) {throw new Exception("指定修改的字段名称有误或为空"); }foreach (var item in dictPros) { PropertyInfo proInfo = item.Value;//取出 要修改的值object newValue = proInfo.GetValue(model, null);//批量设置 要修改 对象的 属性foreach (T oModel in listModifing) {//为 要修改的对象 的 要修改的属性 设置新的值proInfo.SetValue(oModel, newValue, null); } } }#endregion UPDATE#region SELECT/// /// 根据主键查询/// /// ///public T FindById(dynamic id) {return SlaveDb.Set ().Find(id); }/// /// 获取默认一条数据,没有则为NULL/// /// ///public T FirstOrDefault(Expression > whereLambda = null) {if (whereLambda == null) {return SlaveDb.Set ().FirstOrDefault(); }return SlaveDb.Set ().FirstOrDefault(whereLambda); }/// /// 获取全部数据/// ///public List GetAll(string ordering = null) {return ordering == null? SlaveDb.Set ().ToList() : SlaveDb.Set ().OrderBy(ordering).ToList(); }/// /// 带条件查询获取数据/// /// /// ///public List GetAll(Expression > whereLambda, string ordering = null) {var iQueryable = SlaveDb.Set ().Where(whereLambda);return ordering == null? iQueryable.ToList() : iQueryable.OrderBy(ordering).ToList(); }/// /// 带条件查询获取数据/// /// ///public IQueryable GetAllIQueryable(Expression > whereLambda = null) {return whereLambda == null ? SlaveDb.Set () : SlaveDb.Set ().Where(whereLambda); }/// /// 获取数量/// /// ///public int GetCount(Expression > whereLambd = null) {return whereLambd == null ? SlaveDb.Set ().Count() : SlaveDb.Set ().Where(whereLambd).Count(); }/// /// 判断对象是否存在/// /// ///public bool Any(Expression > whereLambd) {return SlaveDb.Set ().Where(whereLambd).Any(); }/// /// 分页查询/// /// 当前页码/// 每页大小/// 总条数/// 排序条件(一定要有)/// 查询添加(可有,可无)/// 是否是Order排序///public List Page (int pageIndex, int pageSize, out int rows, Expression > orderBy, Expression > whereLambda = null, bool isOrder = true) { IQueryable data = isOrder ?SlaveDb.Set ().OrderBy(orderBy) : SlaveDb.Set ().OrderByDescending(orderBy);if (whereLambda != null) { data = data.Where(whereLambda); } rows = data.Count();return data.PageBy((pageIndex - 1) * pageSize, pageSize).ToList(); }/// /// 分页查询/// /// 当前页码/// 每页大小/// 总条数/// 排序条件(一定要有)/// 查询添加(可有,可无)///public List Page(int pageIndex, int pageSize, out int rows, string ordering, Expression > whereLambda = null) {// 分页 一定注意: Skip 之前一定要 OrderByvar data = SlaveDb.Set ().OrderBy(ordering);if (whereLambda != null) { data = data.Where(whereLambda); } rows = data.Count();return data.PageBy((pageIndex - 1) * pageSize, pageSize).ToList(); }/// /// 查询转换/// ////// /// public List Select (Expression > whereLambda) {return SlaveDb.Set ().Where(whereLambda).Select ().ToList(); }#endregion SELECT#region ORTHER/// /// 执行存储过程或自定义sql语句--返回集合/// /// /// /// ///public List Query(string sql, List parms, CommandType cmdType = CommandType.Text) {return Query (sql, parms, cmdType); }/// /// 提交保存/// ///public int SaveChanges() {return MasterDb.SaveChanges(); }/// /// 回滚/// public void RollBackChanges() {var items = MasterDb.ChangeTracker.Entries().ToList(); items.ForEach(o => o.State = EntityState.Unchanged); }#endregion ORTHER}
这样简单的读写分离就实现了,实现逻辑也比较清晰,方便扩展。
一般做读写分离,都会做一主多从,特别对读取量比较大的项目,这样多库读取就能减轻读库的压力。所以对于上面的方法,做下改造。
上面可以看到,主库和从库都是通过 DbContextFactory 这个类来获取的,在GetReadDbContext 方法中每次都是获取 ReadDbContext这个对象。那么对于多个从库的情况下,每次读取到底要去哪个库读取数据呢?这里就是一个算法规则的问题了,或者说是策略吧,如果使用过nginx的朋友就知道,nginx本身内部在实现负载均衡的时候提供了多种策略,比如轮询,加权轮询,ip_hash等策略。其实上面获取同一个ReadDbContext 的方法也算一种策略,叫单一策略,每次都获取单一的对象。
多从库的情况下,我们简单的来实现另一种获取策略,随机策略,每次都随机获取到一个从库的对象,这种是最简单的策略,当然,正式使用的话大家可以发挥自己的创造力,写出多了的算法策略。
首先,定义一个策略接口,方便策略的扩展和切换,代码如下:
////// 从数据库获取策略接口 /// public interface IReadDbStrategy { ////// 获取读库 /// ///DbContext GetDbContext(); }
单从库情况下,定义一个单一策略,代码如下:
////// 单一策略 /// public class SingleStrategy : IReadDbStrategy { public DbContext GetDbContext() { return new ReadDbContext(); } }
多从库情况下,定义一个随机策略,代码如下:
////// 随机策略/// public class RandomStrategy : IReadDbStrategy {//所有读库类型public static ListDbTypes; static RandomStrategy() { LoadDbs(); } //加载所有的读库类型static void LoadDbs() { DbTypes = new List ();var assembly = Assembly.GetExecutingAssembly();var types = assembly.GetTypes();foreach (var type in types) {if (type.BaseType == typeof(BaseReadDbContext)) { DbTypes.Add(type); } } } public DbContext GetDbContext() {int randomIndex = new Random().Next(0, DbTypes.Count);var dbType = DbTypes[randomIndex];var dbContext = Activator.CreateInstance(dbType) as DbContext;return dbContext; } }
这样,所有从库我们都基于策略去获取,扩展也比较方便。修改下 DbContextFactory类的 GetReadDbContext 方法,通过策略接口来获取,代码如下:
public class DbContextFactory {//todo:这里可以自己通过注入的方式来实现,就会更加灵活private static readonly IReadDbStrategy ReadDbStrategy = new RandomStrategy();public DbContext GetWriteDbContext() {string key = typeof(DbContextFactory).Name + "WriteDbContext"; DbContext dbContext = CallContext.GetData(key) as DbContext;if (dbContext == null) { dbContext = new WriteDbContext(); CallContext.SetData(key, dbContext); }return dbContext; }public DbContext GetReadDbContext() {string key = typeof(DbContextFactory).Name + "ReadDbContext"; DbContext dbContext = CallContext.GetData(key) as DbContext;if (dbContext == null) { dbContext = ReadDbStrategy.GetDbContext();CallContext.SetData(key, dbContext); }return dbContext; } }
这样简单的一主多从也实现了。
关于如何实现EF通用数据层封装类就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。