发布网友 发布时间:2022-04-09 14:35
共2个回答
懂视网 时间:2022-04-09 18:56
linq和各种扩展方法,再加上实体模型,编写数据库的访问代码确实是优美、舒服,但是生成的sql不尽如意、性能低下,尤其是复杂些的逻辑关系,最终大家还是会回归自然,选择能够友好执行sql语句的ORM,认认真真的编写sql;问题是:EF是否也能够很友好的执行sql语句?EF提供直接执行sql语句的方法并不多,而且也是极其简单的;那是否容易进行扩展?答案是肯定的,在DbContext下提供了Database属性就是为了执行sql用的,然后自己就通过Database下的方法属性进行了扩展(不过最后为了各种数据库的兼容性,使用了DbContext的扩展方法GetService获取相应的服务进行sql语句的执行),以完成这个扩展类库的编写。扩展类库大体功能简介:
1) sql语句执行器:用于直接执行sql语句
2) EF的查询缓存器:IQueryable(linq) 或 sql语句 的查询缓存,分为本地存储 或 非本地存储(Redis)
a) 缓存存储:永久缓存(不过期) 或者 过期缓存
b) 缓存清理
3) sql配置管理器(让EFCore像MyBatis配置sql,但是通过json配置):加载与管理配置文件中的sql语句
a) sql配置执行器:用于执行配置的sql语句
b) 策略管理器:用于管理策略 与 策略执行器(目前分为三种策略执行器)
i. 策略管理:管理各种策略类型,用于初始化配置文件中的策略配置转换成对象
ii. 策略执行器(一般通过策略对象进行相应的处理)
1. 初始化型的策略执行器
a) 配置策略对象的初始化、替换表名、合并分部sql等的策略执行器
2. sql执行前的策略执行器
a) foreach策略执行器:对SqlParameter或者某些数据类型(list/dictionary/model)进行遍历生成字串替换到sql中
3. sql执行时的策略执行器
a) sql与参数的日志记录策略执行器
b) 查询缓存与清理策略执行器
4) 类库的扩展与优化(因为类库中的各种类是通过DI进行管理的,因此易于扩展与优化)
a) 将查询缓存存储到Redis中
b) 策略与策略执行器的扩展
c) 其他:例如反射帮助类的优化(如果有更好的实现,因为类库内部有不少实现需要通过反射)
源码:
github:https://github.com/skigs/EFCoreExtend
引用类库:
nuget:https://www.nuget.org/packages/EFCoreExtend/
PM> Install-Package EFCoreExtend
查询缓存引用Redis:
PM> Install-Package EFCoreExtend.Redis
类库的使用说明会分好几篇文章进行详细描述,也可参考源码(源码中也有使用测试),类库目前仅支持EFCore 1.1.0,兼容性:MSSqlServer、sqlite、mysql、PostgreSql基本都兼容(EFCore兼容的应该都可以兼容),因为刚完成不久,可能还存在一些bug或不合理的地方,望大家谅解,也请告知。
Person.json配置文件内容:
{ //"name" : "Person", //设置表名,如果不指定name,那么默认文件名为表名 //配置sql:key为Sql的名称(SqlName,获取配置sql执行器的时候需要根据key获取) "sqls": { "GetList": { //"sql": "select name,birthday,addrid from [Person] where name=@name or id=@id", "sql": "select name,birthday,addrid from ##tname where name=@name or id=@id", //##tname => 表名 "type": "query" } } }
表的实体模型:
1 [Table(nameof(Person))] 2 public class Person 3 { 4 public int id { get; set; } 5 public string name { get; set; } 6 [Column(TypeName = "datetime")] 7 public DateTime? birthday { get; set; } 8 public int? addrid { get; set; } 9 }View Code
DbContext(MSSqlServer、sqlite、mysql、PostgreSql):
1 public class MSSqlDBContext : DbContext 2 { 3 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 4 { 5 if (optionsBuilder.IsConfigured == false) 6 { 7 optionsBuilder.UseSqlServer(@"data source=localhost;initial catalog=TestDB;uid=sa;pwd=123;"); 8 } 9 base.OnConfiguring(optionsBuilder); 10 } 11 12 public DbSet<Person> Person { get; set; } 13 } 14 15 public class SqlieteDBContext : DbContext 16 { 17 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 18 { 19 if (optionsBuilder.IsConfigured == false) 20 { 21 optionsBuilder.UseSqlite(@"data source=./Datas/db.sqlite"); //把/Datas/db.sqlite放到bin下 22 } 23 base.OnConfiguring(optionsBuilder); 24 } 25 26 public DbSet<Person> Person { get; set; } 27 } 28 29 public class MysqlDBContext : DbContext 30 { 31 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 32 { 33 if (optionsBuilder.IsConfigured == false) 34 { 35 //SapientGuardian.EntityFrameworkCore.MySql 36 optionsBuilder.UseMySQL(@"Data Source=localhost;port=3306;Initial Catalog=testdb;user id=root;password=123456;"); 37 } 38 base.OnConfiguring(optionsBuilder); 39 } 40 41 public DbSet<Person> Person { get; set; } 42 } 43 44 public class PostgreSqlDBContext : DbContext 45 { 46 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 47 { 48 if (optionsBuilder.IsConfigured == false) 49 { 50 optionsBuilder.UseNpgsql(@"User ID=admin;Password=123456;Host=localhost;Port=5432;Database=TestDB;Pooling=true;"); 51 } 52 base.OnConfiguring(optionsBuilder); 53 } 54 55 public DbSet<Person> Person { get; set; } 56 }View Code
加载配置文件(在程序初始化的时候调用):
1 ////加载指定的配置文件 2 //EFHelper.Services.SqlConfigMgr.Config.LoadFile(Directory.GetCurrentDirectory() + "/Person.json"); 3 //加载指定目录下的所有json配置文件 4 EFHelper.Services.SqlConfigMgr.Config.LoadDirectory(Directory.GetCurrentDirectory() + "/Datas");
获取与调用配置sql的代码:
1 DbContext db = new MSSqlDBContext(); 2 //获取指定表(配置文件名)的配置信息 3 var tinfo = db.GetConfigTable<Person>(); 4 //获取指定sql的执行器 5 var exc = tinfo.GetExecutor(); //使用了CallerMemberNameAttribute,因此会自动获取 方法/属性名 作为参数 6 var exc1 = tinfo.GetExecutor("GetList"); //这行和上面的一样,"GetList"为在配置文件配置的key 7 8 //执行sql: 9 //方式一:使用SqlParameter传递sql参数 10 var rtn1 = exc.Query<Person>( //泛型为返回值数据类型 11 //SqlParams 12 new [] { new SqlParameter("name", "tom"), new SqlParameter("id", 1) }, 13 //返回值类型中需要忽略的属性 14 new[] { "id" }); //select name,birthday,addrid,并没有加载获取id,因此需要忽略,否则抛异常 15 16 //方式二:使用Dictionary传递sql参数 17 var rtn2 = exc.QueryUseDict<Person>( //泛型为返回值数据类型 18 //Dictionary => SqlParams 19 new Dictionary<string, object> 20 { 21 { "name", "tom" }, 22 { "id", 1 }, 23 }, 24 //返回值类型中需要忽略的属性 25 new[] { "id" }); //select name,birthday,addrid,并没有加载获取id,因此需要忽略,否则抛异常 26 27 //方式三:使用Model传递sql参数 28 var rtn3 = exc.QueryUseModel<Person>( 29 //Model => SqlParams 30 new { name = "tom", id = 1, addrid = 123 }, 31 //参数Model需要忽略的属性 32 new[] { "addrid" }, //where name=@name or id=@id,并不需要设置addrid 33 //返回值类型中需要忽略的属性 34 new[] { "id" }); //select name,birthday,addrid,并没有加载获取id,因此需要忽略,否则抛异常
{ //"name" : "Person", //设置表名,如果不指定name,那么默认文件名为表名 "policies": { ////表名策略 //"tname": { // //"tag": "##tname" //默认值为 ##tname // "prefix": "[", //前缀 // "suffix": "]" //后缀 //} }, //配置sql:key为Sql的名称(SqlName,获取配置sql执行器的时候需要根据key获取) "sqls": { "GetList": { //"sql": "select * from [Person] where name=@name", "sql": "select * from ##tname where name=@name", //##tname => Table Name "type": "query" //可以不设置,如果设置了会在执行前进行类型检测, // notsure(默认,不确定),query(查询), nonquery(非查询),scalar,nonexecute(不用于执行的sql,例如分部sql) }, "GetPerson": { "sql": "select * from ##tname where name=@name", "type": "query" }, "Count": { "sql": "select count(*) from ##tname", "type": "scalar" }, "UpdatePerson": { "sql": "update ##tname set birthday=@birthday, addrid=@addrid where name=@name", "type": "nonquery" }, "AddPerson": { "sql": "insert into ##tname(name, birthday, addrid) values(@name, @birthday, @addrid) ", "type": "nonquery" }, "DeletePerson": { "sql": "delete from ##tname where name=@name", "type": "nonquery" }, //执行存储过程 "ProcQuery": { "sql": "exec TestQuery @name", "type": "query" }, "ProcUpdate": { "sql": "exec TestUpdate @addrid,@name", "type": "nonquery" } } }View Code
调用sql配置的代码(包括事物处理):
1 public class PersonBLL 2 { 3 string _name = "tom"; 4 DBConfigTable tinfo; 5 public PersonBLL(DbContext db) 6 { 7 //获取指定表(配置文件名)的配置信息 8 tinfo = db.GetConfigTable<Person>(); 9 } 10 11 public IReadOnlyList<Person> GetList() 12 { 13 return tinfo.GetExecutor().QueryUseModel<Person>( 14 //Model => SqlParams 15 new { name = _name, id = 123 }, 16 //不需要的SqlParams 17 new[] { "id" }, 18 //返回值类型需要忽略的属性 19 new[] { "name" }); 20 21 } 22 23 public int AddPerson() 24 { 25 return tinfo.GetExecutor() //获取sql执行器 26 .NonQueryUseModel(new Person 27 { 28 addrid = 1, 29 birthday = DateTime.Now, 30 name = _name, 31 }, null); 32 } 33 34 public int UpdatePerson(int? addrid = null) 35 { 36 var exc = tinfo.GetExecutor(); 37 return exc.NonQueryUseModel(new { name = _name, birthday = DateTime.Now, addrid = addrid }, null); 38 } 39 40 public int DeletePerson() 41 { 42 return tinfo.GetExecutor().NonQueryUseModel(new 43 { 44 name = _name 45 }, null); 46 } 47 48 public int Count() 49 { 50 var exc = tinfo.GetExecutor(); 51 var rtn = exc.ScalarUseModel(new { name = _name }, null); 52 //MSSqlServer返回值会为int,而Sqlite会为long,转换就会出错,因此需要ChangeValueType 53 return (int)typeof(int).ChangeValueType(rtn); 54 } 55 56 public Person GetPerson() 57 { 58 return tinfo.GetExecutor().QueryUseModel<Person>(new 59 { 60 name = _name 61 }, null)?.FirstOrDefault(); 62 } 63 64 //执行存储过程 65 public IReadOnlyList<Person> ProcQuery() 66 { 67 ////Stored procedure sql: 68 //create proc TestQuery 69 //@name varchar(256) = null 70 //as 71 //begin 72 // select * from person where [name] = @name 73 //end 74 75 return tinfo.GetExecutor().QueryUseModel<Person>(new { name = "tom" }, null); 76 } 77 78 //执行存储过程 79 public int ProcUpdate() 80 { 81 ////Stored procedure sql: 82 //create proc TestUpdate 83 //@addrid int = 0, 84 //@name varchar(256) 85 //as 86 //begin 87 88 // update person set addrid = @addrid where[name] = @name 89 //end 90 91 return tinfo.GetExecutor().NonQueryUseModel(new { addrid = 3, name = "tom" }, null); 92 } 93 94 //事物 95 public void DoTran() 96 { 97 try 98 { 99 //开启事物 100 tinfo.DB.Database.BeginTransaction(); 101 bool bRtn = UpdatePerson() > 0; 102 bRtn &= AddPerson() > 0; 103 if (bRtn) 104 { 105 tinfo.DB.Database.CommitTransaction(); //提交 106 } 107 else 108 { 109 tinfo.DB.Database.RollbackTransaction(); //回滚 110 } 111 } 112 catch (Exception ex) 113 { 114 tinfo.DB.Database.RollbackTransaction(); //回滚 115 } 116 } 117 118 }View Code
配置sql除了通过配置文件之外 还可以通过代码进行配置的:
1 public void AddSqls() 2 { 3 EFHelper.Services.SqlConfigMgr.Config.AddSqls<Person>(new Dictionary<string, IConfigSqlInfo> 4 { 5 { 6 "UpdatePerson", //SqlName 7 new ConfigSqlInfo 8 { 9 Sql = $"update {nameof(Person)} set name=@name where id=@id", 10 Type = ConfigSqlExecuteType.nonquery, 11 } 12 }, 13 { 14 "GetPersonList", //SqlName 15 new ConfigSqlInfo热心网友 时间:2022-04-09 16:04
至开发上的一个巨大进步,.net程序员以对象方式操作数据,以类sql语法在程序里查询数据,大大减少了繁琐的构造SQL语句的工作,可以更加专注于编写业务逻辑代码。但是在多层架构的分布式应用系统中,实体对象通过远程序列化到客户端时,这些实体会与其数据上下文(也就是实体容器)分离,在客户端无法对实体直接进行查询以及CUD(Create,Update,Delete)操作,下面以SQL Server为数据库,Remoting+Entity Framework3.5作为数据服务层,WinForm作为客户端,讲述一下如何使用EF框架搭建多层分布式应用系统。 二、 技术分析 1. 通过远程客户端传输过来的实体,都是处于分离状态(EntityState属性值为Detached),所以在多层应用程序中的服务端实现实体的更新或删除时,关键是如何把实体附加回实体容器中。MSDN上关于对分离实体的查询和CUD操作描述如下: 1) 附加对象(实体框架) 在实体框架的某个对象上下文内执行查询时,返回的对象会自动附加到该对象上下文。还可以将从源而不是从查询获得的对象附加到对象上下文。您可以附加以前分离的对象、由 NoTracking 查询返回的对象或从对象上下文的外部获取的对象。还可以附加存储在 ASP.NET 应用程序的视图状态中的对象或从远程方法调用或 Web 服务返回的对象。 使用下列方法之一将对象附加到对象上下文: · 调用 ObjectContext 上的 AddObject 将对象附加到对象上下文。当对象为数据源中尚不存在的新对象时采用此方法。 · 调用 ObjectContext上的Attach 将对象附加到对象上下文。当对象已存在于数据源中但当前尚未附加到上下文时采用此方法。有关更多信息,请参见如何:附加相关对象(实体框架)。 · 调用 ObjectContext的AttachTo,以将对象附加到对象上下文中的特定实体集。如果对象具有 null(在 Visual Basic 中为 Nothing)EntityKey 值,也可以执行此操作。 · 调用 ObjectContext上的ApplyPropertyChanges。当对象已存在于数据源中,并且分离的对象具有您希望保存的属性更新时采用此方法。如果简单地附加该对象,则属性更改将丢失。有关更多信息,请参见如何:应用对已分离对象的更改(实体框架)。 2) 应用对已分离对象的更改(实体框架)示例代码 View Code 2. 实现动态条件查询。在本地环境中,对于Linq,我们可以通过动态构造Lambda表达式树来实现动态条件查询,但是在远程环境中,Lamdba表达式不支持远程序列化传输,只能通过ObjectContext的CreateQuery方法实现,但幸好微软后来又提供了一个LINQ动态查询扩展库Dynamic.cs,使用起来更方便,于是采用它实现。 3. EF中核心抽象类是ObjectContext,实体容器都从它派生,实体容器上的CUD方法其实都是通过调用ObjectContext的CUD操作方法实现的。1) AddObject(string,object):表示添加实体object到实体容器,只要实体的EntityKey值为空,无论是否Detached状态均可以通过此方法实现添加操作。2) ApplyPropertyChanges(string,object)表示把分离状态的实体object上的所作的修改更新回容器中已存在的对应的实体,执行条件有两个:①实体处于分离状态,②实体容器中存在主键值与其相同的且为Unchanged状态的实体,所以,当我们需要更新一个Detached状态的实体时,可以先把一个具有原始值的相同键值的实体附加回容器中,或者直接执行一下查询,从数据库中取出该实体。3) DeleteObject(object)表示从实体容器中删除一个实体,执行条件是该实体存在于实体容器中,所以删除一个Detach状态的实体之前,需要把它通过Attach方法附加回实体容器中。 4. 实体对象也是基于抽象类EntityObject派生的,由此我们完全可以用ContextObject和EntityObject实现服务端对实体的查询和CUD方法,其实现子类在运行时由客户端注入,从而使服务端和数据库实现松耦合。 5. 下图是MSDN上关于在数据访问层中使用 LINQ to SQL 的 n 层应用程序的基本体系结构图,其实EF的结构也是一样的,不过是把DataContext换成ObjectContext。 三、 动手开发 1. 利用EF建立数据库概念模型新建一个解决方案EFServiceSystem,添加一个新项目,命名为EFModel,添加项目,在项目下添加一个ADO.NET Entity Data Model项,命名为EFModel.edmx,选择从数据库生成(假设我们已经建好了一个SQL Server数据库),一路点击下一步,直至完成。编译项目成功后就算完成。为什么要把数据库模型单独编译成一个dll呢,我将在后面给予解释。 2. 建立数据服务层在解决方案下再添加一个类库项目,命名为EFService。1) 利用外观模式,我们把客户端常用的查询和CUD操作方法简化为3个方法Query<T>,Save(T t),Delete(T t),根据针对接口编程的设计原则,定义一个CUD方法接口供客户端调用。 View Code 2) 实现类EntityHelper的代码。主要思路是通过构造函数注入数据上下文实例名称,在配置文件取出其程序集限定名,通过反射创建实例,调用实例的相应方法实现接口。 View Code 3) 最后,我们创建一个服务工厂类,暴露给客户端,负责以接口方式向客户端提供远程服务对象,数据服务层创建完毕。 View Code 4) 补充一下Dynamic.cs的内容,省得你去网上找了View Code 3. 创建运行服务的宿主程序。实际开发中,通常选择创建一个windows服务程序来运行Remoting,但是服务需要安装才能启动,运行和调试起来都比较繁琐,所以这里创建一个简单的控制台程序来运行它。在解决方案下添加一个控制台程序项目,在program.cs编写如下代码: View Code 配置文件App.Config主要包括数据库连接信息以及自己定义一个数据上下文名称(这里和数据库连接名称相同,事实上不必相同),数据库连接信息可以从EFModel项目中配置文件中直接拷贝过来。内容如下: View Code 编译成功后,拷贝EFModel和和EFService两个项目生成的dll文件至可执行文件EFServiceHost.exe同一目录下,点击运行EFServiceHost.exe。 4. 最后,我们建立一个winform客户端作为测试。在program.cs注册远程服务: View Code 四、 部署应用 1. 至此,整个系统搭建完毕。在本例中,我把所有项目都统一建立在一个解决方案下,其实是为了演示方便,实际开发时候,完全可以各自独立创建。下面我们来分析一下各个项目的职能和相互之间的引用关系。 1) EFModel:由Visual Studio 的数据模型工具生成的数据库实例模型,提供数据的查询以及CUD操作。不需引用其它项目。 2) EFService:使用数据库实例模型以及实体的抽象基类编写完成,代码里不涉及具体数据库模型实例,运行时通过客户端注入参数和读取配置文件动态生成数据库模型实例,并调用实例的查询和CUD方法实现客户端的请求。不需引用其它项目。 3) EFServiceHost:负责运行Remoting服务,如果通过配置文件方式发布服务的话,编译时也不需引用其它项目,我这里引用了EFService项目,是因为使用了代码方式暴露EFSservice的服务类。运行时需要将EFService和EFModel的dll文件拷贝至运行目录下。 4) EFClient:需要引用EFModel和EFService。(注:因为本例中式使用了Remoting作为远程服务,如果是WebService或者WCF则只需添加服务引用,然后在本地生成客户端代理类)。事实上EFService中的实现类EntityHelper也可以独立出去,不必让客户端引用,对于客户端而言,仅仅是使用ServiceFactory和接口IentityHelper就足够了。这样只要接口不变,EntityHelper更新的时候,客户端无须更新引用,而且服务端代码可以完全被隔离开客户端,对一些服务端和客户端之间的保密性比较敏感的项目尤为有利。 2. 通过分析我们发现,在开发下一个新项目的时候,即使整个数据库都变了,从SQL SERVER变成Oracle,数据库服务名变了,表也变了,我们仍然无需修改服务端代码,只需针对新的数据库,生成新的EFModel,然后拷贝DLL文件至EFServiceHost的运行目录下(这也就是我为什么要把EFModel独立成一个项目的原因),再修改一下EFServiceHost的配置文件中的数据库连接和实体容器名称即可完成新系统的部署。对于客户端来说,也就是更新一下EFModel.dll,还是调用服务端提供的那几个API,便可完成查询和CUD操作,不用关心底层的数据库是SQL Server还是Oracle,更不用自己实现对新库新表的查询和CUD操作(本来也不用)。当然,对于正在运行的系统,我们也可以针对新建数据库生成新的实体模型DLL,拷贝至EFServiceHost运行目录下,实现热插拔方式扩展数据库,而对原来的系统毫无影响,即使新加的库是不同类型的库。 五、 系统架构图示 六、 总结 从以上分析可以看出,该系统运用到项目开发中,对服务端来说,实现了最大程度组件重用(零代码修改),对客户端开发来说,高度简化了对数据的操作命令,并封装了实现细节,大大降低了开发的技术难度,提高了开发速度。当然,我这里写的代码仅仅是最简单的演示代码,在实际项目开发中,服务端要处理的细节和扩展的功能要比这复杂得多。比如性能优化,实现复杂查询和批量CUD操作,并发处理,事务控制,日志跟踪,数据缓存等等。另外,如果各层采用不同的技术实现,服务层实现的代码也有差异。比如EF可以选择最新版的更完善更强大的EF4.0,远程服务可以选择Remoting,WebService,WCF等,不同的远程服务,宿主程序也有所不同,Remoting和WCF可以选择winform,控制台程序,IIS,而Web Service只能选择IIS。不同的服务,不同的宿主程序,会有不同的通信通道 (Http,Tcp),不同的数据传输格式 (二进制,XML,JSON)。如果你嫌上面的实现方式涉及的技术太多,开发起来太麻烦,那么,微软现成的具有REST风格的远程数据服务WCF Data Services会是你的最佳选择。