EntityFramework进阶——数据编辑与维护

实体数据对象状态

在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语句,这对于性能的提升会有一定帮助。


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值