实体数据对象状态

在EF环境下,应用程序更改数据对象会引发数据集状态的变更,可能的状态有以下几种:

数据对象状态列表
Added 添加实体对象创建到实体集中,数据未添加进数据库
Modified 实体对象已经存在于实体数据集中,数据库同时存在对应的数据,但某些实体对象属性值已经改变,未更新到数据库中
Deleted 实体对象已经存在于实体数据集中,数据库同时存在对应的数据,但是实体对象本身被标识为删除
Unchanged 实体对象存在于数据集中,数据库同时包含对应未曾更改的相同数据
Detached 实体对象已经不在数据集中

SaveChanges方法被调用时:

1、Added状态会将新实体对象的属性数据更新到数据库。

2、Modified状态实体对象的属性值就会逐一更新到数据库中对应的数据字段。

3、Deleted则将其中对应实体对象的数据删除。

4、当实体对象为Added或者Modified状态时,调用SaveChanges方法更新数据成功后,会将状态调整为Unchanged

5、当实体对象为Deleted状态时,调用SaveChanges方法更新数据成功后,会将对象的状态调整为Detached

可以使用DbContext.Entry方法获取对象的状态。

下面有代码说明举例,假设存在如下实体类:

    public class Product{public int Id { get; set; }public string Name { get; set; }public string Category { get; set; }public int Price { get; set; }}

上下文类代码如下图所示:

    public class SaveChangesModel : DbContext{public SaveChangesModel(): base("name=SaveChangesModel"){}public virtual DbSet<Product> Product { get; set; }}

运行程序建立相应的数据库和表结构后,修改Main函数中的代码如下图所示:

static void Main(string[] args)
{using (SaveChangesModel db = new SaveChangesModel()){Product product = new Product{Name = "电脑",Category = "办公用品",Price = 5000,};Console.WriteLine("初始状态:{0}", db.Entry(product).State.ToString());db.Product.Add(product);Console.WriteLine("Add后的状态:{0}", db.Entry(product).State.ToString());Console.ReadKey();}
}

运行后效果如下图所示:

可以看到,一开始product对象创建完成后是Detached状态,表示其与数据库无任何关联,调用Add方法后,转化成为Added状态,表示这一组数据对象当前是准备加入数据库的状态。

EF根据数据对象的状态决定是否进行底层数据库的变更,并且通过状态调整达到数据变更的目的。

可以将其中代码部分修改下,如下图所示:

//db.Product.Add(product);
db.Entry(product).State = System.Data.Entity.EntityState.Added;

其中直接将Product对象的state设置为Added,效果与Add方法相同,因此不需要明确调用Add方法,只需要通过状态标识即可完成添加操作


更新与删除

更新和删除操作必须先要找到数据集中相应的数据才能进行操作。

Find()方法可以利用数据表的主键进行快速查询出想要的数据。更新操作的示例代码如下图所示:

static void Main(string[] args)
{using (SaveChangesModel db = new SaveChangesModel()){var product = db.Product.Find(1);Console.WriteLine("初始状态:{0}", db.Entry(product).State.ToString());product.Price = 4500;Console.WriteLine("修改后状态:{0}", db.Entry(product).State.ToString());db.SaveChanges();Console.ReadKey();}
}

运行结果如下图所示:

删除效果的实例代码如下图所示:

static void Main(string[] args)
{using (SaveChangesModel db = new SaveChangesModel()){var product = db.Product.Find(1);Console.WriteLine("初始状态:{0}", db.Entry(product).State.ToString());db.Product.Remove(product);Console.WriteLine("删除后状态:{0}", db.Entry(product).State.ToString());db.SaveChanges();Console.ReadKey();}
}

运行结果下图所示:


Attach

DbSet类定义了一个Attach方法,此方法定义接收一个实体数据对象参数,将其附加到数据集中,等同于将此数据直接从数据库取出并转换成对应的数据对象,而附加完成之后,entity的状态时Unchanged,通过修改其状态,即可通过SaveChanges方法变成变更更新操作。

