ORM之EF初识
之前有写过ef的codefirst,今天来更进一步认识EF!
一:EF的初步认识
ORM(Object Relational Mapping):对象关系映射,其实就是一种对数据访问的封装。主要实现流程如下图:
EF:是一种通过映射操作实现数据交互的ORM框架技术
今天我们主要先初步认识一下EF6,EntityFramwork6能支持多数据库;支持函数,存储过程;并且跟VS集成的比较好;能够跟项目完美结合;能基本实现增删改查,里面有两个主要的组成部分:Context(映射数据库实例)和实体类(跟数据库的映射关系表)
Visual Studio可以使用四种方式来创建EF的项目,分别如下:
第一是:EntityFramework DBFirst,这个是数据库优先,传统的开发模式,有个很重的edmx
第二种是:EntityFramework codeFirst from db && codeFirst,这个代码先行,不关心数据库,从业务出发,然后能自动生成数据库。
二:ef中我们如果想要看到底层生成的sql语句,有两种方式:
1:使用sqlserver中的sqlprofiler 监测工具,这个每次执行都会得到相应的sql的
2:在项目中添加context.Database.Log += s => Console.WriteLine($"当前执行sql:{s}"); 这个会把每次操作数据库的日志全部打印出来的,有兴趣的可以自己试一下。
三:如果数据库的字段跟项目中实体的字段名字不匹配,可以通过下面三种方式来实现:
1:使用特性直接完成,比如在实体类头部或者字段头部增加对应的特性,如下面的【Table】和【Column】等特性:
1 [Table("JD_Commodity_001")] 2 public partial class JDCommodity001 3 { 4 public int Id { get; set; } 5 6 public long? ProductId { get; set; } 7 8 public int? CategoryId { get; set; } 9 10 [StringLength(500)] 11 [Column("Title")] 12 public string Text { get; set; } 13 }
2:在DbContext中的OnModelCreating中完成链式映射,具体代码如下:
1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 Database.SetInitializer(new CreateDatabaseIfNotExists<CodeFirstFromDBContext>()); 4 5 modelBuilder.Entity<JDCommodity002>() 6 .ToTable("JD_Commodity_002") 7 .Property(c => c.Text).HasColumnName("Title"); 8 }
3:DbContext中的OnModelCreating中增加配置文件,具体代码如下:
A:首先先创建JDCommodity003Mapping映射类然后继承于 EntityTypeConfiguration<JDCommodity003>,具体如下:
1 public class JDCommodity003Mapping : EntityTypeConfiguration<JDCommodity003> 2 { 3 public JDCommodity003Mapping() 4 { 5 this.ToTable("JD_Commodity_003"); 6 this.Property(c => c.Text).HasColumnName("Title"); 7 } 8 }
B:然后在DbContext中的OnModelCreating增加配置文件:
1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 modelBuilder.Configurations.Add(new JDCommodity003Mapping()); 4 }
通过上面三种方式都能实现实体类与数据库表对应的映射。
四:EF中复杂的查询以及写sql语句进行查询
1:EF普通的查询,使用IQuerable和linq的方式查询,如下:
1 #region 其他查询 2 using (JDDbContext dbContext = new JDDbContext()) 3 { 4 { 5 var list = dbContext.Users.Where(u => new int[] { 1, 2, 3, 5, 8, 9, 10, 11, 12, 14, 17 } 6 .Contains(u.Id));//in查询 7 //这些都是延迟加载,只有用到list的话才会实际去查询数据库 8 foreach (var user in list) 9 { 10 Console.WriteLine(user.Name ?? "Name为空"); 11 } 12 } 13 { 14 //没有任何差别,只有写法上的熟悉 15 var list = from u in dbContext.Users 16 where new int[] { 1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 14 }.Contains(u.Id) 17 where u.Id>14 18 select u; 19 //上面这个也是延迟加载,只有用到list的话才会实际去查询数据库 20 foreach (var user in list) 21 { 22 Console.WriteLine(user.Name??"name为空"); 23 } 24 } 25 { 26 var list = dbContext.Users.Where(u => new int[] { 1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 14, 18, 19, 20, 21, 22, 23 }.Contains(u.Id)) 27 .OrderBy(u => u.Id) 28 .Select(u => new 29 { 30 Account = u.Account, 31 Pwd = u.Password 32 }).Skip(3).Take(5); 33 foreach (var user in list) 34 { 35 Console.WriteLine(user.Pwd); 36 } 37 } 38 { 39 var list = (from u in dbContext.Users 40 where new int[] { 1, 2, 3, 5, 7, 8, 9, 10, 11, 12, 14 }.Contains(u.Id) 41 orderby u.Id 42 select new 43 { 44 Account = u.Account, 45 Pwd = u.Password 46 }).Skip(3).Take(5); 47 48 foreach (var user in list) 49 { 50 Console.WriteLine(user.Account); 51 } 52 } 53 54 { 55 var list = dbContext.Users.Where(u => u.Name.StartsWith("小") && u.Name.EndsWith("新")) 56 .Where(u => u.Name.EndsWith("新")) 57 .Where(u => u.Name.Contains("小新")) 58 .Where(u => u.Name.Length < 5) 59 .OrderBy(u => u.Id); 60 61 foreach (var user in list) 62 { 63 Console.WriteLine(user.Name); 64 } 65 } 66 { 67 var list = from u in dbContext.Users 68 join c in dbContext.Companies on u.CompanyId equals c.Id 69 where new int[] { 1, 2, 3, 4, 6, 7, 10 }.Contains(u.Id) 70 select new 71 { 72 Account = u.Account, 73 Pwd = u.Password, 74 CompanyName = c.Name 75 }; 76 foreach (var user in list) 77 { 78 Console.WriteLine("{0} {1}", user.Account, user.Pwd); 79 } 80 } 81 { 82 var list = from u in dbContext.Users 83 join c in dbContext.Categories on u.CompanyId equals c.Id 84 into ucList 85 from uc in ucList.DefaultIfEmpty() 86 where new int[] { 1, 2, 3, 4, 6, 7, 10 }.Contains(u.Id) 87 select new 88 { 89 Account = u.Account, 90 Pwd = u.Password 91 }; 92 foreach (var user in list) 93 { 94 Console.WriteLine("{0} {1}", user.Account, user.Pwd); 95 } 96 } 97 } 98 #endregion
2:EF自定义sql语句,然后EF框架自己调用sql执行sql,可以使用ado.net自带的事务来操作。
1 #region 自定义sql,然后ef框架调用 2 using (JDDbContext dbContext = new JDDbContext()) 3 { 4 { 5 DbContextTransaction trans = null; 6 try 7 { 8 trans = dbContext.Database.BeginTransaction(); 9 string sql = "Update [User] Set Name='小新' WHERE Id=@Id"; 10 SqlParameter parameter = new SqlParameter("@Id", 1); 11 dbContext.Database.ExecuteSqlCommand(sql, parameter); 12 trans.Commit(); 13 } 14 catch (Exception ex) 15 { 16 if (trans != null) 17 trans.Rollback(); 18 throw ex; 19 } 20 finally 21 { 22 trans.Dispose(); 23 } 24 } 25 { 26 DbContextTransaction trans = null; 27 try 28 { 29 trans = dbContext.Database.BeginTransaction(); 30 string sql = "SELECT * FROM [User] WHERE Id=@Id"; 31 SqlParameter parameter = new SqlParameter("@Id", 1); 32 List<User> userList = dbContext.Database.SqlQuery<User>(sql, parameter).ToList<User>(); 33 trans.Commit(); 34 } 35 catch (Exception ex) 36 { 37 if (trans != null) 38 trans.Rollback(); 39 throw ex; 40 } 41 finally 42 { 43 trans.Dispose(); 44 } 45 } 46 } 47 #endregion
五:EF的IQuerable延迟和IEnumerable的延迟加载的比较跟区别,首先我们写了一个例子如下:
1 #region 2 //userList是IQueryable类型,数据在数据库里面, 3 //这个list里面有表达式目录树---返回值类型--IQueryProvider(查询的支持工具,sqlserver语句的生成) 4 //其实userList只是一个包装对象,里面有表达式目录树,有结果类型,有解析工具,还有上下文, 5 //真需要数据的时候才去解析sql,执行sql,拿到数据的---因为表达式目录树可以拼装; 6 IQueryable<User> sources = null; 7 using (JDDbContext dbContext = new JDDbContext()) 8 { 9 sources = dbContext.Set<User>().Where(u => u.Id > 20); 10 //延迟查询也要注意:a 迭代使用时,用完了关闭连接 b 脱离context作用域则会异常 11 foreach (var user in sources)//sources必须在该dbContext的using范围内使用 12 { 13 Console.WriteLine(user.Name); 14 } 15 Console.WriteLine("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&"); 16 17 var userList = dbContext.Set<User>().Where(u => u.Id > 10);//1 这句话执行完,没有数据库查询 18 foreach (var user in userList)//2 迭代遍历数据才去数据库查询--在真实需要使用数据时,才去数据库查询的 19 { 20 Console.WriteLine(user.Name); 21 } 22 //IEnumerator 23 //这就是延迟查询,可以叠加多次查询条件,一次提交给数据库;可以按需获取数据; 24 userList = userList.Where(u => u.Id < 100); 25 userList = userList.Where(u => u.State < 3); 26 userList = userList.OrderBy(u => u.Name); 27 28 var list = userList.ToList<User>();//ToList() ,Count(), FitstOrDefalut()都会自动去调用数据库 29 } 30 //这个时候查询,已经超出作用域。会异常,所以必须在using范围内使用 31 //foreach (var user in sources) 32 //{ 33 // Console.WriteLine(user.Name); 34 //} 35 36 //intList实现了IEnumerable类型,这里是延迟的,其实数据已经在内存里,利用的是迭代器的方式,每次去迭代访问时,才去筛选一次,委托+迭代器 37 { 38 List<int> intList = new List<int>() { 123, 4354, 3, 23, 3, 4, 4, 34, 34, 3, 43, 43, 4, 34, 3 }; 39 var list = intList.Where(i => 40 { 41 Thread.Sleep(i); 42 return i > 10; 43 });//没有过滤 44 foreach (var i in list)//才去过滤 45 { 46 Console.WriteLine(i); 47 } 48 Console.WriteLine("*********************"); 49 } 50 #endregion
1:IEnumerable:利用的是迭代器的方式,每次去迭代访问时,才去筛选一次,委托+迭代器
2:IQueryable:只是一个包装对象,里面有表达式目录树,有结果类型,有解析工具,还有上下文,真需要数据的时候才去解析sql,执行sql,拿到数据的---因为表达式目录树可以拼装;
六:EF状态的跟踪变化
1:ef内置的监控
1 User userNew = new User() 2 { 3 Account = "Admin", 4 State = 0, 5 CompanyId = 4, 6 CompanyName = "万达集团", 7 CreateTime = DateTime.Now, 8 CreatorId = 1, 9 Email = "loverwangshan@qq.com", 10 LastLoginTime = null, 11 LastModifierId = 0, 12 LastModifyTime = DateTime.Now, 13 Mobile = "18664876671", 14 Name = "intitName", 15 Password = "12356789", 16 UserType = 1 17 }; 18 using (JDDbContext context = new JDDbContext()) 19 { 20 Console.WriteLine(context.Entry<User>(userNew).State);//实体跟context没关系 Detached 21 userNew.Name = "小鱼"; 22 context.SaveChanges();//如果状态为Detached的时候,SaveChanges什么也不会做的 23 24 context.Users.Add(userNew); 25 Console.WriteLine(context.Entry<User>(userNew).State);//Added 26 27 context.SaveChanges();//插入数据(自增主键在插入成功后,会自动赋值过去) 28 Console.WriteLine(context.Entry<User>(userNew).State);//Unchanged(跟踪,但是没变化) 29 30 userNew.Name = "加菲猫";//一旦任何字段修改----内存clone 31 Console.WriteLine(context.Entry<User>(userNew).State);//Modified 32 context.SaveChanges();//更新数据库,因为状态是Modified 33 Console.WriteLine(context.Entry<User>(userNew).State);//Unchanged(跟踪,但是没变化) 34 35 context.Users.Remove(userNew); 36 Console.WriteLine(context.Entry<User>(userNew).State);//Deleted 37 context.SaveChanges();//删除数据,因为状态是Deleted 38 Console.WriteLine(context.Entry<User>(userNew).State);//Detached已经从内存移除了 39 }
通过执行上面的代码,我们能看到每一步的操作状态,其实EF本身是依赖监听变化,如果有任何字段发生改变(会拿当前字段的值跟内存进行比较因此晓得是否发生改变),则会把context.Entry<User>(user20).State修改为Modified的,然后SaveChanges是以context为标准的,如果监听到任何数据的变化,会一次性的保存到数据库去,而且会开启事务!
2:因为EF默认会对与context的对象有关系的一些实体进行监控,如果仅仅是做查询而不会做更新,则不需要监控,以提高效率,下面代码是取消监控:
1 using (JDDbContext context = new JDDbContext()) 2 { 3 //如果获取对象仅仅为了查询,而不会对其修改,则没有必要进行监听某个对象, 4 //可以使用AsNoTracking取消监听,这样可以提高一些效率 5 User user21 = context.Users.Where(u => u.Id == 21).AsNoTracking().FirstOrDefault(); 6 Console.WriteLine(context.Entry<User>(user21).State); //Detached 7 }
3:EF追踪对象的三种方式如下:
1 User user = null;//声明一个新的对象 2 using (JDDbContext context = new JDDbContext()) 3 { 4 User user20 = context.Users.Find(20); 5 Console.WriteLine(context.Entry<User>(user20).State); 6 user = user20; 7 } 8 9 user.Name = "滑猪小板123456789"; 10 using (JDDbContext context = new JDDbContext()) 11 { 12 Console.WriteLine(context.Entry<User>(user).State); //因为user是新的字段,跟context么有关系,所以是Detached 13 14 //第一种:如果user是新的对象,与context没有关系,则先使用Attach建立关系,然后修改字段 15 //context.Users.Attach(user);//使user跟context建立关系 16 //Console.WriteLine(context.Entry<User>(user).State);//Unchanged 17 //user.Name = "滑猪小板";//只能更新这个字段 18 //Console.WriteLine(context.Entry<User>(user).State);//Modified 19 20 //第二种:强制指定State的状态为:EntityState.Modified,这个是默认所有的字段都会追踪 21 context.Entry<User>(user).State = EntityState.Modified;//全字段更新 22 Console.WriteLine(context.Entry<User>(user).State);//Modified 23 24 //第三种:使用context直接查询出来的字段,是默认监听的 25 user = context.Users.Find(user.Id);//查出来自然是监听 26 user.Name = "ddds"; 27 Console.WriteLine(context.Entry<User>(user).State);//Modified 28 29 context.SaveChanges(); 30 }
刚刚有个设置整个对象的字段更新,可以通过:context.Entry<User>(user5).Property("Name").IsModified = true来指定某字段被改过 !
七:EF内置的一些缓存
1 using (JDDbContext context = new JDDbContext()) 2 { 3 var userList = context.Users.Where(u => u.Id > 10).ToList(); 4 //var userList = context.Users.Where(u => u.Id > 10).AsNoTracking().ToList(); 5 Console.WriteLine(context.Entry<User>(userList[3]).State); 6 Console.WriteLine("*********************************************"); 7 var user5 = context.Users.Find(5); 8 Console.WriteLine("*********************************************"); 9 var user1 = context.Users.Find(30); 10 Console.WriteLine("*********************************************"); 11 var user2 = context.Users.FirstOrDefault(u => u.Id == 30); 12 Console.WriteLine("*********************************************"); 13 var user3 = context.Users.Find(30); 14 Console.WriteLine("*********************************************"); 15 var user4 = context.Users.FirstOrDefault(u => u.Id == 30); 16 }
Find可以使用缓存,优先从内存查找(限于同一个context),但是linq时不能用缓存,每次都是要查询的
八:DbContext的一些声明周期以及用法
1:DbContext的SaveChanges是开启事务的,任何一个失败直接全部失败,如下:
1 #region 多个数据操作一次savechange,任何一个失败直接全部失败 2 using (JDDbContext dbContext = new JDDbContext()) 3 { 4 User userNew = new User() 5 { 6 Account = "Admin", 7 State = 0, 8 CompanyId = 4, 9 CompanyName = "万达集团", 10 CreateTime = DateTime.Now, 11 CreatorId = 1, 12 Email = "dddd@qq.com", 13 LastLoginTime = null, 14 LastModifierId = 0, 15 LastModifyTime = DateTime.Now, 16 Mobile = "2222", 17 Name = "aaa", 18 Password = "12356789", 19 UserType = 1 20 }; 21 dbContext.Users.Add(userNew); 22 23 User user17 = dbContext.Users.FirstOrDefault(u => u.Id == 17); 24 user17.Name += "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 25 26 User user18 = dbContext.Set<User>().Find(18); 27 user18.Name += "bbb"; 28 29 //Company company2019 = dbContext.Set<Company>().Find(2019); 30 //dbContext.Companies.Remove(company2019); 31 32 dbContext.SaveChanges(); 33 } 34 #endregion
2:多context实例 join 不行,因为上下文环境不一样;除非把数据都查到内存,再去linq,如下代码会报错:
1 using (JDDbContext dbContext1 = new JDDbContext()) 2 using (JDDbContext dbContext2 = new JDDbContext()) 3 { 4 var list = from u in dbContext1.Users 5 join c in dbContext2.Companies on u.CompanyId equals c.Id 6 where new int[] { 1, 2, 3, 4, 6, 7, 10 }.Contains(u.Id) 7 select new 8 { 9 Account = u.Account, 10 Pwd = u.Password, 11 CompanyName = c.Name 12 }; 13 foreach (var user in list) 14 { 15 Console.WriteLine("{0} {1}", user.Account, user.Pwd); 16 } 17 }
3:context的一些使用建议:
- DbContext是个上下文环境,里面内置对象跟踪,会开启链接(就等于一个数据库链接)
- 一次请求,最好是一个context;
- 多个请求 /多线程最好是多个实例;
- 用完尽快释放;
九:多个数据源的事务
在第八条我们晓得不同的DbContext是没办法jioin联合查询的,那现在如果有一个需求,如果我想操作不同的数据源,又想放在一个事务里面统一操作,这个需要怎么做呢,.net帮我们提供了一个TransactionScope,具体实现如下:
1 using (JDDbContext dbContext1 = new JDDbContext()) 2 using (JDDbContext dbContext2 = new JDDbContext()) 3 { 4 using (TransactionScope trans = new TransactionScope()) 5 { 6 User userNew1 = new User() 7 { 8 Account = "Admin", 9 State = 0, 10 CompanyId = 2031, 11 CompanyName = "wwww", 12 CreateTime = DateTime.Now, 13 CreatorId = 1, 14 Email = "dddd@qq.com", 15 LastLoginTime = null, 16 LastModifierId = 0, 17 LastModifyTime = DateTime.Now, 18 Mobile = "adfadf", 19 Name = "民工甲123333333", 20 Password = "12356789", 21 UserType = 1 22 }; 23 dbContext1.Set<User>().Add(userNew1); 24 dbContext1.SaveChanges(); 25 26 SysLog sysLog = new SysLog() 27 { 28 CreateTime = DateTime.Now, 29 CreatorId = userNew1.Id, 30 LastModifierId = 0, 31 LastModifyTime = DateTime.Now, 32 Detail = "12345678", 33 Introduction = "sadsfghj", 34 LogType = 1, 35 UserName = "zhangsanan2zhangsanan2zhangsanan2zhangsanan2333333333" 36 }; 37 dbContext2.Set<SysLog>().Add(sysLog); 38 dbContext2.SaveChanges(); 39 40 trans.Complete();//能执行这个,就表示成功了; 41 } 42 }
这样就能实现不同的数据源使用同一个事务了。
十:EF的导航属性
如果EF中有表对应的关系,一对多一对一,则如果想要查询出来一个表的同时把另外一个表也对应的查询出来,则可以使用导航属性,比如我们一个用户表属于某个公司,再查询用户表的时候想要把公司的信息给带出来,则可以进行如下操作:
1 [Table("User")] 2 public partial class User 3 { 4 public int Id { get; set; } 5 6 public int CompanyId { get; set; } 7 8 [StringLength(500)] 9 public string CompanyName { get; set; } 10 11 public int State { get; set; } 12 13 public int UserType { get; set; } 14 15 public DateTime? LastModifyTime { get; set; } 16 17 [ForeignKey("CompanyId")] 18 public virtual Company Company { get; set; } 19 }
注意:一般导航属性,实体与主键对应最好使用ForeignKey来标识一下,不要使用.netFramwork对应的框架那种默认方式去做,比较不明确。
然后查询如下:
1 using (JDDbContext dbContext = new JDDbContext()) 2 { 3 var userList = dbContext.Set<User>().Where(c => c.Id < 20); 4 foreach (var user in userList)//只差company 5 { 6 Console.WriteLine($"用户Id:{user.Id}"); 7 Console.WriteLine($"公司名字:{user.Company?.Name}"); //每次会重新去加载用户信息 8 } 9 }
导航加载是懒加载,就是只有用到才会去读取。比如每次使用到user.Company都会重新去查询数据。所以这样会增加了对数据库的频繁打开跟关闭。如果要禁止懒加载,可以通过下面的Include来强制加载:
如果想要禁止懒加载,即每次查询的时候,也不想去查询数据,可以通过下面的方式来配置: