实体数据对象状态
在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语句,这对于性能的提升会有一定帮助。