由于对c#EntityFramework的了解还不够,滥用EF,导致我在写一个c#窗体项目的时候,遇到了这个问题并让我花费了大量的时间去修改。写下此文来记录我犯的错误。
我的项目场景是:
winfrom窗体程序,允许许多客户端同时运行在不同的机器上的,客户端分为用户和管理者。用来实现不同的功能。全部客户端访问的是同一数据库。
但是不管是winfrom窗体还是web应用程序数据使用EF(EntityFramework)出现数据不同步问题大多数情况下都是同一个问题导致的。
出现的问题场景:
一个客户端运行的时候是检查不到这个问题的,如果你的项目只是一个客户端的话就不存在这个问题。如果是多个客户端同时运行,就比如A、B客户端现在都已经打开了同一个Employee(员工)的个人信息面板,然后在A客户端上对A的信息进行更新,然后直接在B客户端刷新这个页面,而B客户端显示的信息还是之前的信息,不是A客户端更新过的最新信息。
问题的原因:
RepastEntities 类是针对我的项目VS自动生成的(派生DbContext的上下文类),DbContext类是负责与数据交互作为对象的主要类。在此贴出我的代码:
namespace Repast.Dao
{
static class RepastEntityDb
{
public static RepastEntities db = new RepastEntities();
}
}
Dao类:
namespace Repast.Dao { class EmployeeDaoImpl : EmployeeDao { private RepastEntities db =RepastEntityDb.db;
//返回所有的员工 public List<Employee> AllEmployee(string sort) { List<Employee> list = null; try { //linq sql 语句 var sql = from e in db.Employee select e; list = sql.ToList(); if (list.Count == 0) { list = null; } } catch (Exception e) { Console.WriteLine(e.Message); Console.WriteLine("Dao.EmployeeDaoImpl.AllEmployee 方法出错"); } return list; } } }
大家注意;那个RepastEntityDb类是全局静态类,由于我在整个程序中都使用了这个静态类,所以这个静态类只实例化了一次,也就是说在程序运行的全部过程中,只在启动程序的时候去从数据库中获取了信息并映射到实体,此后的所有操作都是在这个DbContext上下文类中进行的。
如果我这个项目仅仅是只有一个客户端运行,这样写是肯定没问题的,因为所有的数据库操作只在这个客户端进行,而所有的数据也保存在这个DbContext上下文类中。
但是项目是多客户端同时运行的,这就会有造成在A客户端修改后的数据,数据库中也修改成功了,但是只有A客户端能看到,其他客户端却看不到的情况。因为每个客户端的所有数据只在启动自身的时候去从数据库中获取了信息并映射到实体,当其他客户端去查看信息的时候查看的还是自身没有更新的DbContext上下文类。
解决办法:
我对我的代码进行了这样的改动,删除全局RepastEntityDb类,并对Dao类改成如下:
namespace Repast.Dao
{
class EmployeeDaoImpl : EmployeeDao
{
//创建RepastEntityDb类
private RepastEntities db = null;
public EmployeeDaoImpl(RepastEntities db)
{
this.db = db;
}
//返回所有的员工
public List<Employee> AllEmployee(string sort)
{
List<Employee> list = null;
try
{
//linq sql 语句
var sql = from e in db.Employee select e;
list = sql.ToList();
if (list.Count == 0)
{
list = null;
}
} catch (Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine("Dao.EmployeeDaoImpl.AllEmployee 方法出错");
}
return list;
}
}
}
调用的时候这样调用 EmployeeDaoImpl dao=new EmployeeDaoImpl(new RepastEntities ());
思路就是在每次调用数据库操作的时候重新从数据库中获取信息并映射到实体,这时候DbContext上下文类中存放的就是新的信息。
当然也不是每次调用数据库操作都进行一次获取新的DbContext上下文类。因为这样会造成另外一个问题!使用不当会报这个错:
一个实体对象不能由多个IEntityChangeTracker实例引用
意思就是一个实体对象不能由多个DBContext上下文对象引用修改(当前线程中存在多个DBContext上下文对象)。举个例子:
namespace Repast.Dao
{
class EmployeeFrom
{
private RepastEntities db =new RepastEntities();
//选中某行员工数据在弹出的选项卡中点击修改按钮
public void AllEmployee()
{
EmployeeDao dao=new EmployeeDao(db);
int Account=获取选中的员工的账号方法();
Employee employee=EmployeeDao.根据员工账号获取员工实体(Account);
//弹出修改员工面板(将此类获取到的实体当成参数传给修改面板)
ModifyEmployee modify=new ModifyEmployee(employee);
modify.show();
}
}
}
namespace Repast.Dao
{
class ModifyEmployee
{
private RepastEntities db =new RepastEntities();
private Employee employee=null;
public ModifyEmployee(Employee employee){
this.employee=employee;
}
//修改操作
public void Modify()
{
EmployeeDao dao=new EmployeeDao(db);
this.employee.name="测试";
//调用EmployeeDao类来修改Employee
dao.Modify(employee);
}
}
}
浏览员工界面:
修改员工界面:
然后去点击修改后,就会报错:
一个实体对象不能由多个IEntityChangeTracker实例引用
这是因为在ModifyEmployee中的要修改的Employee实体是由EmployeeFrom中的db获得的。而ModifyEmployee中又重新new RepastEntities(); ,所以这是两个不同的DbContext,进行修改操作的时候又由ModifyEmployee中的DB去修改当然会报错!
所以我们在使用的时候就要注意不要去在每次调用数据库操作都进行一次获取新的DbContext上下文类,要确保在对同一个实体操作的时候是同一个DbContext!
1、上面实例代码的修改方法有两种:要不就在EmployeeFrom类的DB也当成参数一并传给ModifyEmployee,让ModifyEmployee类中的操作使用传参过来的db,这样就保证了同一实体的操作是同一个DbContext。代码如下:
namespace Repast.Dao
{
class EmployeeFrom
{
private RepastEntities db =new RepastEntities();
//选中某行员工数据在弹出的选项卡中点击修改按钮
public void AllEmployee()
{
EmployeeDao dao=new EmployeeDao(db);
int Account=获取选中的员工的账号方法();
Employee employee=EmployeeDao.根据员工账号获取员工实体(Account);
//弹出修改员工面板(将此类获取到的实体当成参数传给修改面板)
//修改如下
//把db当成参数一并传过去
ModifyEmployee modify=new ModifyEmployee(employee,db);
modify.show();
}
}
}
namespace Repast.Dao
{
class ModifyEmployee
{
private RepastEntities db =null;
private Employee employee=null;
public ModifyEmployee(Employee employee,RepastEntities db){
this.employee=employee;
this.db=db;
}
//修改操作
public void Modify()
{
EmployeeDao dao=new EmployeeDao(db);
this.employee.name="测试";
//调用EmployeeDao类来修改Employee
dao.Modify(employee);
}
}
}
2、或者是让ModifyEmployee中employee实体是从ModifyEmployee的db中获得的。代码如下:
namespace Repast.Dao
{
class EmployeeFrom
{
private RepastEntities db =new RepastEntities();
//选中某行员工数据在弹出的选项卡中点击修改按钮
public void AllEmployee()
{
EmployeeDao dao=new EmployeeDao(db);
int Account=获取选中的员工的账号方法();
//弹出修改员工面板(把帐号当成参数传过去)
ModifyEmployee modify=new ModifyEmployee(employee);
modify.show();
}
}
}
namespace Repast.Dao
{
class ModifyEmployee
{
private RepastEntities db =new RepastEntities();
private Employee employee=null;
EmployeeDao dao=null;
public ModifyEmployee(string account){
dao=new EmployeeDao(db);
Employee employee=EmployeeDao.根据员工账号获取员工实体(Account);
this.employee=employee;
}
//修改操作
public void Modify()
{
this.employee.name="测试";
//调用EmployeeDao类来修改Employee
dao.Modify(employee);
}
}
}
以上就是我踩得坑,我举的例子是winform窗体,但是Web项目类似,按照这个思路应该都能解决。
我的博客地址 www.b0c0.com 欢迎访问