修改Main函数中的方法如下图所示:

static void Main(string[] args)
{using (SaveChangesModel db = new SaveChangesModel()){Product product = new Product{Id = 1,Name = "电脑",Category = "办公用品",Price = 6000,};Console.WriteLine("初始状态:{0}", db.Entry(product).State.ToString());db.Product.Attach(product);Console.WriteLine("Attach后状态:{0}", db.Entry(product).State.ToString());db.Entry(product).State = System.Data.Entity.EntityState.Modified;Console.WriteLine("Modified后状态:{0}", db.Entry(product).State.ToString());Console.ReadKey();}
}

运行后结果如下图所示:


变更追踪——DbContext.ChangeTracker

DbContext会对实体的更新操作进行追踪,如果想要存取变更状态的信息,可以通过DbContext.ChangeTracker属性的调用来获取支持追踪功能的DbChangeTracker对象,语句如下:

DbChangeTracker tracker = context.changeTracker

DbChangeTracker定义了Entries方法,执行这个方法返回的是一个当前DbContext追踪的实体对象,其相关的IEnumerable<DbEntityEntry>集合,进一步调用此方法即可逐一取出所有的DbEntityEntry对象,并提取所需的实体对象变更信息。

示例代码如下图所示:

static void Main(string[] args)
{using (SaveChangesModel db = new SaveChangesModel()){var product = db.Product.Find(1);DbChangeTracker tracker = db.ChangeTracker;EntryInfo(tracker, "首次载入");product.Name = "惠普电脑";product.Price = 3000;EntryInfo(tracker,"修改一项商品数据的名称");db.Product.Add(new Product{Name = "鼠标",Price = 19,Category = "办公用品",});EntryInfo(tracker, "添加一项商品数据");var product2 = db.Product.Find(2);db.Product.Remove(product2);EntryInfo(tracker,"删除一项商品数据");Console.Read();}
}static void EntryInfo(DbChangeTracker tracker,string desc)
{Console.WriteLine("\n{0}: ",desc);Console.WriteLine("".PadRight(48, '.'));IEnumerable<DbEntityEntry> entries = tracker.Entries();foreach (DbEntityEntry entry in entries){Console.WriteLine("变更实体:{0}",entry.Entity.GetType().FullName);EntityState state = entry.State;Console.WriteLine("状态:{0}", state);if (state != EntityState.Deleted){if (state != EntityState.Added){PropertyList(entry.GetDatabaseValues(),"当前数据库中的副本");PropertyList(entry.OriginalValues,"属性值");}if (state != EntityState.Unchanged){PropertyList(entry.CurrentValues, "变更后的属性值");}Console.WriteLine("//");}}
}static void PropertyList(DbPropertyValues values,string desc)
{Console.WriteLine("{0}:", desc);foreach (string name in values.PropertyNames){Console.WriteLine("\t{0}:{1}", name, values[name]);}
}

修改实体部分的运行结果:

新增实体部分的运行结果:

删除实体部分的运行结果:


更新验证异常——DbEntityValidationException

数据进入数据库之前,很有可能发生各种更新错误,因此必须进行各种验证以确保正确性。当我们执行SaveChanges方法进行底层数据更新操作时,EF会根据实体类的属性逐一进行验证,以决定是否执行数据的更新操作,避免错误数据进入数据库。一旦出现验证不符的数据内容,就会产生DbEntityVaildationException异常。

下面通过例子说明,新建控制台项目SaveChangesEX,并添加如下实体类:

    [Table("Product")]public class Product{[Key]public int Id { get; set; }[Required]public string Name { get; set; }[Range(100,5000)]public int Price { get; set; }[Range(100,5000)]public int SPrice { get; set; }}

上下文类代码如下图所示:

    public class SaveChangesEXModel : DbContext{public SaveChangesEXModel(): base("name=SaveChangesEXModel"){}public virtual DbSet<Product> Product { get; set; }}

首次运行项目在数据库中建立的表结构如下图所示:

修改Main函数中的代码如下图所示:

static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){try{Product product = new Product{Name = null,Price = -40,SPrice = -120,};db.Product.Add(product);int count = db.SaveChanges();}catch(Exception ex){Console.WriteLine("\n错误信息:{0}\n",ex.Message);}Console.ReadKey();}
}

以上代码创建了一个新的Product对象,并将其name属性赋值为null, 这是无法通过验证的,同理,Price属性和SPrice属性。

运行效果如下图所示:

下面进一步扩展catch子句中的代码:

static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){try{Product product = new Product{Name = null,Price = -40,SPrice = -120,};db.Product.Add(product);int count = db.SaveChanges();}catch(Exception ex){Console.WriteLine("\n错误信息:{0}\n",ex.Message);if (ex is DbEntityValidationException){foreach (DbEntityValidationResult validationResult in ((DbEntityValidationException)ex).EntityValidationErrors){foreach (DbValidationError error in validationResult.ValidationErrors){Console.WriteLine(".....{0}",error.ErrorMessage);}}}}Console.ReadKey();}
}

DbEntityValidationException.EntityValidationErrors属性返回与此实体对象有关的验证错误,每一个验证错误封装为DbEntityValidationResult 对象,最后形成IEnumerable<DbEntityValidationResult>返回,因此我们可以通过foreach循环列举其中所有的DbEntityValidationResult 对象。

每一个实体相关的验证错误进一步封装在DbEntityValidationResult.ValidationErrors属性返回的ICollection<DbVaildationError>集合中,通过其中的DbValidationError.ValidationErrors可以取出真正的信息。

如果想要同时获取造成这个错误的关键属性。可以调用DbValidationError.PropertyName以获取属性名称。

运行效果如下图所示:

上述输出结果中,除了原来的信息外,同时还列出了每一组属性专属的错误信息,这些信息是属性数据注解内置的默认消息正文。如果需要设置ErrorMessage指定输出信息,可以修改Product.cs如下所示:

    [Table("Product")]public class Product{[Key]public int Id { get; set; }[Required(ErrorMessage ="必须指定商品名称")]public string Name { get; set; }[Range(100,5000,ErrorMessage ="商品价格范围10-500")]public int Price { get; set; }[Range(100,5000,ErrorMessage ="商品特价范围10-500")]public int SPrice { get; set; }}

运行效果如下图所示:


覆写DbContext.ValidateEntity方法

可以覆写DbContext.ValidateEntity方法自定义验证程序,以进一步调整输出验证结果,修改上下文类代码如下图所示:

public class SaveChangesEXModel : DbContext
{public SaveChangesEXModel(): base("name=SaveChangesEXModel"){}public virtual DbSet<Product> Product { get; set; }protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items){var list = new List<DbValidationError>();if (entityEntry.CurrentValues.GetValue<string>("Name") == null || entityEntry.CurrentValues.GetValue<string>("Name").Length < 4){list.Add(new DbValidationError("Name", "商品名称必须多于四个字符"));}if (entityEntry.CurrentValues.GetValue<int>("Price") < 0){list.Add(new DbValidationError("Price","商品价格不得小于0"));}if (entityEntry.CurrentValues.GetValue<int>("SPrice") < 0){list.Add(new DbValidationError("SPrice", "商品特价不得小于0"));}if (list.Count() > 0){return new DbEntityValidationResult(entityEntry, list);}else{return base.ValidateEntity(entityEntry, items);}}
}

再次运行程序会得到如下结果:


覆写SaveChange方法

在EF环境下,数据变更是最后调用SaveChanges方法将数据正式更新到数据库,如果在更新过程中有需要执行的程序代码,可以尝试直接在实体模型中覆写这个方法。

我们可以将处理验证异常的相关代码覆写到SaveChange方法中,如此一来,就不需要每次调用SaveChange时处理相关问题。

修改上下文代码如下图所示:

    public class SaveChangesEXModel : DbContext{public SaveChangesEXModel(): base("name=SaveChangesEXModel"){}public virtual DbSet<Product> Product { get; set; }public override int SaveChanges(){try{return base.SaveChanges();}catch (Exception ex){string message = ex.Message;if (ex is DbEntityValidationException){message = "验证异常\n";foreach (var validationResult in ((DbEntityValidationException)ex).EntityValidationErrors){foreach (var error in validationResult.ValidationErrors){message += ("......" + error.ErrorMessage + "\n");}}}throw new Exception(message);}           }}

修改Main方法中代码如下图所示:

static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){try{Product product = new Product{Name = null,Price = -40,SPrice = -120,};db.Product.Add(product);int count = db.SaveChanges();Console.WriteLine("添加了{0}项数据!", count);}catch(Exception ex){Console.WriteLine("\n错误信息:{0}\n", ex.Message);}Console.ReadKey();}
}

运行结果如下图所示:


SQL语句

EF支持SQL语句以便我们随时进行数据的增删改查操作。示例代码如下图所示:

using System.Data.Entity.Infrastructure;static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){string sql = "SELECT * FROM dbo.Product";DbSqlQuery<Product> query = db.Product.SqlQuery(sql);List<Product> products = query.ToList();Console.WriteLine("商品项数:{0}",products.Count());foreach (Product product in products){Console.WriteLine("{0}\t售价:{1},特价:{2}",product.Name,product.Price,product.SPrice);}Console.ReadKey();}
}

运行结果如下图所示:

注意以下几点:

1、SQl语句语法必须正确, 不然报错。

2、SQl语句得到的属性值个数必须和指定的类型的属性个数相同。比如,select Name,Price From Product 这句Sql中只有Name和Price两个属性,无法顺利转化成Product对象,故会报错。

当查询结果属性个数不同时可以使用如下方法,修改Main函数中代码如下图所示:

static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){string sql = "SELECT Name FROM dbo.Product";DbRawSqlQuery<string> query = db.Database.SqlQuery<string>(sql);//此方法List<string> productsName = query.ToList();Console.WriteLine("商品项数:{0}", productsName.Count());foreach (string productName in productsName)Console.WriteLine("{0}", productName);}Console.ReadKey();
}

如果需要返回多个字段,可以预先创建对应的类进行转换,如下图所示:

    public class SProduct{public int Id { get; set; }public string Name { get; set; }public int Price { get; set; }}

修改Main函数中的代码:

static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){string sql = "SELECT Name,Price FROM dbo.Product";DbRawSqlQuery<SProduct> query = db.Database.SqlQuery<SProduct>(sql);List<SProduct> products = query.ToList();Console.WriteLine("商品项数:{0}", products.Count());foreach (SProduct product in products){Console.WriteLine("{0} ,价格:{1}", product.Name,product.Price);}Console.ReadKey();}
}

在SQL语句中使用参数

不当的SQL语句很容易遭受黑客的注入攻击,我们可以使用参数动态建立所需要的SQL语句。代码如下:

static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){SqlParameter P0 = new SqlParameter("P0",8);SqlParameter P1 = new SqlParameter("P1", "%移动%");object[] parameters = { P0,P1 };string sql = "select Id,Name,Price From Product Where Id >@P0 AND Name LIKE @P1 ";DbRawSqlQuery<SProduct> query = db.Database.SqlQuery<SProduct>(sql, parameters);List<SProduct> Products = query.ToList();}
}

上述SQL语句查找Id大于8,并且名字包含“移动”的数据。


执行非查询变更指令——ExecuteSqlCommand

Update、InsertInto、delete等更新操作必须通过ExecuteSqlCommand方法,而且不会返回任何数据集。语句如下:

int count = context.Database.ExecuteSqlCommand(sql)

上述的程序代码返回变更的数据项,count 记录了被更新的项数。

using (SaveChangesEXModel db = new SaveChangesEXModel())
{string sql = "UPDATE Product SET SPrice = -1 Where Price > 100";int count = db.Database.ExecuteSqlCommand(sql);Console.WriteLine("更新数据项数:{0}",count);Console.ReadKey();
}

同样可以在SQL语句中使用参数:

using (SaveChangesEXModel db = new SaveChangesEXModel())
{SqlParameter P0 = new SqlParameter("P0", -1);SqlParameter P1 = new SqlParameter("P1", 100);object[] parameters = { P0, P1 };string sql = "UPDATE Product SET SPrice = @P0 Where Price > @P1";int count = db.Database.ExecuteSqlCommand(sql,parameters);Console.WriteLine("更新数据项数:{0}",count);Console.ReadKey();
}

使用Local查询追踪本地数据集

可以使用DbSet.Local属性返回当前系统本地的数据内容,也就是DbContext对象追踪的数据集内容。

参考如下代码:

context.Product.Count();

context.Product.Local.Count();

第一条会送出SQL语句查询,返回数据库中对应的数据表中所储存的记录条数。第二行则是针对本地的DbSet对象读取其中的数据项数(或者记录条数),由于没有执行查询,DbContext并没有获取任何数据进行处理,因此,第二条的值为0.

static void Main(string[] args)
{using (SaveChangesEXModel db = new SaveChangesEXModel()){var count = db.Product.Count();Console.WriteLine("数据库中数据的条数:{0}",count);try{Product product = new Product{Name = "茶杯",Price = 10,SPrice = 5,};db.Product.Add(product);Console.WriteLine("本地的数据条数:{0}", db.Product.Local.Count);}catch (Exception ex){Console.WriteLine("\n错误信息:{0}\n", ex.Message);}Console.ReadKey();}
}

运行结果如下图所示:


local查询

EF只有真正取出数据内容时才会进行SQL语句的传送,而这个过程是EF自动执行的,因此不当的设计容易造成重复查询,引发严重的性能问题,若要避免这种情况,可以尝试通过Local获取载入的数据,往后对Local数据进行操作,以减少SQL查询操作。

通过以下程序代码来说明:

using(var context = new KTStoreModel)
{var products = context.product.Where(p => p.price > 100);Product firstProduct = products.First();Console.WriteLine("\n第一项商品数据名称:{0}\n",firstProduct.Name);Console.Writeline("\n所有商品数据列表:\n");foreach(var product in products){Console.WriteLine(Product.Name);}Console.Read()
}

上述代码会导致SQL语句执行两次,第一次是查询第一项商品数据,第二次是查询所有商品数据。

改写代码,将其中Where方法的调用调整如下:

context.Product.Where(p => p.price > 100).Load();
var products = context.product.local;

第一次直接调用Load方法预先将数据载入DbContext,并且通过Local属性的引用取回载入的数据内容并存储于Products变量中。由于直接查询Local属性获取本地数据,因此不需要重复执行SQL语句,这对于性能的提升会有一定帮助。


EntityFramework进阶——数据编辑与维护相关推荐

  1. EntityFramework进阶(三)- 根据IQueryable获取DbContext

    本系列原创博客代码已在EntityFramework6.0.0测试通过,转载请标明出处 有时候我们要通过IQueryable获取所在的DbContext信息,这是完全可以的. 以下代码从个人开源框架中 ...

  2. EntityFramework进阶——CodeFirst数据库迁移

    与视觉操作界面相比,CodeFirst机制提供了一套命令行模式的迁移工具,协助开发人员有效的进行数据库与数据模型的同步维护操作. 新建一个控制台项目CFMigrationsDemo,选用来自数据库的E ...

  3. EntityFramework进阶——继承

    通过类型继承,我们可以更弹性地处理数据,有3中相关的技巧,即TPH,TPT,TPC. Table Per Hierarchy(TPH) 当单个数据表存储不同数据类型时,在数据模型的设计上,可以使用Ta ...

  4. EntityFramework进阶——Entity Splitting和Table Splitting

    Entity Splitting--把单个实体拆分成多个表.Table Splitting--把单个表拆分成多个实体 Entity Splitting 下面通过例子来说明: 假设存在如下实体类: pu ...

  5. EntityFramework进阶——事务

    事务处理 EF支持事务处理操作,以下语句会获取当前DbContext对象专用的DbContextTransaction事务处理的对象: DbContextTransaction transaction ...

  6. EntityFramework进阶——数据变更冲突

    TimeStamp 更新操作可能伴随数据冲突,我们可以通过并发处理妥善解决这一方面的问题.避免数据冲突比较方便的做法是自动加入字节数组(byte[])类型的TimeStamp属性,对应到数据表中的ro ...

  7. IdentityServer(14)- 通过EntityFramework Core持久化配置和操作数据

    本文用了EF,如果不适用EF的,请参考这篇文章,实现这些接口来自己定义存储等逻辑.http://www.cnblogs.com/stulzq/p/8144056.html IdentityServer ...

  8. 网吧网管必备电脑常识教程篇

    主题: 网吧网管必备电脑常识教程篇 作者: 古佰(jention)  时间: 2005-12-11 18:23:39     -- [转帖]网管必备电脑常识 一.电脑基础知识 1. 电脑基础: 电脑的 ...

  9. SQL Server调优系列进阶篇(如何维护数据库索引)

    前言 上一篇我们研究了如何利用索引在数据库里面调优,简要的介绍了索引的原理,更重要的分析了如何选择索引以及索引的利弊项,有兴趣的可以点击查看. 本篇延续上一篇的内容,继续分析索引这块,侧重索引项的日常 ...

最新文章

  1. 基于OpenSSL安全会话的实现
  2. 生成4位验证码(后台)
  3. 创业故事:腾讯的创始人们
  4. PHP+Redis 实例【一】点赞 + 热度 下篇
  5. esp8266 防掉线方法_esp8266 smartconfig-智能配网分析和使用及注意事项
  6. vuepress 代码高亮支持的语言
  7. 哈希表查找失败的平均查找长度_你还应该知道的哈希冲突解决策略
  8. php二进制加密_怎样给PHP源代码加密?PHP二进制加密与解密的解决办法
  9. 小黄鸡 php,PHP调用小黄鸡 api post发送
  10. python学习10
  11. CSS3 选择器(Selector)
  12. javacv解码、录制视频,保存图片
  13. GitHub 引入缺陷和Pull Request 模版,并支持直接上传文件
  14. python课设带报告_python实时投票系统的课设报告
  15. 【Python】勒索病毒模拟
  16. Java实现飞机大战(详细思路与过程,含源代码)
  17. 数据可视化上集:使用Gliffy,ProcessOn迅速绘制论文中的流程图,系统结构图
  18. 如何让Win10 新建txt文档, 默认格式UTF-8
  19. 基于MATLAB的疲劳检测系统研究解析
  20. 学堂在线android客户端,学堂在线学生登录入口app

热门文章

  1. 记一次,jvm 内存溢出
  2. JAVA多线程(一)线程安全问题产生的原因
  3. Nginx严格访问代理HTTP资源
  4. Crawler - 如何爬取列表后进行文章的爬取
  5. x264编码参数大测试:08 subme与指定码率(1000Kbps)
  6. 如何将本地项目上传到gitee
  7. ANDROID:SHOWASACTION="NEVER"是做什么用的?
  8. Linux磁盘分区及要求
  9. python发短信脚本_python脚本发送短信
  10. java 向上抛异常_Java 异常的处理方式throws